Files
AWatch-rus/windows/aw-collector-guard.ps1
T

682 lines
26 KiB
PowerShell

[CmdletBinding()]
param(
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
[ValidateSet('shadow', 'enforce')]
[string]$Mode = 'shadow',
[int]$LoopSeconds = 60,
[int]$InteractiveMaxAgeSeconds = 900,
[int]$HeadlessMaxAgeSeconds = 900,
[int]$RestartWindowSeconds = 600,
[int]$MaxRestarts = 3,
[int]$ActionCooldownSeconds = 300,
[int]$InteractiveActionCooldownSeconds = 60,
[switch]$Once,
[switch]$HeadlessEndpointEnabled,
[switch]$HeadlessFileOpsEnabled,
[switch]$SelfTest
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1'
Import-Module $modulePath -Force
function New-GuardLock {
param([string]$StateRoot)
if (-not (Test-Path -LiteralPath $StateRoot)) {
New-Item -Path $StateRoot -ItemType Directory -Force | Out-Null
}
$lockPath = Join-Path $StateRoot 'collector-guard.lock'
if (Test-Path -LiteralPath $lockPath) {
try {
$lockData = Get-Content -LiteralPath $lockPath -Raw | ConvertFrom-Json
$existingPid = [int]$lockData.pid
if ($existingPid -gt 0 -and (Get-Process -Id $existingPid -ErrorAction SilentlyContinue)) {
return $null
}
}
catch {
}
}
$payload = @{
pid = $PID
createdAt = (Get-Date).ToUniversalTime().ToString('o')
} | ConvertTo-Json -Compress
Set-Content -LiteralPath $lockPath -Value $payload -Encoding UTF8
return $lockPath
}
function Write-GuardLog {
param(
[string]$LogPath,
[string]$Message
)
try {
$directory = Split-Path -Path $LogPath -Parent
if ($directory -and -not (Test-Path -LiteralPath $directory)) {
New-Item -Path $directory -ItemType Directory -Force | Out-Null
}
Add-Content -LiteralPath $LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message)
}
catch {
}
}
function Get-AwApiBase {
param([pscustomobject]$Config)
$scheme = if ($Config.server.PSObject.Properties.Name -contains 'scheme') { [string]$Config.server.scheme } else { 'http' }
$hostName = [string]$Config.server.host
$port = [int]$Config.server.port
return ('{0}://{1}:{2}/api/0' -f $scheme, $hostName, $port)
}
function Invoke-AwJson {
param(
[Parameter(Mandatory = $true)]
[string]$Method,
[Parameter(Mandatory = $true)]
[string]$Uri,
[object]$Body
)
$params = @{
Method = $Method
Uri = $Uri
TimeoutSec = 15
ErrorAction = 'Stop'
}
if ($null -ne $Body) {
$params.Body = ($Body | ConvertTo-Json -Depth 16 -Compress)
$params.ContentType = 'application/json'
}
return Invoke-RestMethod @params
}
function Ensure-AwBucket {
param(
[string]$ApiBase,
[string]$BucketId,
[string]$ClientName,
[string]$BucketType,
[string]$Hostname
)
try {
Invoke-AwJson -Method 'GET' -Uri "$ApiBase/buckets/$BucketId" | Out-Null
return $true
}
catch {
}
try {
$body = @{
client = $ClientName
type = $BucketType
hostname = $Hostname
}
Invoke-AwJson -Method 'POST' -Uri "$ApiBase/buckets/$BucketId" -Body $body | Out-Null
return $true
}
catch {
return $false
}
}
function Get-LatestBucketAge {
param(
[string]$ApiBase,
[string]$BucketId
)
try {
$events = Invoke-AwJson -Method 'GET' -Uri "$ApiBase/buckets/$BucketId/events?limit=20"
$latest = @($events | Where-Object { $null -ne $_.timestamp } | Sort-Object timestamp -Descending | Select-Object -First 1)
if (-not $latest) {
return [pscustomobject]@{ bucket = $BucketId; found = $false; timestamp = $null; ageSeconds = $null }
}
$ts = [DateTimeOffset]::Parse([string]$latest.timestamp).UtcDateTime
$age = [Math]::Max(0, [int]((Get-Date).ToUniversalTime() - $ts).TotalSeconds)
return [pscustomobject]@{ bucket = $BucketId; found = $true; timestamp = [string]$latest.timestamp; ageSeconds = $age }
}
catch {
return [pscustomobject]@{ bucket = $BucketId; found = $false; timestamp = $null; ageSeconds = $null; error = $_.Exception.Message }
}
}
function Send-GuardHeartbeat {
param(
[string]$ApiBase,
[string]$Hostname,
[object]$State,
[int]$PulseSeconds
)
$bucketId = "aw-rus-collector-guard_$Hostname"
if (-not (Ensure-AwBucket -ApiBase $ApiBase -BucketId $bucketId -ClientName 'aw-rus-collector-guard' -BucketType 'aw.rus.collector.guard' -Hostname $Hostname)) {
return $false
}
$event = @{
timestamp = (Get-Date).ToUniversalTime().ToString('o')
duration = 0
data = $State
}
try {
Invoke-AwJson -Method 'POST' -Uri "$ApiBase/buckets/$bucketId/heartbeat?pulsetime=$PulseSeconds" -Body $event | Out-Null
return $true
}
catch {
return $false
}
}
function Read-GuardRuntime {
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path)) {
return [pscustomobject]@{ restartHistory = @{}; lastAction = @{}; quarantine = @{} }
}
try {
$state = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
if ($null -eq $state.restartHistory) { $state | Add-Member -NotePropertyName restartHistory -NotePropertyValue @{} }
if ($null -eq $state.lastAction) { $state | Add-Member -NotePropertyName lastAction -NotePropertyValue @{} }
if ($null -eq $state.quarantine) { $state | Add-Member -NotePropertyName quarantine -NotePropertyValue @{} }
return $state
}
catch {
return [pscustomobject]@{ restartHistory = @{}; lastAction = @{}; quarantine = @{} }
}
}
function Write-GuardRuntime {
param(
[string]$Path,
[object]$Runtime
)
$directory = Split-Path -Path $Path -Parent
if ($directory -and -not (Test-Path -LiteralPath $directory)) {
New-Item -Path $directory -ItemType Directory -Force | Out-Null
}
$Runtime | ConvertTo-Json -Depth 16 | Set-Content -LiteralPath $Path -Encoding UTF8
}
function Get-RuntimeMapValue {
param(
[object]$Map,
[string]$Key
)
if ($null -eq $Map) {
return $null
}
if ($Map -is [hashtable] -and $Map.ContainsKey($Key)) {
return $Map[$Key]
}
$propertyNames = @($Map.PSObject.Properties | ForEach-Object { $_.Name })
if ($propertyNames -contains $Key) {
return $Map.$Key
}
return $null
}
function Set-RuntimeMapValue {
param(
[object]$Map,
[string]$Key,
[object]$Value
)
if ($Map -is [hashtable]) {
$Map[$Key] = $Value
return
}
$propertyNames = @($Map.PSObject.Properties | ForEach-Object { $_.Name })
if ($propertyNames -contains $Key) {
$Map.$Key = $Value
}
else {
$Map | Add-Member -NotePropertyName $Key -NotePropertyValue $Value -Force
}
}
function Remove-RuntimeMapValue {
param(
[object]$Map,
[string]$Key
)
if ($null -eq $Map) {
return
}
if ($Map -is [hashtable]) {
if ($Map.ContainsKey($Key)) {
$Map.Remove($Key)
}
return
}
$property = $Map.PSObject.Properties[$Key]
if ($null -ne $property) {
$Map.PSObject.Properties.Remove($Key)
}
}
function Reset-GuardActionBudget {
param(
[object]$Runtime,
[string]$Key
)
Remove-RuntimeMapValue -Map $Runtime.restartHistory -Key $Key
Remove-RuntimeMapValue -Map $Runtime.lastAction -Key $Key
Remove-RuntimeMapValue -Map $Runtime.quarantine -Key $Key
}
function Invoke-GuardSelfTest {
$emptyObject = [pscustomobject]@{}
if ($null -ne (Get-RuntimeMapValue -Map $emptyObject -Key 'missing')) {
throw 'empty PSCustomObject should not return a missing runtime-map value'
}
Set-RuntimeMapValue -Map $emptyObject -Key 'headless:worktime-session' -Value 123
if ((Get-RuntimeMapValue -Map $emptyObject -Key 'headless:worktime-session') -ne 123) {
throw 'failed to set runtime-map value on empty PSCustomObject'
}
$hash = @{}
Set-RuntimeMapValue -Map $hash -Key 'headless:worktime-session' -Value @(1, 2)
$hashValue = @(Get-RuntimeMapValue -Map $hash -Key 'headless:worktime-session')
if ($hashValue.Count -ne 2) {
throw 'failed to round-trip runtime-map value on hashtable'
}
$runtime = [pscustomobject]@{ restartHistory = [pscustomobject]@{}; lastAction = [pscustomobject]@{}; quarantine = [pscustomobject]@{} }
$allowed = Test-ActionAllowed -Runtime $runtime -Key 'headless:worktime-session' -CooldownSeconds 1 -WindowSeconds 60 -MaxCount 3
if (-not $allowed.allowed) {
throw "expected action to be allowed, got $($allowed.reason)"
}
Register-GuardAction -Runtime $runtime -Key 'headless:worktime-session'
$blocked = Test-ActionAllowed -Runtime $runtime -Key 'headless:worktime-session' -CooldownSeconds 300 -WindowSeconds 60 -MaxCount 3
if ($blocked.allowed -or $blocked.reason -ne 'cooldown') {
throw 'expected cooldown after registering guard action'
}
$budgetRuntime = [pscustomobject]@{ restartHistory = [pscustomobject]@{}; lastAction = [pscustomobject]@{}; quarantine = [pscustomobject]@{} }
foreach ($i in 1..3) {
Register-GuardAction -Runtime $budgetRuntime -Key 'task:test'
}
$budgetBlocked = Test-ActionAllowed -Runtime $budgetRuntime -Key 'task:test' -CooldownSeconds 0 -WindowSeconds 600 -MaxCount 3
if ($budgetBlocked.allowed -or $budgetBlocked.reason -ne 'quarantine') {
throw 'expected quarantine when restart budget is exhausted'
}
Reset-GuardActionBudget -Runtime $budgetRuntime -Key 'task:test'
$budgetAllowed = Test-ActionAllowed -Runtime $budgetRuntime -Key 'task:test' -CooldownSeconds 0 -WindowSeconds 600 -MaxCount 3
if (-not $budgetAllowed.allowed) {
throw 'expected reset action budget to clear quarantine'
}
$oldComputerName = $env:COMPUTERNAME
try {
$env:COMPUTERNAME = 'SHARKON2025'
$sessionRecords = @(
[pscustomobject]@{ SessionName = 'USER5'; UserName = 'USER5'; SessionId = 2; State = 'Disc'; IsLive = $false },
[pscustomobject]@{ SessionName = 'console'; UserName = ''; SessionId = 1; State = 'Conn'; IsLive = $true }
)
$taskDefs = @(
[pscustomobject]@{ taskName = 'ActivityWatch Launch [SHARKON2025_user5]'; userId = 'SHARKON2025\user5' }
)
if (-not (Test-ActivityWatchUserHasManagedSession -UserId 'SHARKON2025\user5' -SessionRecords $sessionRecords -IncludeDisconnected)) {
throw 'expected disconnected managed session to match task user'
}
if (Test-ActivityWatchUserHasManagedSession -UserId 'SHARKON2025\user5' -SessionRecords $sessionRecords -IncludeLive) {
throw 'disconnected managed session should not match live-only filter'
}
$managed = @(Get-ActivityWatchManagedInteractiveSessions -TaskDefinitions $taskDefs -SessionRecords $sessionRecords -IncludeDisconnected)
if ($managed.Count -ne 1 -or [int]$managed[0].SessionId -ne 2) {
throw 'failed to enumerate disconnected managed session'
}
}
finally {
if ($null -eq $oldComputerName) {
Remove-Item Env:COMPUTERNAME -ErrorAction SilentlyContinue
}
else {
$env:COMPUTERNAME = $oldComputerName
}
}
Write-Output 'collector guard self-test OK'
}
function Test-ActionAllowed {
param(
[object]$Runtime,
[string]$Key,
[int]$CooldownSeconds,
[int]$WindowSeconds,
[int]$MaxCount
)
$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$last = Get-RuntimeMapValue -Map $Runtime.lastAction -Key $Key
if ($null -ne $last -and ($now - [int64]$last) -lt $CooldownSeconds) {
return [pscustomobject]@{ allowed = $false; reason = 'cooldown' }
}
$history = @(Get-RuntimeMapValue -Map $Runtime.restartHistory -Key $Key)
$history = @($history | Where-Object { ($now - [int64]$_) -le $WindowSeconds })
Set-RuntimeMapValue -Map $Runtime.restartHistory -Key $Key -Value @($history)
if ($history.Count -ge $MaxCount) {
Set-RuntimeMapValue -Map $Runtime.quarantine -Key $Key -Value @{
since = (Get-Date).ToUniversalTime().ToString('o')
reason = 'restart-budget-exhausted'
count = $history.Count
}
return [pscustomobject]@{ allowed = $false; reason = 'quarantine' }
}
Remove-RuntimeMapValue -Map $Runtime.quarantine -Key $Key
return [pscustomobject]@{ allowed = $true; reason = 'ok' }
}
function Register-GuardAction {
param(
[object]$Runtime,
[string]$Key
)
$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$history = @(Get-RuntimeMapValue -Map $Runtime.restartHistory -Key $Key)
$history += $now
Set-RuntimeMapValue -Map $Runtime.restartHistory -Key $Key -Value @($history)
Set-RuntimeMapValue -Map $Runtime.lastAction -Key $Key -Value $now
}
function Get-CollectorProcessSnapshot {
param([pscustomobject]$Config)
$scriptPaths = [ordered]@{}
foreach ($name in @('collectorScript', 'endpointCollectorScript', 'fileCollectorScript', 'emailCollectorScript', 'sessionCollectorScript')) {
if ($Config.paths.PSObject.Properties.Name -contains $name) {
$value = [string]$Config.paths.$name
if (-not [string]::IsNullOrWhiteSpace($value)) {
$scriptPaths[$name] = $value
}
}
}
$powershellCollectors = @()
try {
$processes = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
Where-Object { $_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe' })
foreach ($proc in $processes) {
$commandLine = [string]$proc.CommandLine
foreach ($entry in $scriptPaths.GetEnumerator()) {
if ($commandLine -match [Regex]::Escape([string]$entry.Value)) {
$powershellCollectors += [pscustomobject]@{
name = [string]$entry.Key
processId = [int]$proc.ProcessId
sessionId = [int]$proc.SessionId
scriptPath = [string]$entry.Value
}
}
}
}
}
catch {
}
$watchers = @()
try {
$watchers = @(Get-Process -Name 'aw-watcher-afk','aw-watcher-window' -ErrorAction SilentlyContinue |
Select-Object @{Name='name'; Expression={$_.Name}}, @{Name='processId'; Expression={$_.Id}}, @{Name='sessionId'; Expression={$_.SessionId}})
}
catch {
$watchers = @()
}
return [pscustomobject]@{
watchers = @($watchers)
collectors = @($powershellCollectors)
}
}
function Invoke-ExactTaskRun {
param([string]$TaskName)
& schtasks.exe /Run /TN $TaskName | Out-Null
return ($LASTEXITCODE -eq 0)
}
function Invoke-GuardCycle {
param(
[object]$Runtime,
[string]$RuntimePath,
[string]$LogPath
)
$config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
$stateRoot = if ($config.paths.PSObject.Properties.Name -contains 'stateRoot') { [string]$config.paths.stateRoot } else { Split-Path -Path $ConfigPath -Parent }
$hostname = if ($config.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$config.awHostname)) { [string]$config.awHostname } else { [string]$env:COMPUTERNAME }
$apiBase = Get-AwApiBase -Config $config
$configPaths = Get-ActivityWatchRecoveryConfigPaths -PrimaryConfigPath $ConfigPath
$taskDefs = @(Get-ActivityWatchRecoveryTaskDefinitions -ConfigPaths $configPaths)
$sessionRecords = @(Get-ActivityWatchSessionRecords)
$liveSessions = @(Get-ActivityWatchLiveInteractiveSessions -SessionRecords $sessionRecords)
$managedInteractiveSessions = @(Get-ActivityWatchManagedInteractiveSessions -TaskDefinitions $taskDefs -SessionRecords $sessionRecords -IncludeLive -IncludeDisconnected)
$processSnapshot = Get-CollectorProcessSnapshot -Config $config
$liveSessionIds = @($liveSessions | ForEach-Object { [int]$_.SessionId })
$managedSessionIds = @($managedInteractiveSessions | ForEach-Object { [int]$_.SessionId } | Sort-Object -Unique)
$bucketChecks = [ordered]@{}
foreach ($bucket in @(
"aw-worktime-sessions_$hostname",
"aw-watcher-afk_$hostname",
"aw-watcher-window_$hostname",
"aw-dlp-endpoint-signals_$hostname"
)) {
$bucketChecks[$bucket] = Get-LatestBucketAge -ApiBase $apiBase -BucketId $bucket
}
$actions = New-Object System.Collections.Generic.List[object]
$problems = New-Object System.Collections.Generic.List[string]
$worktimeAge = $bucketChecks["aw-worktime-sessions_$hostname"].ageSeconds
$sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' }
$sessionCollectorRunning = Test-ActivityWatchCollectorRunningGlobal -ScriptPath $sessionCollectorScript
$headlessKey = 'headless:worktime-session'
$needsHeadlessAction = (-not $sessionCollectorRunning -or $null -eq $worktimeAge -or [int]$worktimeAge -gt $HeadlessMaxAgeSeconds)
if ($needsHeadlessAction) {
$key = $headlessKey
$allowed = Test-ActionAllowed -Runtime $Runtime -Key $key -CooldownSeconds $ActionCooldownSeconds -WindowSeconds $RestartWindowSeconds -MaxCount $MaxRestarts
if ($allowed.allowed) {
if ($Mode -eq 'enforce') {
Start-ActivityWatchCollectorScriptGlobalIfNeeded -ScriptPath $sessionCollectorScript -ConfigPath $ConfigPath
Register-GuardAction -Runtime $Runtime -Key $key
Write-GuardLog -LogPath $LogPath -Message "started $key"
$actions.Add([pscustomobject]@{ action = 'start'; target = $key; applied = $true }) | Out-Null
}
else {
$actions.Add([pscustomobject]@{ action = 'start'; target = $key; applied = $false; mode = 'shadow' }) | Out-Null
}
}
else {
$problems.Add("$key action blocked: $($allowed.reason)") | Out-Null
}
}
else {
Reset-GuardActionBudget -Runtime $Runtime -Key $headlessKey
}
if ($Mode -eq 'enforce') {
Stop-ActivityWatchProcessesInNonLiveSessions -SessionRecords $sessionRecords -Config $config -TaskDefinitions $taskDefs -PreserveManagedSessions
}
$interactiveStale = $false
foreach ($bucket in @("aw-watcher-afk_$hostname", "aw-watcher-window_$hostname", "aw-dlp-endpoint-signals_$hostname")) {
$age = $bucketChecks[$bucket].ageSeconds
if ($null -eq $age -or [int]$age -gt $InteractiveMaxAgeSeconds) {
$interactiveStale = $true
}
}
$watchersInLiveSessions = @($processSnapshot.watchers | Where-Object { $liveSessionIds -contains [int]$_.sessionId })
$watchersInManagedSessions = @($processSnapshot.watchers | Where-Object { $managedSessionIds -contains [int]$_.sessionId })
$liveWatcherMissing = $false
if ($liveSessions.Count -gt 0) {
$hasAfk = @($watchersInLiveSessions | Where-Object { [string]$_.name -ieq 'aw-watcher-afk' }).Count -gt 0
$hasWindow = @($watchersInLiveSessions | Where-Object { [string]$_.name -ieq 'aw-watcher-window' }).Count -gt 0
$liveWatcherMissing = (-not $hasAfk) -or (-not $hasWindow)
}
$managedWatcherMissing = $false
if ($managedInteractiveSessions.Count -gt 0) {
$afkEnabled = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]$config.collectors.afkEnabled } else { $true }
$windowEnabled = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]$config.collectors.windowEnabled } else { $true }
foreach ($managedSession in @($managedInteractiveSessions)) {
$sessionId = [int]$managedSession.SessionId
$sessionWatchers = @($watchersInManagedSessions | Where-Object { [int]$_.sessionId -eq $sessionId })
$hasManagedAfk = @($sessionWatchers | Where-Object { [string]$_.name -ieq 'aw-watcher-afk' }).Count -gt 0
$hasManagedWindow = @($sessionWatchers | Where-Object { [string]$_.name -ieq 'aw-watcher-window' }).Count -gt 0
if (($afkEnabled -and -not $hasManagedAfk) -or ($windowEnabled -and -not $hasManagedWindow)) {
$managedWatcherMissing = $true
break
}
}
}
if ($managedInteractiveSessions.Count -eq 0 -and $liveSessions.Count -gt 0 -and $interactiveStale) {
$problems.Add('interactive buckets stale but no managed interactive sessions found') | Out-Null
}
$needsInteractiveTaskAction = $managedInteractiveSessions.Count -gt 0 -and (
$liveWatcherMissing -or
$managedWatcherMissing -or
($interactiveStale -and $liveSessions.Count -gt 0)
)
if ($needsInteractiveTaskAction) {
foreach ($taskDef in $taskDefs) {
if (-not (Test-ActivityWatchUserHasManagedSession -UserId ([string]$taskDef.userId) -SessionRecords $sessionRecords -IncludeLive -IncludeDisconnected)) {
continue
}
$key = "task:$($taskDef.taskName)"
$allowed = Test-ActionAllowed -Runtime $Runtime -Key $key -CooldownSeconds $InteractiveActionCooldownSeconds -WindowSeconds $RestartWindowSeconds -MaxCount $MaxRestarts
if (-not $allowed.allowed) {
$problems.Add("$key action blocked: $($allowed.reason)") | Out-Null
continue
}
if ($Mode -eq 'enforce') {
$ok = Invoke-ExactTaskRun -TaskName ([string]$taskDef.taskName)
if ($ok) {
Register-GuardAction -Runtime $Runtime -Key $key
}
Write-GuardLog -LogPath $LogPath -Message ("run {0} ok={1}" -f $key, $ok)
$actions.Add([pscustomobject]@{ action = 'run-task'; target = [string]$taskDef.taskName; applied = $true; ok = $ok }) | Out-Null
}
else {
$actions.Add([pscustomobject]@{ action = 'run-task'; target = [string]$taskDef.taskName; applied = $false; mode = 'shadow' }) | Out-Null
}
}
}
else {
foreach ($taskDef in $taskDefs) {
if (Test-ActivityWatchUserHasManagedSession -UserId ([string]$taskDef.userId) -SessionRecords $sessionRecords -IncludeLive -IncludeDisconnected) {
Reset-GuardActionBudget -Runtime $Runtime -Key "task:$($taskDef.taskName)"
}
}
}
$status = 'ok'
if ($problems.Count -gt 0) {
$status = 'warn'
}
if ($managedInteractiveSessions.Count -gt 0 -and $interactiveStale -and $Mode -eq 'shadow') {
$status = 'warn'
}
$sessionState = @(
foreach ($session in @($sessionRecords)) {
[pscustomobject]@{
SessionName = [string]$session.SessionName
UserName = [string]$session.UserName
SessionId = [int]$session.SessionId
State = [string]$session.State
IsLive = [bool]$session.IsLive
}
}
)
$state = @{}
$state['status'] = $status
$state['mode'] = $Mode
$state['host'] = $hostname
$state['generatedAtUtc'] = (Get-Date).ToUniversalTime().ToString('o')
$state['pid'] = $PID
$state['sessions'] = @($sessionState)
$state['liveSessionCount'] = $liveSessions.Count
$state['managedSessionCount'] = $managedInteractiveSessions.Count
$state['managedSessions'] = @($managedInteractiveSessions)
$bucketState = @{}
foreach ($key in $bucketChecks.Keys) {
$bucketState[$key] = $bucketChecks[$key]
}
$state['processes'] = $processSnapshot
$state['buckets'] = $bucketState
$state['actions'] = @($actions.ToArray())
$state['problems'] = @($problems.ToArray())
$state['quarantine'] = $Runtime.quarantine
$statePath = Join-Path $stateRoot 'collector-guard-state.json'
$state | ConvertTo-Json -Depth 16 | Set-Content -LiteralPath $statePath -Encoding UTF8
Write-GuardRuntime -Path $RuntimePath -Runtime $Runtime
[void](Send-GuardHeartbeat -ApiBase $apiBase -Hostname $hostname -State $state -PulseSeconds ([Math]::Max($LoopSeconds * 2, 60)))
return $state
}
if ($SelfTest) {
Invoke-GuardSelfTest
exit 0
}
$initialConfig = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
$initialStateRoot = if ($initialConfig.paths.PSObject.Properties.Name -contains 'stateRoot') { [string]$initialConfig.paths.stateRoot } else { Split-Path -Path $ConfigPath -Parent }
$initialLogsRoot = if ($initialConfig.paths.PSObject.Properties.Name -contains 'logsRoot') { [string]$initialConfig.paths.logsRoot } else { Join-Path $initialStateRoot 'logs' }
$logPath = Join-Path $initialLogsRoot 'collector-guard.log'
$runtimePath = Join-Path $initialStateRoot 'collector-guard-runtime.json'
$lockPath = New-GuardLock -StateRoot $initialStateRoot
if (-not $lockPath) {
Write-GuardLog -LogPath $logPath -Message 'another collector guard instance is already running'
exit 0
}
try {
$runtime = Read-GuardRuntime -Path $runtimePath
Write-GuardLog -LogPath $logPath -Message "collector guard started mode=$Mode loop=$LoopSeconds once=$($Once.IsPresent)"
while ($true) {
try {
Invoke-GuardCycle -Runtime $runtime -RuntimePath $runtimePath -LogPath $logPath | Out-Null
}
catch {
Write-GuardLog -LogPath $logPath -Message ("cycle error: {0}; at {1}" -f $_.Exception.Message, $_.ScriptStackTrace)
}
if ($Once) {
break
}
Start-Sleep -Seconds ([Math]::Max($LoopSeconds, 15))
}
}
finally {
if ($lockPath -and (Test-Path -LiteralPath $lockPath)) {
Remove-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue
}
Write-GuardLog -LogPath $logPath -Message 'collector guard stopped'
}