feat: detect OS reboot on startup via System log and LastBootUpTime

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-17 10:42:40 +10:00
parent 35e25063e3
commit 746f9e23b2
3 changed files with 151 additions and 4 deletions
+147 -3
View File
@@ -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 {
+3
View File
@@ -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
View File
@@ -1 +1 @@
2.0.36-SAC
2.0.37-SAC