feat: graceful restart without Stop-Process; SAC key in settings example

This commit is contained in:
PTah
2026-05-27 14:43:04 +10:00
parent bd8076a1d0
commit 22a32bc1cb
8 changed files with 223 additions and 27 deletions
+120 -21
View File
@@ -36,7 +36,9 @@ param(
[switch]$Watchdog,
[switch]$InstallTasks,
[switch]$SkipScheduledTaskMaintenance,
[switch]$CheckSac
[switch]$CheckSac,
[switch]$RequestRestart,
[switch]$Recycle
)
Set-StrictMode -Version Latest
@@ -69,13 +71,16 @@ $script:ScheduledTaskNameMain = "RDP-Login-Monitor"
$script:ScheduledTaskNameWatchdog = "RDP-Login-Monitor-Watchdog"
# Один экземпляр: эксклюзивная блокировка файла в InstallRoot (SYSTEM и интерактивный админ — одинаково; Global mutex давал «Отказано в доступе» между контекстами).
$script:MonitorSingletonLockStream = $null
$script:MonitorRestartRequestFile = Join-Path $script:InstallRoot 'restart.request'
$script:MonitorRecycleRequested = $false
$script:MonitorLoopInitialized = $false
# Версия: пишется в лог и в Telegram. При доменном развёртывании через шару см. DEPLOY.md —
# триггер обновления на клиентах даёт файл version.txt на шаре (его номер можно поднять и без смены
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
$ScriptVersion = "1.2.0-SAC"
$ScriptVersion = "1.2.1-SAC"
# Логи (все под InstallRoot)
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
@@ -385,6 +390,59 @@ function Get-RdpMonitorPowerShellExe {
return "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
}
function Set-RdpMonitorRestartRequest {
param(
[ValidateSet('settings', 'recycle')]
[string]$Mode = 'settings',
[string]$Reason = 'manual'
)
if (-not (Test-Path -LiteralPath $script:InstallRoot)) {
New-Item -ItemType Directory -Path $script:InstallRoot -Force | Out-Null
}
$lines = @(
"mode=$Mode"
"reason=$Reason"
"requested_at=$((Get-Date).ToString('o'))"
)
Write-TextFileUtf8Bom -Path $script:MonitorRestartRequestFile -Text (($lines -join "`r`n") + "`r`n")
}
function Get-RdpMonitorRestartRequest {
if (-not (Test-Path -LiteralPath $script:MonitorRestartRequestFile)) {
return $null
}
$mode = 'settings'
$reason = ''
try {
foreach ($ln in (Get-Content -LiteralPath $script:MonitorRestartRequestFile -ErrorAction Stop)) {
if ($ln -match '^\s*mode\s*=\s*(.+)\s*$') { $mode = $Matches[1].Trim().ToLowerInvariant() }
if ($ln -match '^\s*reason\s*=\s*(.+)\s*$') { $reason = $Matches[1].Trim() }
}
} catch { }
try {
Remove-Item -LiteralPath $script:MonitorRestartRequestFile -Force -ErrorAction SilentlyContinue
} catch { }
if ($mode -ne 'recycle') { $mode = 'settings' }
return [pscustomobject]@{ Mode = $mode; Reason = $reason }
}
function Invoke-RdpMonitorReloadSettings {
if (-not (Test-Path -LiteralPath $script:LoginMonitorSettingsFile)) {
Write-Log "Graceful restart: login_monitor.settings.ps1 не найден, настройки не перечитаны."
return $false
}
try {
. $script:LoginMonitorSettingsFile
$script:LoginMonitorSettingsLoaded = $true
Write-Log "Graceful restart: login_monitor.settings.ps1 перечитан (каналы: $(Get-NotifyChainHuman))."
return $true
} catch {
Write-Log "Graceful restart: ошибка чтения settings: $($_.Exception.Message)"
return $false
}
}
function Test-RdpMonitorScheduledTaskMatches {
param(
[Parameter(Mandatory = $true)][string]$TaskName,
@@ -865,6 +923,13 @@ if ($CheckSac) {
exit $code
}
if ($RequestRestart) {
$restartMode = if ($Recycle) { 'recycle' } else { 'settings' }
Set-RdpMonitorRestartRequest -Mode $restartMode -Reason 'RequestRestart'
Write-Log "Запрошен graceful restart (mode=$restartMode, файл restart.request). Активный монитор обработает запрос без Stop-Process."
exit 0
}
Invoke-RdpMonitorProcessMigrationAndRelaunch
Lock-RdpMonitorSingleInstance
Ensure-RdpMonitorScheduledTasks
@@ -2487,28 +2552,47 @@ function Start-LoginMonitor {
$script:MonitorStartedAt = Get-Date
$script:HeartbeatStaleAlertActive = $false
Cleanup-OldLogs
Send-Heartbeat -IsStartup
Enable-SecurityAudit
do {
$script:MonitorRecycleRequested = $false
$rdGatewayAvailable = $false
if ($EnableRDGatewayMonitoring) { $rdGatewayAvailable = Test-RDGatewayLog }
if (-not $script:MonitorLoopInitialized) {
Cleanup-OldLogs
Send-Heartbeat -IsStartup
Enable-SecurityAudit
$script:MonitorLoopInitialized = $true
}
$rcmMonitoringEnabled = ($osKind.IsWorkstation -and (Test-RcmLogAvailable))
if ($osKind.IsWorkstation -and -not $rcmMonitoringEnabled) {
Write-Log "Рабочая станция: журнал Remote Connection Manager недоступен — уведомления только по Security 4624/4625 (LogonType 10). Проверьте, что включён удалённый рабочий стол."
}
$rdGatewayAvailable = $false
if ($EnableRDGatewayMonitoring) { $rdGatewayAvailable = Test-RDGatewayLog }
$nextHeartbeatTime = (Get-Date).AddSeconds($HeartbeatInterval)
$nextRotationCheck = Check-AndRotateLog
$nextReportCheck = Check-AndSendDailyReport
$lastCheckTime = (Get-Date).AddSeconds(-10)
$lastGatewayCheckTime = (Get-Date).AddSeconds(-10)
$lastRcmCheckTime = (Get-Date).AddSeconds(-10)
$lastLockout4740CheckTime = (Get-Date).AddSeconds(-10)
$monitorEvents = @(4624, 4625, 4648)
$rcmMonitoringEnabled = ($osKind.IsWorkstation -and (Test-RcmLogAvailable))
if ($osKind.IsWorkstation -and -not $rcmMonitoringEnabled) {
Write-Log "Рабочая станция: журнал Remote Connection Manager недоступен — уведомления только по Security 4624/4625 (LogonType 10). Проверьте, что включён удалённый рабочий стол."
}
$nextHeartbeatTime = (Get-Date).AddSeconds($HeartbeatInterval)
$nextRotationCheck = Check-AndRotateLog
$nextReportCheck = Check-AndSendDailyReport
$lastCheckTime = (Get-Date).AddSeconds(-10)
$lastGatewayCheckTime = (Get-Date).AddSeconds(-10)
$lastRcmCheckTime = (Get-Date).AddSeconds(-10)
$lastLockout4740CheckTime = (Get-Date).AddSeconds(-10)
$monitorEvents = @(4624, 4625, 4648)
while ($true) {
$restartReq = Get-RdpMonitorRestartRequest
if ($null -ne $restartReq) {
$script:StopNotificationSent = $true
if ($restartReq.Mode -eq 'recycle') {
Write-Log "Graceful recycle: запрос '$($restartReq.Reason)' — выход для запуска нового процесса (обновлённый скрипт с диска)."
$script:MonitorRecycleRequested = $true
break
}
Write-Log "Graceful restart (settings): запрос '$($restartReq.Reason)' — перечитываю настройки в этом же процессе PowerShell."
Invoke-RdpMonitorReloadSettings | Out-Null
break
}
while ($true) {
try {
# ignore.lst: сверка mtime и лог при изменении файла.
[void](Get-RdpMonitorIgnoreListEntries)
@@ -2761,7 +2845,12 @@ function Start-LoginMonitor {
Write-Log "Ошибка цикла мониторинга: $($_.Exception.Message)"
}
Start-Sleep -Seconds $MonitorInterval
}
}
if ($script:MonitorRecycleRequested) {
return
}
} while ($true)
}
$script:StopNotificationSent = $false
@@ -2792,6 +2881,16 @@ try {
}
}
Start-LoginMonitor -MonitorInterval 5 -MonitorInteractiveOnly
if ($script:MonitorRecycleRequested) {
$script:StopNotificationSent = $true
$canonicalScript = Join-Path $script:InstallRoot $script:CanonicalScriptName
Write-Log "Graceful recycle: запуск нового процесса монитора ($canonicalScript)."
Start-Process -FilePath (Get-RdpMonitorPowerShellExe) -ArgumentList @(
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $canonicalScript
) -WindowStyle Hidden | Out-Null
exit 0
}
} catch {
if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) {
Write-Log "Выполнение прервано (Ctrl+C / Stop-Pipeline)."