fix(windows): stabilize standalone agent + DLP/worktime collectors
This commit is contained in:
@@ -69,15 +69,40 @@ while ($true) {
|
||||
try {
|
||||
$cfg = Get-Config -Path $ConfigPath
|
||||
$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.fileCollectorScript) -ConfigPath $ConfigPath
|
||||
if ($startFileOps) {
|
||||
Start-CollectorIfNeeded -ScriptPath ([string]$paths.fileCollectorScript) -ConfigPath $ConfigPath
|
||||
}
|
||||
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') {
|
||||
Start-CollectorIfNeeded -ScriptPath ([string]$paths.sessionCollectorScript) -ConfigPath $ConfigPath
|
||||
if ($startWorktime) {
|
||||
Start-CollectorIfNeeded -ScriptPath ([string]$paths.sessionCollectorScript) -ConfigPath $ConfigPath
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
@@ -85,4 +110,3 @@ while ($true) {
|
||||
}
|
||||
Start-Sleep -Seconds ([Math]::Max($LoopSeconds, 5))
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,13 @@ param(
|
||||
Set-StrictMode -Version Latest
|
||||
$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 {
|
||||
param([string]$Path)
|
||||
if ($Path -and (Test-Path -LiteralPath $Path)) {
|
||||
@@ -39,8 +46,54 @@ function Invoke-AwJsonPost {
|
||||
[Parameter(Mandatory = $true)][string]$Json
|
||||
)
|
||||
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null
|
||||
try {
|
||||
$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 {
|
||||
@@ -54,13 +107,39 @@ function Ensure-Bucket {
|
||||
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 = @{
|
||||
client = $ClientName
|
||||
type = $BucketType
|
||||
hostname = $script:Hostname
|
||||
} | 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
|
||||
}
|
||||
|
||||
@@ -333,11 +412,17 @@ function Get-ClipboardTextSafe {
|
||||
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.
|
||||
try {
|
||||
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null
|
||||
$result = [string]::Empty
|
||||
$thread = [System.Threading.Thread]{
|
||||
$script:__aw_clip = $null
|
||||
$threadStart = [System.Threading.ThreadStart]{
|
||||
try {
|
||||
$script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText()
|
||||
}
|
||||
@@ -345,10 +430,13 @@ function Get-ClipboardTextSafe {
|
||||
$script:__aw_clip = $null
|
||||
}
|
||||
}
|
||||
$thread = New-Object System.Threading.Thread($threadStart)
|
||||
$thread.SetApartmentState([System.Threading.ApartmentState]::STA)
|
||||
$thread.Start()
|
||||
$thread.Join(3000) | Out-Null
|
||||
if ($thread.IsAlive) { $thread.Abort() }
|
||||
if ($thread.IsAlive) {
|
||||
try { $thread.Abort() } catch {}
|
||||
}
|
||||
$result = [string]$script:__aw_clip
|
||||
Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue
|
||||
return $result
|
||||
@@ -391,9 +479,11 @@ function Load-DlpPolicy {
|
||||
}
|
||||
|
||||
if ($raw.endpoint) {
|
||||
if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) }
|
||||
if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) }
|
||||
if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) }
|
||||
$props = @()
|
||||
try { $props = @($raw.endpoint.PSObject.Properties.Name) } catch { $props = @() }
|
||||
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 {
|
||||
|
||||
@@ -58,7 +58,15 @@ function Get-Config {
|
||||
}
|
||||
try {
|
||||
$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
|
||||
}
|
||||
catch {
|
||||
@@ -137,7 +145,9 @@ function Parse-SessionLines {
|
||||
if (-not $Lines) { return $records }
|
||||
|
||||
$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++) {
|
||||
$line = $Lines[$i].Trim()
|
||||
@@ -163,7 +173,9 @@ function Test-SessionIsActive {
|
||||
param([string]$State)
|
||||
if (-not $State) { return $false }
|
||||
$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
|
||||
|
||||
Reference in New Issue
Block a user