feat: detect OS reboot on startup via System log and LastBootUpTime
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+147
-3
@@ -90,7 +90,7 @@ $script:SkipLogDetailLimit = 15
|
||||
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
|
||||
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
|
||||
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
|
||||
$ScriptVersion = "2.0.36-SAC"
|
||||
$ScriptVersion = "2.0.37-SAC"
|
||||
|
||||
# Логи (все под InstallRoot)
|
||||
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
||||
@@ -103,6 +103,8 @@ $MaxBackupDays = 31
|
||||
|
||||
# Heartbeat (файл; при отсутствии обновления > HeartbeatStaleAlertMultiplier × интервал — оповещение)
|
||||
$HeartbeatInterval = 14400
|
||||
# Окно (мин) для определения «старт после перезагрузки ОС» (LastBootUpTime + System 41/1074/6005/6008/6009).
|
||||
$StartupRebootDetectMinutes = 5
|
||||
# Инвентаризация железа/ПО для SAC (agent.inventory); интервал опроса, сек (12 ч).
|
||||
$InventoryIntervalSec = 43200
|
||||
$GetInventory = 1
|
||||
@@ -1095,7 +1097,8 @@ function Send-RdpMonitorLifecycleNotification {
|
||||
[Parameter(Mandatory = $true)][string]$SacSummary,
|
||||
[string]$SacTitle = '',
|
||||
[string]$SacSeverity = 'info',
|
||||
[string]$EmailSubject = 'RDP Login Monitor'
|
||||
[string]$EmailSubject = 'RDP Login Monitor',
|
||||
[hashtable]$SacDetailsExtra = @{}
|
||||
)
|
||||
|
||||
$title = if (-not [string]::IsNullOrWhiteSpace($SacTitle)) { $SacTitle } else { "RDP login monitor: $Lifecycle" }
|
||||
@@ -1104,6 +1107,9 @@ function Send-RdpMonitorLifecycleNotification {
|
||||
lifecycle = $Lifecycle
|
||||
trigger = $Trigger
|
||||
}
|
||||
foreach ($dk in @($SacDetailsExtra.Keys)) {
|
||||
$sacDetails[$dk] = $SacDetailsExtra[$dk]
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($plainBody)) {
|
||||
$sacDetails.notification_body = $plainBody
|
||||
}
|
||||
@@ -2447,6 +2453,110 @@ function Send-RdpMonitorSacHeartbeat {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RdpMonitorSystemEventShortLabel {
|
||||
param([int]$Id)
|
||||
switch ($Id) {
|
||||
41 { return 'Kernel-Power: unexpected reboot' }
|
||||
1074 { return 'User32: planned shutdown/restart' }
|
||||
6005 { return 'EventLog: service started' }
|
||||
6008 { return 'EventLog: previous shutdown unexpected' }
|
||||
6009 { return 'EventLog: OS version at startup' }
|
||||
default { return "System ID $Id" }
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RdpMonitorStartupCause {
|
||||
param([int]$WindowMinutes = 5)
|
||||
|
||||
if ($WindowMinutes -lt 1) { $WindowMinutes = 5 }
|
||||
$now = Get-Date
|
||||
$since = $now.AddMinutes(-$WindowMinutes)
|
||||
$bootTime = $null
|
||||
$uptimeMin = $null
|
||||
try {
|
||||
$os = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop
|
||||
$bootTime = $os.LastBootUpTime
|
||||
if ($null -ne $bootTime) {
|
||||
$uptimeMin = ($now - $bootTime).TotalMinutes
|
||||
}
|
||||
} catch {
|
||||
Write-Log "WARN: StartupCause: Win32_OperatingSystem недоступен: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
$rebootEventIds = @(41, 1074, 6005, 6008, 6009)
|
||||
$events = @()
|
||||
$eventQueryStart = $since
|
||||
# После ребута монитор может стартовать позже окна (5 мин): тогда берём System с LastBootUpTime (до 60 мин).
|
||||
if ($null -ne $bootTime -and $null -ne $uptimeMin -and $uptimeMin -le 60 -and $bootTime -lt $eventQueryStart) {
|
||||
$eventQueryStart = $bootTime
|
||||
}
|
||||
try {
|
||||
$events = @(Get-WinEvent -FilterHashtable @{
|
||||
LogName = 'System'
|
||||
Id = $rebootEventIds
|
||||
StartTime = $eventQueryStart
|
||||
} -MaxEvents 15 -ErrorAction Stop | Sort-Object TimeCreated)
|
||||
} catch {
|
||||
if ($_.Exception -and $_.Exception.Message -notmatch 'No events were found|NoMatchingEventsFound') {
|
||||
Write-Log "WARN: StartupCause: чтение журнала System: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
$bootRecent = ($null -ne $uptimeMin -and $uptimeMin -le $WindowMinutes)
|
||||
$hasRebootEvents = ($events.Count -gt 0)
|
||||
$bootEventsRecent = ($hasRebootEvents -and $null -ne $uptimeMin -and $uptimeMin -le 60)
|
||||
|
||||
if ($bootRecent -or $bootEventsRecent) {
|
||||
return [pscustomobject]@{
|
||||
Cause = 'os_reboot'
|
||||
WindowMinutes = $WindowMinutes
|
||||
BootTime = $bootTime
|
||||
UptimeMinutes = if ($null -ne $uptimeMin) { [math]::Round($uptimeMin, 1) } else { $null }
|
||||
SystemEvents = @($events | ForEach-Object {
|
||||
[pscustomobject]@{
|
||||
TimeCreated = $_.TimeCreated
|
||||
Id = $_.Id
|
||||
ProviderName = $_.ProviderName
|
||||
MessageShort = (Get-RdpMonitorSystemEventShortLabel -Id $_.Id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
Cause = 'process_start'
|
||||
WindowMinutes = $WindowMinutes
|
||||
BootTime = $bootTime
|
||||
UptimeMinutes = if ($null -ne $uptimeMin) { [math]::Round($uptimeMin, 1) } else { $null }
|
||||
SystemEvents = @()
|
||||
}
|
||||
}
|
||||
|
||||
function Format-RdpMonitorStartupCauseLogLine {
|
||||
param([Parameter(Mandatory = $true)]$StartupCause)
|
||||
|
||||
if ($StartupCause.Cause -eq 'os_reboot') {
|
||||
$parts = [System.Collections.Generic.List[string]]::new()
|
||||
[void]$parts.Add('Старт монитора после перезагрузки ОС')
|
||||
if ($null -ne $StartupCause.UptimeMinutes) {
|
||||
[void]$parts.Add("uptime $($StartupCause.UptimeMinutes) мин")
|
||||
}
|
||||
if ($StartupCause.BootTime) {
|
||||
[void]$parts.Add("LastBootUpTime=$($StartupCause.BootTime.ToString('dd.MM.yyyy HH:mm:ss'))")
|
||||
}
|
||||
if ($StartupCause.SystemEvents.Count -gt 0) {
|
||||
$evSumm = ($StartupCause.SystemEvents | ForEach-Object {
|
||||
"$($_.TimeCreated.ToString('dd.MM.yyyy HH:mm:ss')) ID$($_.Id) ($($_.MessageShort))"
|
||||
}) -join '; '
|
||||
[void]$parts.Add("System: $evSumm")
|
||||
}
|
||||
return ($parts -join '; ') + '.'
|
||||
}
|
||||
|
||||
$uptimeStr = if ($null -ne $StartupCause.UptimeMinutes) { "$($StartupCause.UptimeMinutes) мин" } else { '?' }
|
||||
return "Старт монитора: перезагрузка ОС за последние $($StartupCause.WindowMinutes) мин не обнаружена (uptime $uptimeStr) — перезапуск процесса или задачи планировщика."
|
||||
}
|
||||
|
||||
function Send-Heartbeat {
|
||||
param([switch]$IsStartup = $false)
|
||||
|
||||
@@ -2492,6 +2602,14 @@ function Send-Heartbeat {
|
||||
}
|
||||
|
||||
if ($IsStartup) {
|
||||
$windowMin = 5
|
||||
if (Get-Variable -Name StartupRebootDetectMinutes -Scope Script -ErrorAction SilentlyContinue) {
|
||||
$w = (Get-Variable -Name StartupRebootDetectMinutes -Scope Script -ValueOnly)
|
||||
if ($null -ne $w -and [int]$w -gt 0) { $windowMin = [int]$w }
|
||||
}
|
||||
$startupCause = Get-RdpMonitorStartupCause -WindowMinutes $windowMin
|
||||
Write-Log (Format-RdpMonitorStartupCauseLogLine -StartupCause $startupCause)
|
||||
|
||||
$message = "<b>✅ Мониторинг логинов ЗАПУЩЕН</b>`r`n"
|
||||
$message += "🏷️ Версия скрипта: $(ConvertTo-TelegramHtml $ScriptVersion)"
|
||||
$upd = Get-DeployUpdateMarker
|
||||
@@ -2500,10 +2618,18 @@ function Send-Heartbeat {
|
||||
$message += " (обновлён $(ConvertTo-TelegramHtml $upd.UpdatedAt))"
|
||||
$lifecycleTrigger = 'deploy_recycle'
|
||||
Set-DeployUpdateMarkerPendingOff -Marker $upd
|
||||
} elseif ($startupCause.Cause -eq 'os_reboot') {
|
||||
$lifecycleTrigger = 'os_reboot'
|
||||
} elseif ($startupCause.Cause -eq 'process_start') {
|
||||
$lifecycleTrigger = 'process_start'
|
||||
}
|
||||
$message += "`r`n"
|
||||
$message += "🖥️ Сервер: $hHost`r`n"
|
||||
$message += "🕐 Время запуска: $timestamp"
|
||||
if ($startupCause.Cause -eq 'os_reboot') {
|
||||
$causePlain = Format-RdpMonitorStartupCauseLogLine -StartupCause $startupCause
|
||||
$message += "`r`n🔄 $(ConvertTo-TelegramHtml $causePlain)"
|
||||
}
|
||||
if ($script:OsInstallKindLabel) {
|
||||
$message += "`r`n💻 <b>Тип установки:</b> $(ConvertTo-TelegramHtml $script:OsInstallKindLabel)"
|
||||
}
|
||||
@@ -2554,10 +2680,28 @@ function Send-Heartbeat {
|
||||
$message += " IP из IIS не заданы (<code>ExchangeIisLogPath</code> пуст)."
|
||||
}
|
||||
}
|
||||
$sacStartupExtra = @{
|
||||
startup_cause = $startupCause.Cause
|
||||
}
|
||||
if ($null -ne $startupCause.UptimeMinutes) {
|
||||
$sacStartupExtra.uptime_minutes = $startupCause.UptimeMinutes
|
||||
}
|
||||
if ($startupCause.BootTime) {
|
||||
$sacStartupExtra.last_boot_up_time = $startupCause.BootTime.ToString('o')
|
||||
}
|
||||
if ($startupCause.SystemEvents.Count -gt 0) {
|
||||
$sacStartupExtra.system_event_ids = @($startupCause.SystemEvents | ForEach-Object { $_.Id }) -join ','
|
||||
}
|
||||
|
||||
Send-RdpMonitorLifecycleNotification -Lifecycle 'started' -Trigger $lifecycleTrigger `
|
||||
-TelegramHtmlMessage $message -EmailSubject 'RDP Login Monitor: запуск' `
|
||||
-SacTitle 'RDP login monitor started' `
|
||||
-SacSummary "Мониторинг запущен на $(Get-MonitorServerLabelWithIp), версия $ScriptVersion"
|
||||
-SacSummary $(if ($startupCause.Cause -eq 'os_reboot') {
|
||||
"Мониторинг запущен после перезагрузки ОС на $(Get-MonitorServerLabelWithIp), версия $ScriptVersion"
|
||||
} else {
|
||||
"Мониторинг запущен на $(Get-MonitorServerLabelWithIp), версия $ScriptVersion"
|
||||
}) `
|
||||
-SacDetailsExtra $sacStartupExtra
|
||||
Write-Log "Отправлено уведомление о запуске скрипта (каналы: $notifyChain)"
|
||||
Send-RdpMonitorSacHeartbeat -Timestamp $timestamp
|
||||
} else {
|
||||
|
||||
@@ -47,6 +47,9 @@ $DailyReportEnabled = 1
|
||||
# --- Heartbeat SAC (agent.heartbeat): интервал в секундах; 14400 = 4 ч ---
|
||||
$HeartbeatInterval = 14400
|
||||
|
||||
# Окно (мин): LastBootUpTime + System 41/1074/6005/6008/6009 → «старт после перезагрузки ОС»
|
||||
$StartupRebootDetectMinutes = 5
|
||||
|
||||
# --- Инвентаризация железа/ПО для SAC (agent.inventory, раз в 12 ч) ---
|
||||
$GetInventory = $true
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2.0.36-SAC
|
||||
2.0.37-SAC
|
||||
|
||||
Reference in New Issue
Block a user