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 одинаково; при только
|
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
|
||||||
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
|
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
|
||||||
$ScriptVersion = "2.0.36-SAC"
|
$ScriptVersion = "2.0.37-SAC"
|
||||||
|
|
||||||
# Логи (все под InstallRoot)
|
# Логи (все под InstallRoot)
|
||||||
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
||||||
@@ -103,6 +103,8 @@ $MaxBackupDays = 31
|
|||||||
|
|
||||||
# Heartbeat (файл; при отсутствии обновления > HeartbeatStaleAlertMultiplier × интервал — оповещение)
|
# Heartbeat (файл; при отсутствии обновления > HeartbeatStaleAlertMultiplier × интервал — оповещение)
|
||||||
$HeartbeatInterval = 14400
|
$HeartbeatInterval = 14400
|
||||||
|
# Окно (мин) для определения «старт после перезагрузки ОС» (LastBootUpTime + System 41/1074/6005/6008/6009).
|
||||||
|
$StartupRebootDetectMinutes = 5
|
||||||
# Инвентаризация железа/ПО для SAC (agent.inventory); интервал опроса, сек (12 ч).
|
# Инвентаризация железа/ПО для SAC (agent.inventory); интервал опроса, сек (12 ч).
|
||||||
$InventoryIntervalSec = 43200
|
$InventoryIntervalSec = 43200
|
||||||
$GetInventory = 1
|
$GetInventory = 1
|
||||||
@@ -1095,7 +1097,8 @@ function Send-RdpMonitorLifecycleNotification {
|
|||||||
[Parameter(Mandatory = $true)][string]$SacSummary,
|
[Parameter(Mandatory = $true)][string]$SacSummary,
|
||||||
[string]$SacTitle = '',
|
[string]$SacTitle = '',
|
||||||
[string]$SacSeverity = 'info',
|
[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" }
|
$title = if (-not [string]::IsNullOrWhiteSpace($SacTitle)) { $SacTitle } else { "RDP login monitor: $Lifecycle" }
|
||||||
@@ -1104,6 +1107,9 @@ function Send-RdpMonitorLifecycleNotification {
|
|||||||
lifecycle = $Lifecycle
|
lifecycle = $Lifecycle
|
||||||
trigger = $Trigger
|
trigger = $Trigger
|
||||||
}
|
}
|
||||||
|
foreach ($dk in @($SacDetailsExtra.Keys)) {
|
||||||
|
$sacDetails[$dk] = $SacDetailsExtra[$dk]
|
||||||
|
}
|
||||||
if (-not [string]::IsNullOrWhiteSpace($plainBody)) {
|
if (-not [string]::IsNullOrWhiteSpace($plainBody)) {
|
||||||
$sacDetails.notification_body = $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 {
|
function Send-Heartbeat {
|
||||||
param([switch]$IsStartup = $false)
|
param([switch]$IsStartup = $false)
|
||||||
|
|
||||||
@@ -2492,6 +2602,14 @@ function Send-Heartbeat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($IsStartup) {
|
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 = "<b>✅ Мониторинг логинов ЗАПУЩЕН</b>`r`n"
|
||||||
$message += "🏷️ Версия скрипта: $(ConvertTo-TelegramHtml $ScriptVersion)"
|
$message += "🏷️ Версия скрипта: $(ConvertTo-TelegramHtml $ScriptVersion)"
|
||||||
$upd = Get-DeployUpdateMarker
|
$upd = Get-DeployUpdateMarker
|
||||||
@@ -2500,10 +2618,18 @@ function Send-Heartbeat {
|
|||||||
$message += " (обновлён $(ConvertTo-TelegramHtml $upd.UpdatedAt))"
|
$message += " (обновлён $(ConvertTo-TelegramHtml $upd.UpdatedAt))"
|
||||||
$lifecycleTrigger = 'deploy_recycle'
|
$lifecycleTrigger = 'deploy_recycle'
|
||||||
Set-DeployUpdateMarkerPendingOff -Marker $upd
|
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 += "`r`n"
|
||||||
$message += "🖥️ Сервер: $hHost`r`n"
|
$message += "🖥️ Сервер: $hHost`r`n"
|
||||||
$message += "🕐 Время запуска: $timestamp"
|
$message += "🕐 Время запуска: $timestamp"
|
||||||
|
if ($startupCause.Cause -eq 'os_reboot') {
|
||||||
|
$causePlain = Format-RdpMonitorStartupCauseLogLine -StartupCause $startupCause
|
||||||
|
$message += "`r`n🔄 $(ConvertTo-TelegramHtml $causePlain)"
|
||||||
|
}
|
||||||
if ($script:OsInstallKindLabel) {
|
if ($script:OsInstallKindLabel) {
|
||||||
$message += "`r`n💻 <b>Тип установки:</b> $(ConvertTo-TelegramHtml $script:OsInstallKindLabel)"
|
$message += "`r`n💻 <b>Тип установки:</b> $(ConvertTo-TelegramHtml $script:OsInstallKindLabel)"
|
||||||
}
|
}
|
||||||
@@ -2554,10 +2680,28 @@ function Send-Heartbeat {
|
|||||||
$message += " IP из IIS не заданы (<code>ExchangeIisLogPath</code> пуст)."
|
$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 `
|
Send-RdpMonitorLifecycleNotification -Lifecycle 'started' -Trigger $lifecycleTrigger `
|
||||||
-TelegramHtmlMessage $message -EmailSubject 'RDP Login Monitor: запуск' `
|
-TelegramHtmlMessage $message -EmailSubject 'RDP Login Monitor: запуск' `
|
||||||
-SacTitle 'RDP login monitor started' `
|
-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)"
|
Write-Log "Отправлено уведомление о запуске скрипта (каналы: $notifyChain)"
|
||||||
Send-RdpMonitorSacHeartbeat -Timestamp $timestamp
|
Send-RdpMonitorSacHeartbeat -Timestamp $timestamp
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ $DailyReportEnabled = 1
|
|||||||
# --- Heartbeat SAC (agent.heartbeat): интервал в секундах; 14400 = 4 ч ---
|
# --- Heartbeat SAC (agent.heartbeat): интервал в секундах; 14400 = 4 ч ---
|
||||||
$HeartbeatInterval = 14400
|
$HeartbeatInterval = 14400
|
||||||
|
|
||||||
|
# Окно (мин): LastBootUpTime + System 41/1074/6005/6008/6009 → «старт после перезагрузки ОС»
|
||||||
|
$StartupRebootDetectMinutes = 5
|
||||||
|
|
||||||
# --- Инвентаризация железа/ПО для SAC (agent.inventory, раз в 12 ч) ---
|
# --- Инвентаризация железа/ПО для SAC (agent.inventory, раз в 12 ч) ---
|
||||||
$GetInventory = $true
|
$GetInventory = $true
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2.0.36-SAC
|
2.0.37-SAC
|
||||||
|
|||||||
Reference in New Issue
Block a user