fix: quarantine corrupt SAC spool files, flush newest first (2.1.6-SAC)
Reject null-padded spool payloads to rejected/, read UTF-16 spool, process recent events first, and raise flush batch to 20 so daily reports are not blocked by legacy junk. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+2
-2
@@ -90,7 +90,7 @@ $script:SkipLogDetailLimit = 15
|
|||||||
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
|
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
|
||||||
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
|
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
|
||||||
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
|
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
|
||||||
$ScriptVersion = "2.1.5-SAC"
|
$ScriptVersion = "2.1.6-SAC"
|
||||||
|
|
||||||
# Логи (все под InstallRoot)
|
# Логи (все под InstallRoot)
|
||||||
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
||||||
@@ -5016,7 +5016,7 @@ function Start-LoginMonitor {
|
|||||||
$nextReportCheck = Check-AndSendDailyReport
|
$nextReportCheck = Check-AndSendDailyReport
|
||||||
}
|
}
|
||||||
if ($script:SacClientLoaded) {
|
if ($script:SacClientLoaded) {
|
||||||
Invoke-SacFlushSpool -MaxFiles 5 | Out-Null
|
Invoke-SacFlushSpool -MaxFiles 20 | Out-Null
|
||||||
if ((Get-SacNormalizedMode) -ne 'off' -and $now -ge $script:NextSacCommandPollTime) {
|
if ((Get-SacNormalizedMode) -ne 'off' -and $now -ge $script:NextSacCommandPollTime) {
|
||||||
Invoke-SacProcessPendingCommands | Out-Null
|
Invoke-SacProcessPendingCommands | Out-Null
|
||||||
$script:NextSacCommandPollTime = $now.AddSeconds($SacCommandPollIntervalSec)
|
$script:NextSacCommandPollTime = $now.AddSeconds($SacCommandPollIntervalSec)
|
||||||
|
|||||||
+102
-7
@@ -474,6 +474,84 @@ function Move-SacSpoolToRejected {
|
|||||||
Move-Item -LiteralPath $src -Destination $dst -Force -ErrorAction SilentlyContinue
|
Move-Item -LiteralPath $src -Destination $dst -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Test-SacGuidString {
|
||||||
|
param([string]$Value)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Value)) { return $false }
|
||||||
|
return ($Value.Trim() -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SacEventIdFromSpoolFileName {
|
||||||
|
param([string]$Path)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Path)) { return $null }
|
||||||
|
$base = [System.IO.Path]::GetFileNameWithoutExtension($Path)
|
||||||
|
if (Test-SacGuidString -Value $base) {
|
||||||
|
return $base.ToLowerInvariant()
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SacEventIdFromJsonText {
|
||||||
|
param([string]$JsonText)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($JsonText)) { return $null }
|
||||||
|
if ($JsonText -match '"event_id"\s*:\s*"([0-9a-fA-F-]{36})"') {
|
||||||
|
return $Matches[1].ToLowerInvariant()
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-SacSpoolBytesCorrupt {
|
||||||
|
param([byte[]]$Bytes)
|
||||||
|
if ($null -eq $Bytes -or $Bytes.Length -lt 2) { return $true }
|
||||||
|
# UTF-16/UTF-32 с нулевым стартом или «пустой» spool — не JSON (см. hex 00 00 00 00).
|
||||||
|
if ($Bytes[0] -eq 0 -and $Bytes[1] -eq 0) { return $true }
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Read-SacSpoolFileBytes {
|
||||||
|
param([Parameter(Mandatory = $true)][string]$Path)
|
||||||
|
return [System.IO.File]::ReadAllBytes($Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Read-SacSpoolFileText {
|
||||||
|
param([Parameter(Mandatory = $true)][string]$Path)
|
||||||
|
$bytes = Read-SacSpoolFileBytes -Path $Path
|
||||||
|
if (Test-SacSpoolBytesCorrupt -Bytes $bytes) { return $null }
|
||||||
|
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
|
||||||
|
return [System.Text.Encoding]::Unicode.GetString($bytes)
|
||||||
|
}
|
||||||
|
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0x7B -and $Bytes[1] -eq 0) {
|
||||||
|
return [System.Text.Encoding]::Unicode.GetString($bytes)
|
||||||
|
}
|
||||||
|
$utf8 = New-Object System.Text.UTF8Encoding $false
|
||||||
|
return $utf8.GetString($bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Move-SacSpoolFileToRejected {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$SpoolFilePath,
|
||||||
|
[string]$Reason = ''
|
||||||
|
)
|
||||||
|
$eventId = Get-SacEventIdFromSpoolFileName -Path $SpoolFilePath
|
||||||
|
if ($Reason) {
|
||||||
|
$label = if ($eventId) { "$eventId.json" } else { [System.IO.Path]::GetFileName($SpoolFilePath) }
|
||||||
|
Write-SacLog "WARN: SAC spool → rejected ($Reason): $label"
|
||||||
|
}
|
||||||
|
if ($eventId) {
|
||||||
|
Move-SacSpoolToRejected -EventId $eventId
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$dir = Get-SacSpoolDirResolved
|
||||||
|
$name = [System.IO.Path]::GetFileName($SpoolFilePath)
|
||||||
|
$src = Join-Path $dir $name
|
||||||
|
if (-not (Test-Path -LiteralPath $src)) { return }
|
||||||
|
$rejDir = Join-Path $dir 'rejected'
|
||||||
|
if (-not (Test-Path -LiteralPath $rejDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $rejDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
$dst = Join-Path $rejDir $name
|
||||||
|
Move-Item -LiteralPath $src -Destination $dst -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
function Get-SacPostBodyBytes {
|
function Get-SacPostBodyBytes {
|
||||||
param([string]$JsonText)
|
param([string]$JsonText)
|
||||||
$bytes = Get-SacUtf8Bytes -Text $JsonText
|
$bytes = Get-SacUtf8Bytes -Text $JsonText
|
||||||
@@ -588,7 +666,10 @@ function Write-SacPostBodyDiagnostic {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Invoke-SacPostPayload {
|
function Invoke-SacPostPayload {
|
||||||
param([string]$JsonBody)
|
param(
|
||||||
|
[string]$JsonBody,
|
||||||
|
[string]$SpoolFilePath = ''
|
||||||
|
)
|
||||||
|
|
||||||
if (-not (Test-SacConfigured)) { return $false }
|
if (-not (Test-SacConfigured)) { return $false }
|
||||||
if (-not (Test-SacShouldAttemptSend)) { return $false }
|
if (-not (Test-SacShouldAttemptSend)) { return $false }
|
||||||
@@ -597,11 +678,15 @@ function Invoke-SacPostPayload {
|
|||||||
if ([string]::IsNullOrWhiteSpace($ingest)) { return $false }
|
if ([string]::IsNullOrWhiteSpace($ingest)) { return $false }
|
||||||
|
|
||||||
$jsonText = Repair-SacJsonText -Text (Get-SacSingleString -Value $JsonBody -Label 'spool payload')
|
$jsonText = Repair-SacJsonText -Text (Get-SacSingleString -Value $JsonBody -Label 'spool payload')
|
||||||
if ($jsonText -notmatch '"event_id"\s*:\s*"([0-9a-fA-F-]{36})"') {
|
$eventId = Get-SacEventIdFromJsonText -JsonText $jsonText
|
||||||
|
if (-not $eventId) {
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($SpoolFilePath)) {
|
||||||
|
Move-SacSpoolFileToRejected -SpoolFilePath $SpoolFilePath -Reason 'no event_id in JSON body'
|
||||||
|
} else {
|
||||||
Write-SacLog 'WARN: SAC JSON has no event_id (uuid); skip POST'
|
Write-SacLog 'WARN: SAC JSON has no event_id (uuid); skip POST'
|
||||||
|
}
|
||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
$eventId = $Matches[1]
|
|
||||||
$eventType = 'unknown'
|
$eventType = 'unknown'
|
||||||
if ($jsonText -match '"type"\s*:\s*"([^"]+)"') {
|
if ($jsonText -match '"type"\s*:\s*"([^"]+)"') {
|
||||||
$eventType = $Matches[1]
|
$eventType = $Matches[1]
|
||||||
@@ -838,15 +923,25 @@ function Invoke-SacFlushSpool {
|
|||||||
$dir = Get-SacSpoolDirResolved
|
$dir = Get-SacSpoolDirResolved
|
||||||
if (-not (Test-Path -LiteralPath $dir)) { return }
|
if (-not (Test-Path -LiteralPath $dir)) { return }
|
||||||
|
|
||||||
$files = @(Get-ChildItem -LiteralPath $dir -Filter '*.json' -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime)
|
# Сначала свежие события (daily report); битые записи уходят в rejected и не блокируют очередь.
|
||||||
|
$files = @(Get-ChildItem -LiteralPath $dir -Filter '*.json' -File -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object LastWriteTime -Descending)
|
||||||
$count = 0
|
$count = 0
|
||||||
foreach ($f in $files) {
|
foreach ($f in $files) {
|
||||||
$count++
|
$count++
|
||||||
if ($count -gt $MaxFiles) { break }
|
if ($count -gt $MaxFiles) { break }
|
||||||
try {
|
try {
|
||||||
$utf8 = New-Object System.Text.UTF8Encoding $false
|
$bytes = Read-SacSpoolFileBytes -Path $f.FullName
|
||||||
$json = [System.IO.File]::ReadAllText($f.FullName, $utf8)
|
if (Test-SacSpoolBytesCorrupt -Bytes $bytes) {
|
||||||
Invoke-SacPostPayload -JsonBody $json | Out-Null
|
Move-SacSpoolFileToRejected -SpoolFilePath $f.FullName -Reason 'null or empty payload'
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$json = Read-SacSpoolFileText -Path $f.FullName
|
||||||
|
if ([string]::IsNullOrWhiteSpace($json)) {
|
||||||
|
Move-SacSpoolFileToRejected -SpoolFilePath $f.FullName -Reason 'unreadable payload'
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
Invoke-SacPostPayload -JsonBody $json -SpoolFilePath $f.FullName | Out-Null
|
||||||
} catch {
|
} catch {
|
||||||
Write-SacLog "WARN: SAC spool flush failed for $($f.Name): $($_.Exception.Message)"
|
Write-SacLog "WARN: SAC spool flush failed for $($f.Name): $($_.Exception.Message)"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2.1.5-SAC
|
2.1.6-SAC
|
||||||
|
|||||||
Reference in New Issue
Block a user