diff --git a/Login_Monitor.ps1 b/Login_Monitor.ps1
index 0420a6a..7c29092 100644
--- a/Login_Monitor.ps1
+++ b/Login_Monitor.ps1
@@ -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 = "✅ Мониторинг логинов ЗАПУЩЕН`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💻 Тип установки: $(ConvertTo-TelegramHtml $script:OsInstallKindLabel)"
}
@@ -2554,10 +2680,28 @@ function Send-Heartbeat {
$message += " IP из IIS не заданы (ExchangeIisLogPath пуст)."
}
}
+ $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 {
diff --git a/login_monitor.settings.example.ps1 b/login_monitor.settings.example.ps1
index b57cf5c..e348d68 100644
--- a/login_monitor.settings.example.ps1
+++ b/login_monitor.settings.example.ps1
@@ -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
diff --git a/version.txt b/version.txt
index e17fcc9..7499e98 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-2.0.36-SAC
+2.0.37-SAC