fix(windows): stabilize standalone agent + DLP/worktime collectors

This commit is contained in:
igor04091968
2026-05-09 08:29:18 +03:00
parent 5ddceb4dc3
commit 7a320d7862
3 changed files with 142 additions and 16 deletions
+29 -5
View File
@@ -69,15 +69,40 @@ while ($true) {
try { try {
$cfg = Get-Config -Path $ConfigPath $cfg = Get-Config -Path $ConfigPath
$paths = $cfg.paths $paths = $cfg.paths
$collectors = $cfg.collectors
$isSession0 = ([System.Diagnostics.Process]::GetCurrentProcess().SessionId -eq 0)
Start-CollectorIfNeeded -ScriptPath ([string]$paths.collectorScript) -ConfigPath $ConfigPath # In Session 0 (SYSTEM) many collectors that rely on interactive user context (browsers, Outlook)
# will crash/exit immediately. Default to starting only collectors that can run headless.
$startBrowser = $true
$startFileOps = $true
$startEmail = $true
$startWorktime = $true
if ($collectors) {
if ($collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { $startFileOps = [bool]$collectors.fileOpsEnabled }
if ($collectors.PSObject.Properties.Name -contains 'emailEnabled') { $startEmail = [bool]$collectors.emailEnabled }
}
if ($isSession0) {
$startBrowser = $false
$startEmail = $false
}
if ($startBrowser) {
Start-CollectorIfNeeded -ScriptPath ([string]$paths.collectorScript) -ConfigPath $ConfigPath
}
Start-CollectorIfNeeded -ScriptPath ([string]$paths.endpointCollectorScript) -ConfigPath $ConfigPath Start-CollectorIfNeeded -ScriptPath ([string]$paths.endpointCollectorScript) -ConfigPath $ConfigPath
Start-CollectorIfNeeded -ScriptPath ([string]$paths.fileCollectorScript) -ConfigPath $ConfigPath if ($startFileOps) {
Start-CollectorIfNeeded -ScriptPath ([string]$paths.fileCollectorScript) -ConfigPath $ConfigPath
}
if ($paths.PSObject.Properties.Name -contains 'emailCollectorScript') { if ($paths.PSObject.Properties.Name -contains 'emailCollectorScript') {
Start-CollectorIfNeeded -ScriptPath ([string]$paths.emailCollectorScript) -ConfigPath $ConfigPath if ($startEmail) {
Start-CollectorIfNeeded -ScriptPath ([string]$paths.emailCollectorScript) -ConfigPath $ConfigPath
}
} }
if ($paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { if ($paths.PSObject.Properties.Name -contains 'sessionCollectorScript') {
Start-CollectorIfNeeded -ScriptPath ([string]$paths.sessionCollectorScript) -ConfigPath $ConfigPath if ($startWorktime) {
Start-CollectorIfNeeded -ScriptPath ([string]$paths.sessionCollectorScript) -ConfigPath $ConfigPath
}
} }
} }
catch { catch {
@@ -85,4 +110,3 @@ while ($true) {
} }
Start-Sleep -Seconds ([Math]::Max($LoopSeconds, 5)) Start-Sleep -Seconds ([Math]::Max($LoopSeconds, 5))
} }
+98 -8
View File
@@ -13,6 +13,13 @@ param(
Set-StrictMode -Version Latest Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
# Ensure HttpClient is available (Windows PowerShell 5 may not auto-load it)
try {
Add-Type -AssemblyName System.Net.Http
}
catch {
}
function Get-DeploymentConfig { function Get-DeploymentConfig {
param([string]$Path) param([string]$Path)
if ($Path -and (Test-Path -LiteralPath $Path)) { if ($Path -and (Test-Path -LiteralPath $Path)) {
@@ -39,8 +46,54 @@ function Invoke-AwJsonPost {
[Parameter(Mandatory = $true)][string]$Json [Parameter(Mandatory = $true)][string]$Json
) )
$bytes = [Text.Encoding]::UTF8.GetBytes($Json) try {
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null $bytes = [System.Text.Encoding]::UTF8.GetBytes($Json)
$req = [System.Net.HttpWebRequest]::Create($Uri)
$req.Method = 'POST'
$req.ContentType = 'application/json'
$req.Accept = 'application/json'
$req.KeepAlive = $false
$req.Timeout = 15000
$req.ReadWriteTimeout = 15000
$req.ContentLength = $bytes.Length
$stream = $req.GetRequestStream()
try { $stream.Write($bytes, 0, $bytes.Length) } finally { $stream.Close() }
$resp = $req.GetResponse()
try {
# read body for debugging, but discard on success
$rs = $resp.GetResponseStream()
if ($rs) { $sr = New-Object System.IO.StreamReader($rs); $null = $sr.ReadToEnd(); $sr.Close() }
} finally {
$resp.Close()
}
return
}
catch [System.Net.WebException] {
$status = $null
$body = ''
try {
if ($_.Exception.Response) {
try { $status = [int]$_.Exception.Response.StatusCode } catch {}
$rs = $_.Exception.Response.GetResponseStream()
if ($rs) { $sr = New-Object System.IO.StreamReader($rs); $body = $sr.ReadToEnd(); $sr.Close() }
}
} catch {}
# aw-server-rust may return 304 for idempotent bucket create. Treat it as OK.
if ($status -eq 304) {
Write-EndpointLog ("POST bucket exists (304): uri={0}" -f $Uri)
return
}
Write-EndpointLog ("POST failed: uri={0} status={1} err={2} body={3}" -f $Uri, $status, $_.Exception.Message, $body)
throw
}
catch {
Write-EndpointLog ("POST error: uri={0} err={1}" -f $Uri, $_.Exception.Message)
throw
}
} }
function Ensure-Bucket { function Ensure-Bucket {
@@ -54,13 +107,39 @@ function Ensure-Bucket {
return return
} }
if ($script:KnownBuckets.ContainsKey($BucketId)) {
return
}
# Fast-path: if bucket already exists, don't POST.
try {
Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" -TimeoutSec 10 -DisableKeepAlive -ErrorAction Stop | Out-Null
Write-EndpointLog ("bucket ok (GET): {0}" -f $BucketId)
$script:KnownBuckets[$BucketId] = $true
return
}
catch {
Write-EndpointLog ("bucket GET failed: {0} err={1}" -f $BucketId, $_.Exception.Message)
}
$body = @{ $body = @{
client = $ClientName client = $ClientName
type = $BucketType type = $BucketType
hostname = $script:Hostname hostname = $script:Hostname
} | ConvertTo-Json -Compress } | ConvertTo-Json -Compress
Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body try {
Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body
}
catch {
# If create failed (race), verify it exists now.
try {
Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" -TimeoutSec 10 -DisableKeepAlive | Out-Null
}
catch {
throw
}
}
$script:KnownBuckets[$BucketId] = $true $script:KnownBuckets[$BucketId] = $true
} }
@@ -333,11 +412,17 @@ function Get-ClipboardTextSafe {
Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message)
} }
# Clipboard is not reliably accessible from Session 0 (SYSTEM). Avoid noisy thread hacks there.
if ($script:SessionId -eq 0) {
return $null
}
# Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases.
try { try {
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null
$result = [string]::Empty $result = [string]::Empty
$thread = [System.Threading.Thread]{ $script:__aw_clip = $null
$threadStart = [System.Threading.ThreadStart]{
try { try {
$script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText()
} }
@@ -345,10 +430,13 @@ function Get-ClipboardTextSafe {
$script:__aw_clip = $null $script:__aw_clip = $null
} }
} }
$thread = New-Object System.Threading.Thread($threadStart)
$thread.SetApartmentState([System.Threading.ApartmentState]::STA) $thread.SetApartmentState([System.Threading.ApartmentState]::STA)
$thread.Start() $thread.Start()
$thread.Join(3000) | Out-Null $thread.Join(3000) | Out-Null
if ($thread.IsAlive) { $thread.Abort() } if ($thread.IsAlive) {
try { $thread.Abort() } catch {}
}
$result = [string]$script:__aw_clip $result = [string]$script:__aw_clip
Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue
return $result return $result
@@ -391,9 +479,11 @@ function Load-DlpPolicy {
} }
if ($raw.endpoint) { if ($raw.endpoint) {
if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } $props = @()
if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } try { $props = @($raw.endpoint.PSObject.Properties.Name) } catch { $props = @() }
if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } if ($props -contains 'clipboard' -and $raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) }
if ($props -contains 'usb' -and $raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) }
if ($props -contains 'print' -and $raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) }
} }
} }
catch { catch {
+15 -3
View File
@@ -58,7 +58,15 @@ function Get-Config {
} }
try { try {
$bytes = [System.IO.File]::ReadAllBytes($Path) $bytes = [System.IO.File]::ReadAllBytes($Path)
$text = Decode-Bytes-Auto -Bytes $bytes # Config is JSON. Prefer deterministic BOM-based decoding over heuristics.
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
} elseif ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
$text = [System.Text.Encoding]::Unicode.GetString($bytes)
} else {
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
}
$text = $text -replace '^\uFEFF', ''
return $text | ConvertFrom-Json -ErrorAction Stop return $text | ConvertFrom-Json -ErrorAction Stop
} }
catch { catch {
@@ -137,7 +145,9 @@ function Parse-SessionLines {
if (-not $Lines) { return $records } if (-not $Lines) { return $records }
$startIndex = 0 $startIndex = 0
if ($Lines.Count -gt 0 -and $Lines[0] -match '\b(USERNAME|Имя|Имя пользователя|Имя_пользователя)\b') { $startIndex = 1 } # NOTE: Keep this script ASCII-only to stay compatible with Windows PowerShell 5
# when the file is UTF-8 without BOM. Avoid Cyrillic literals in regex patterns.
if ($Lines.Count -gt 0 -and $Lines[0] -match '\b(USERNAME|UserName|USER)\b') { $startIndex = 1 }
for ($i = $startIndex; $i -lt $Lines.Count; $i++) { for ($i = $startIndex; $i -lt $Lines.Count; $i++) {
$line = $Lines[$i].Trim() $line = $Lines[$i].Trim()
@@ -163,7 +173,9 @@ function Test-SessionIsActive {
param([string]$State) param([string]$State)
if (-not $State) { return $false } if (-not $State) { return $false }
$s = $State.Trim().ToLowerInvariant() $s = $State.Trim().ToLowerInvariant()
return ($s -match 'active') -or ($s -match 'актив') # Match English "active" and Russian "актив*" without embedding Cyrillic.
# "актив" = \u0430\u043A\u0442\u0438\u0432
return ($s -match 'active') -or ($s -match '\u0430\u043a\u0442\u0438\u0432')
} }
# Main # Main