feat: graceful restart without Stop-Process; SAC key in settings example
This commit is contained in:
+46
-2
@@ -147,8 +147,52 @@ function Test-CommandLineIsWatchdog {
|
||||
return ($CommandLine -match '(?i)(^|\s)-Watchdog(\s|$)')
|
||||
}
|
||||
|
||||
function Test-RdpMonitorMainProcessRunning {
|
||||
param([string]$CanonicalScript)
|
||||
try {
|
||||
$procs = Get-CimInstance Win32_Process -Filter "Name = 'powershell.exe' OR Name = 'pwsh.exe'" -ErrorAction Stop
|
||||
foreach ($proc in $procs) {
|
||||
$cl = [string]$proc.CommandLine
|
||||
if ($cl -notmatch 'Login_Monitor\.ps1') { continue }
|
||||
if (Test-CommandLineIsWatchdog -CommandLine $cl) { continue }
|
||||
$sp = Get-ScriptPathFromCommandLine -CommandLine $cl
|
||||
if ($null -eq $sp) { continue }
|
||||
if ([System.IO.Path]::GetFullPath($sp) -ne $CanonicalScript) { continue }
|
||||
if ([int]$proc.ProcessId -eq $PID) { continue }
|
||||
return $true
|
||||
}
|
||||
} catch { }
|
||||
return $false
|
||||
}
|
||||
|
||||
function Stop-RdpLoginMonitorMainProcesses {
|
||||
param([int]$GracefulWaitSec = 90)
|
||||
|
||||
$canonical = [System.IO.Path]::GetFullPath($LocalScript)
|
||||
if (-not (Test-RdpMonitorMainProcessRunning -CanonicalScript $canonical)) {
|
||||
Write-DeployLog "Монитор не запущен — остановка не требуется."
|
||||
return
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $LocalScript) {
|
||||
Write-DeployLog "Graceful recycle: Login_Monitor.ps1 -RequestRestart -Recycle (без Stop-Process)."
|
||||
$recycleArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript, '-RequestRestart', '-Recycle')
|
||||
$p = Start-Process -FilePath $PsExe -ArgumentList $recycleArgs -Wait -PassThru -WindowStyle Hidden
|
||||
if ($p.ExitCode -ne 0) {
|
||||
Write-DeployLog "Предупреждение: -RequestRestart -Recycle завершился с кодом $($p.ExitCode)."
|
||||
}
|
||||
}
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($GracefulWaitSec)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if (-not (Test-RdpMonitorMainProcessRunning -CanonicalScript $canonical)) {
|
||||
Write-DeployLog "Монитор завершился gracefully (ожидание $($GracefulWaitSec) с)."
|
||||
return
|
||||
}
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
|
||||
Write-DeployLog "Таймаут graceful recycle — принудительная остановка оставшихся процессов монитора."
|
||||
try {
|
||||
$procs = Get-CimInstance Win32_Process -Filter "Name = 'powershell.exe' OR Name = 'pwsh.exe'" -ErrorAction Stop
|
||||
foreach ($proc in $procs) {
|
||||
@@ -159,11 +203,11 @@ function Stop-RdpLoginMonitorMainProcesses {
|
||||
if ($null -eq $sp) { continue }
|
||||
if ([System.IO.Path]::GetFullPath($sp) -ne $canonical) { continue }
|
||||
if ([int]$proc.ProcessId -eq $PID) { continue }
|
||||
Write-DeployLog "Останавливаю процесс монитора PID $($proc.ProcessId) перед обновлением файла."
|
||||
Write-DeployLog "Stop-Process -Force PID $($proc.ProcessId) (fallback)."
|
||||
Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
} catch {
|
||||
Write-DeployLog "Предупреждение при остановке монитора: $($_.Exception.Message)"
|
||||
Write-DeployLog "Предупреждение при принудительной остановке: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
- `Sac-Client.ps1`
|
||||
- `version.txt`
|
||||
- `Deploy-LoginMonitor.ps1`
|
||||
- `Restart-RdpLoginMonitor.ps1`
|
||||
- `Exchange-MailSecurity.ps1`
|
||||
- `Notify-Common.ps1`
|
||||
- `Install-DomainMonitors.ps1`
|
||||
|
||||
@@ -155,6 +155,28 @@ Get-ScheduledTask -TaskName 'RDP-Login-Monitor','RDP-Login-Monitor-Watchdog' -Er
|
||||
Get-Content 'C:\ProgramData\RDP-login-monitor\Logs\deploy.log' -Tail 15
|
||||
```
|
||||
|
||||
### Graceful restart (без убийства PowerShell)
|
||||
|
||||
После правки **`login_monitor.settings.ps1`** (SAC, Telegram):
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass `
|
||||
-File "C:\ProgramData\RDP-login-monitor\Login_Monitor.ps1" -RequestRestart
|
||||
```
|
||||
|
||||
Или скрипт из репозитория/шары: **`Restart-RdpLoginMonitor.ps1`**.
|
||||
|
||||
Чтобы подхватить **новый `Login_Monitor.ps1` с диска** (после Deploy), нужен **recycle** — новый скрытый процесс, старый завершается сам:
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass `
|
||||
-File "C:\ProgramData\RDP-login-monitor\Login_Monitor.ps1" -RequestRestart -Recycle
|
||||
```
|
||||
|
||||
**`Deploy-LoginMonitor.ps1`** при обновлении вызывает `-RequestRestart -Recycle` и ждёт до 90 с; **`Stop-Process -Force`** только если таймаут.
|
||||
|
||||
Сигнал: файл **`C:\ProgramData\RDP-login-monitor\restart.request`** (создаётся автоматически, не редактировать вручную).
|
||||
|
||||
### D. Первичная установка (ещё нет ProgramData)
|
||||
|
||||
1. Deploy (как в C) — создаст каталог и при отсутствии settings скопирует **`login_monitor.settings.ps1`** из example.
|
||||
|
||||
+120
-21
@@ -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)."
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Graceful restart RDP Login Monitor без Stop-Process.
|
||||
.DESCRIPTION
|
||||
settings — перечитать login_monitor.settings.ps1 в том же процессе PowerShell.
|
||||
recycle — корректно завершить монитор и запустить новый процесс (после обновления Login_Monitor.ps1).
|
||||
.EXAMPLE
|
||||
powershell -ExecutionPolicy Bypass -File Restart-RdpLoginMonitor.ps1
|
||||
powershell -ExecutionPolicy Bypass -File Restart-RdpLoginMonitor.ps1 -Recycle
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$Recycle
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$installRoot = [System.IO.Path]::GetFullPath("$env:ProgramData\RDP-login-monitor")
|
||||
$monitorScript = Join-Path $installRoot 'Login_Monitor.ps1'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $monitorScript)) {
|
||||
Write-Error "Не найден: $monitorScript"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$args = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $monitorScript, '-RequestRestart')
|
||||
if ($Recycle) { $args += '-Recycle' }
|
||||
|
||||
& "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" @args
|
||||
exit $LASTEXITCODE
|
||||
@@ -29,9 +29,9 @@ $NotifyOrder = 'tg'
|
||||
|
||||
# --- Security Alert Center (SAC) ---
|
||||
# off | exclusive | dual | fallback — см. security-alert-center/docs/agent-integration.md
|
||||
# $UseSAC = 'exclusive'
|
||||
# $SacUrl = 'https://sac.kalinamall.ru'
|
||||
# $SacApiKey = 'sac_xxxxxxxx'
|
||||
$UseSAC = 'dual'
|
||||
$SacUrl = 'https://sac.kalinamall.ru'
|
||||
$SacApiKey = 'sac_UkOsAT3UWiQS54KK5OJPBDCSucysQDrKFju28wmYiz8'
|
||||
# $SacSpoolDir = 'C:\ProgramData\RDP-login-monitor\sac-spool'
|
||||
# $SacTimeoutSec = 12
|
||||
# $SacTlsSkipVerify = $false
|
||||
|
||||
@@ -25,6 +25,7 @@ $DistFiles = @(
|
||||
'Sac-Client.ps1',
|
||||
'version.txt',
|
||||
'Deploy-LoginMonitor.ps1',
|
||||
'Restart-RdpLoginMonitor.ps1',
|
||||
'Exchange-MailSecurity.ps1',
|
||||
'Notify-Common.ps1',
|
||||
'Install-DomainMonitors.ps1',
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.2.0-SAC
|
||||
1.2.1-SAC
|
||||
|
||||
Reference in New Issue
Block a user