feat: single deploy startup, stop mode, notification_body 1.2.31-SAC

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
PTah
2026-05-31 18:11:18 +10:00
parent a74c8df877
commit e1ae9834a9
4 changed files with 108 additions and 44 deletions
+25 -10
View File
@@ -190,8 +190,8 @@ function Test-RdpMonitorMainProcessRunning {
function Set-RdpMonitorRestartRequestFromDeploy { function Set-RdpMonitorRestartRequestFromDeploy {
param( param(
[ValidateSet('settings', 'recycle')] [ValidateSet('settings', 'recycle', 'stop')]
[string]$Mode = 'recycle', [string]$Mode = 'stop',
[string]$Reason = 'deploy' [string]$Reason = 'deploy'
) )
@@ -617,8 +617,8 @@ function Stop-RdpLoginMonitorMainProcesses {
return return
} }
Write-DeployLog "Graceful recycle: запись restart.request (mode=recycle), без дочернего PowerShell." Write-DeployLog "Graceful stop: запись restart.request (mode=stop), без дочернего PowerShell."
Set-RdpMonitorRestartRequestFromDeploy -Mode 'recycle' -Reason 'deploy' Set-RdpMonitorRestartRequestFromDeploy -Mode 'stop' -Reason 'deploy'
$deadline = (Get-Date).AddSeconds($GracefulWaitSec) $deadline = (Get-Date).AddSeconds($GracefulWaitSec)
while ((Get-Date) -lt $deadline) { while ((Get-Date) -lt $deadline) {
@@ -629,7 +629,7 @@ function Stop-RdpLoginMonitorMainProcesses {
Start-Sleep -Seconds 1 Start-Sleep -Seconds 1
} }
Write-DeployLog "Таймаут graceful recycle — принудительная остановка оставшихся процессов монитора." Write-DeployLog "Таймаут graceful stop — принудительная остановка оставшихся процессов монитора."
try { try {
$procs = Get-CimInstance Win32_Process -Filter "Name = 'powershell.exe' OR Name = 'pwsh.exe'" -ErrorAction Stop $procs = Get-CimInstance Win32_Process -Filter "Name = 'powershell.exe' OR Name = 'pwsh.exe'" -ErrorAction Stop
foreach ($proc in $procs) { foreach ($proc in $procs) {
@@ -751,7 +751,7 @@ try {
Sync-RdpMonitorSettingsFromShare -ExampleOnShare $settingsExampleShare -LocalSettings $settingsLocal Sync-RdpMonitorSettingsFromShare -ExampleOnShare $settingsExampleShare -LocalSettings $settingsLocal
$installArgs = @( $installArgs = @(
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript, '-InstallTasks' '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript, '-InstallTasks', '-SkipImmediateMainRun'
) )
$instOut = Join-Path $InstallRoot "Logs\deploy_installtasks_stdout.log" $instOut = Join-Path $InstallRoot "Logs\deploy_installtasks_stdout.log"
$instErr = Join-Path $InstallRoot "Logs\deploy_installtasks_stderr.log" $instErr = Join-Path $InstallRoot "Logs\deploy_installtasks_stderr.log"
@@ -787,10 +787,25 @@ try {
Write-DeployLog "Записана метка обновления: $DeployUpdateMarkerPath (Version=$shareVerRaw; UpdatedAt=$updStamp)." Write-DeployLog "Записана метка обновления: $DeployUpdateMarkerPath (Version=$shareVerRaw; UpdatedAt=$updStamp)."
if (-not $SkipStartMonitorAfterUpdate) { if (-not $SkipStartMonitorAfterUpdate) {
Start-Process -FilePath $PsExe -ArgumentList @( $canonical = [System.IO.Path]::GetFullPath($LocalScript)
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript if (Test-RdpMonitorMainProcessRunning -CanonicalScript $canonical) {
) -WindowStyle Hidden Write-DeployLog "Монитор уже запущен — повторный старт не выполняем."
Write-DeployLog "Запущен процесс монитора (новый файл)." } else {
$taskName = 'RDP-Login-Monitor'
$runEa = $ErrorActionPreference
try {
$ErrorActionPreference = 'SilentlyContinue'
$runOut = & schtasks.exe /Run /TN $taskName 2>&1
foreach ($line in @($runOut)) {
if ($null -ne $line -and "$line".Trim().Length -gt 0) {
Write-DeployLog "schtasks /Run $taskName : $line"
}
}
} finally {
$ErrorActionPreference = $runEa
}
Write-DeployLog "Запуск монитора через schtasks /Run ($taskName)."
}
} else { } else {
Write-DeployLog "Запуск монитора пропущен (-SkipStartMonitorAfterUpdate); поднимется при следующей загрузке или watchdog." Write-DeployLog "Запуск монитора пропущен (-SkipStartMonitorAfterUpdate); поднимется при следующей загрузке или watchdog."
} }
+51 -8
View File
@@ -35,6 +35,7 @@ param(
[string]$MailSmtpPasswordProtectedB64 = '', [string]$MailSmtpPasswordProtectedB64 = '',
[switch]$Watchdog, [switch]$Watchdog,
[switch]$InstallTasks, [switch]$InstallTasks,
[switch]$SkipImmediateMainRun,
[switch]$SkipScheduledTaskMaintenance, [switch]$SkipScheduledTaskMaintenance,
[switch]$CheckSac, [switch]$CheckSac,
[switch]$RequestRestart, [switch]$RequestRestart,
@@ -74,13 +75,14 @@ $script:MonitorSingletonLockStream = $null
$script:MonitorRestartRequestFile = Join-Path $script:InstallRoot 'restart.request' $script:MonitorRestartRequestFile = Join-Path $script:InstallRoot 'restart.request'
$script:MonitorRecycleRequested = $false $script:MonitorRecycleRequested = $false
$script:MonitorLoopInitialized = $false $script:MonitorLoopInitialized = $false
$script:MonitorStopRequested = $false
# Версия: пишется в лог и в Telegram. При доменном развёртывании через шару см. DEPLOY.md — # Версия: пишется в лог и в Telegram. При доменном развёртывании через шару см. DEPLOY.md —
# триггер обновления на клиентах даёт файл version.txt на шаре (его номер можно поднять и без смены # триггер обновления на клиентах даёт файл version.txt на шаре (его номер можно поднять и без смены
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах). # строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только # Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1). # исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
$ScriptVersion = "1.2.30-SAC" $ScriptVersion = "1.2.31-SAC"
# Логи (все под InstallRoot) # Логи (все под InstallRoot)
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log" $LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
@@ -415,7 +417,7 @@ function Get-RdpMonitorPowerShellExe {
function Set-RdpMonitorRestartRequest { function Set-RdpMonitorRestartRequest {
param( param(
[ValidateSet('settings', 'recycle')] [ValidateSet('settings', 'recycle', 'stop')]
[string]$Mode = 'settings', [string]$Mode = 'settings',
[string]$Reason = 'manual' [string]$Reason = 'manual'
) )
@@ -446,7 +448,7 @@ function Get-RdpMonitorRestartRequest {
try { try {
Remove-Item -LiteralPath $script:MonitorRestartRequestFile -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $script:MonitorRestartRequestFile -Force -ErrorAction SilentlyContinue
} catch { } } catch { }
if ($mode -ne 'recycle') { $mode = 'settings' } if ($mode -notin @('recycle', 'stop')) { $mode = 'settings' }
return [pscustomobject]@{ Mode = $mode; Reason = $reason } return [pscustomobject]@{ Mode = $mode; Reason = $reason }
} }
@@ -485,6 +487,8 @@ function Test-RdpMonitorScheduledTaskMatches {
} }
function Register-RdpMonitorScheduledTasksCore { function Register-RdpMonitorScheduledTasksCore {
param([switch]$SkipImmediateMainRun)
Write-Log "Register-RdpMonitorScheduledTasksCore: ветка v$ScriptVersion (watchdog через schtasks /SC MINUTE, без CIM RepetitionInterval)." Write-Log "Register-RdpMonitorScheduledTasksCore: ветка v$ScriptVersion (watchdog через schtasks /SC MINUTE, без CIM RepetitionInterval)."
$psExe = Get-RdpMonitorPowerShellExe $psExe = Get-RdpMonitorPowerShellExe
$canonicalScript = [System.IO.Path]::GetFullPath((Join-Path $script:InstallRoot $script:CanonicalScriptName)) $canonicalScript = [System.IO.Path]::GetFullPath((Join-Path $script:InstallRoot $script:CanonicalScriptName))
@@ -522,6 +526,7 @@ function Register-RdpMonitorScheduledTasksCore {
} }
Write-Log "Задача планировщика: $($script:ScheduledTaskNameWatchdog) (schtasks, каждые 5 минут, контроль процесса)." Write-Log "Задача планировщика: $($script:ScheduledTaskNameWatchdog) (schtasks, каждые 5 минут, контроль процесса)."
if (-not $SkipImmediateMainRun) {
# Не ждём перезагрузку: сразу запускаем основную задачу. # Не ждём перезагрузку: сразу запускаем основную задачу.
$runMainEa = $ErrorActionPreference $runMainEa = $ErrorActionPreference
try { try {
@@ -551,6 +556,9 @@ function Register-RdpMonitorScheduledTasksCore {
$ErrorActionPreference = $runEa $ErrorActionPreference = $runEa
} }
Write-Log "Немедленный прогон watchdog запрошен (schtasks /Run)." Write-Log "Немедленный прогон watchdog запрошен (schtasks /Run)."
} else {
Write-Log "Немедленный schtasks /Run пропущен (-SkipImmediateMainRun)."
}
} }
function Ensure-RdpMonitorScheduledTasks { function Ensure-RdpMonitorScheduledTasks {
@@ -640,7 +648,7 @@ if ($InstallTasks) {
Write-Log "InstallTasks: нужны права администратора." Write-Log "InstallTasks: нужны права администратора."
exit 1 exit 1
} }
Register-RdpMonitorScheduledTasksCore Register-RdpMonitorScheduledTasksCore -SkipImmediateMainRun:$SkipImmediateMainRun
Write-Log "InstallTasks: задачи планировщика обновлены." Write-Log "InstallTasks: задачи планировщика обновлены."
exit 0 exit 0
} }
@@ -765,6 +773,18 @@ function ConvertTo-TelegramHtml {
return [System.Net.WebUtility]::HtmlEncode([string]$Text) return [System.Net.WebUtility]::HtmlEncode([string]$Text)
} }
function Convert-TelegramHtmlToPlainBody {
param([string]$Html)
if ([string]::IsNullOrWhiteSpace($Html)) { return '' }
$text = [string]$Html
$text = $text -replace '</?b>', ''
$text = $text -replace '</?i>', ''
$text = $text -replace '</?code>', ''
$text = $text -replace '</?pre>', ''
$text = [System.Net.WebUtility]::HtmlDecode($text)
return $text.Trim()
}
function Send-TelegramMessage { function Send-TelegramMessage {
param([string]$Message) param([string]$Message)
@@ -964,9 +984,17 @@ function Send-RdpMonitorLifecycleNotification {
) )
$title = if (-not [string]::IsNullOrWhiteSpace($SacTitle)) { $SacTitle } else { "RDP login monitor: $Lifecycle" } $title = if (-not [string]::IsNullOrWhiteSpace($SacTitle)) { $SacTitle } else { "RDP login monitor: $Lifecycle" }
$plainBody = Convert-TelegramHtmlToPlainBody -Html $TelegramHtmlMessage
$sacDetails = @{
lifecycle = $Lifecycle
trigger = $Trigger
}
if (-not [string]::IsNullOrWhiteSpace($plainBody)) {
$sacDetails.notification_body = $plainBody
}
Send-MonitorNotification -Message $TelegramHtmlMessage -EmailSubject $EmailSubject ` Send-MonitorNotification -Message $TelegramHtmlMessage -EmailSubject $EmailSubject `
-SacEventType 'agent.lifecycle' -SacSeverity $SacSeverity -SacTitle $title -SacSummary $SacSummary ` -SacEventType 'agent.lifecycle' -SacSeverity $SacSeverity -SacTitle $title -SacSummary $SacSummary `
-SacDetails @{ lifecycle = $Lifecycle; trigger = $Trigger } | Out-Null -SacDetails $sacDetails | Out-Null
} }
function Test-Administrator { function Test-Administrator {
@@ -1779,7 +1807,7 @@ function Send-Heartbeat {
param([switch]$IsStartup = $false) param([switch]$IsStartup = $false)
$timestamp = Get-Date -Format "dd.MM.yyyy HH:mm:ss" $timestamp = Get-Date -Format "dd.MM.yyyy HH:mm:ss"
$hHost = (ConvertTo-TelegramHtml (Get-MonitorServerLabel)) $hHost = (ConvertTo-TelegramHtml (Get-MonitorServerLabelWithIp))
function Get-DeployUpdateMarker { function Get-DeployUpdateMarker {
$info = [pscustomobject]@{ $info = [pscustomobject]@{
@@ -1885,7 +1913,7 @@ function Send-Heartbeat {
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-MonitorServerLabel), версия $ScriptVersion" -SacSummary "Мониторинг запущен на $(Get-MonitorServerLabelWithIp), версия $ScriptVersion"
Write-Log "Отправлено уведомление о запуске скрипта (каналы: $notifyChain)" Write-Log "Отправлено уведомление о запуске скрипта (каналы: $notifyChain)"
} else { } else {
Write-TextFileUtf8Bom -Path $HeartbeatFile -Text $timestamp Write-TextFileUtf8Bom -Path $HeartbeatFile -Text $timestamp
@@ -3151,6 +3179,7 @@ function Start-LoginMonitor {
do { do {
$script:MonitorRecycleRequested = $false $script:MonitorRecycleRequested = $false
$script:MonitorStopRequested = $false
if (-not $script:MonitorLoopInitialized) { if (-not $script:MonitorLoopInitialized) {
Cleanup-OldLogs Cleanup-OldLogs
@@ -3185,6 +3214,11 @@ function Start-LoginMonitor {
$restartReq = Get-RdpMonitorRestartRequest $restartReq = Get-RdpMonitorRestartRequest
if ($null -ne $restartReq) { if ($null -ne $restartReq) {
$script:StopNotificationSent = $true $script:StopNotificationSent = $true
if ($restartReq.Mode -eq 'stop') {
Write-Log "Graceful stop: запрос '$($restartReq.Reason)' — выход без запуска нового процесса."
$script:MonitorStopRequested = $true
break
}
if ($restartReq.Mode -eq 'recycle') { if ($restartReq.Mode -eq 'recycle') {
Write-Log "Graceful recycle: запрос '$($restartReq.Reason)' — выход для запуска нового процесса (обновлённый скрипт с диска)." Write-Log "Graceful recycle: запрос '$($restartReq.Reason)' — выход для запуска нового процесса (обновлённый скрипт с диска)."
$script:MonitorRecycleRequested = $true $script:MonitorRecycleRequested = $true
@@ -3192,7 +3226,7 @@ function Start-LoginMonitor {
} }
Write-Log "Graceful restart (settings): запрос '$($restartReq.Reason)' — перечитываю настройки в этом же процессе PowerShell." Write-Log "Graceful restart (settings): запрос '$($restartReq.Reason)' — перечитываю настройки в этом же процессе PowerShell."
Invoke-RdpMonitorReloadSettings | Out-Null Invoke-RdpMonitorReloadSettings | Out-Null
$hHost = ConvertTo-TelegramHtml (Get-MonitorServerLabel) $hHost = ConvertTo-TelegramHtml (Get-MonitorServerLabelWithIp)
$reloadMsg = "<b>🔄 Настройки монитора перечитаны</b>`r`n" $reloadMsg = "<b>🔄 Настройки монитора перечитаны</b>`r`n"
$reloadMsg += "🖥️ Сервер: $hHost`r`n" $reloadMsg += "🖥️ Сервер: $hHost`r`n"
$reloadMsg += "🏷️ Версия: $(ConvertTo-TelegramHtml $ScriptVersion)`r`n" $reloadMsg += "🏷️ Версия: $(ConvertTo-TelegramHtml $ScriptVersion)`r`n"
@@ -3576,6 +3610,9 @@ function Start-LoginMonitor {
if ($script:MonitorRecycleRequested) { if ($script:MonitorRecycleRequested) {
return return
} }
if ($script:MonitorStopRequested) {
return
}
} while ($true) } while ($true)
} }
@@ -3623,6 +3660,12 @@ try {
} }
Start-LoginMonitor -MonitorInterval 5 -MonitorInteractiveOnly Start-LoginMonitor -MonitorInterval 5 -MonitorInteractiveOnly
if ($script:MonitorStopRequested) {
$script:StopNotificationSent = $true
Write-Log "Graceful stop: завершение без запуска нового процесса."
exit 0
}
if ($script:MonitorRecycleRequested) { if ($script:MonitorRecycleRequested) {
$script:StopNotificationSent = $true $script:StopNotificationSent = $true
$canonicalScript = Join-Path $script:InstallRoot $script:CanonicalScriptName $canonicalScript = Join-Path $script:InstallRoot $script:CanonicalScriptName
+8 -2
View File
@@ -253,12 +253,18 @@ function Get-SacHostBlock {
hostname = $hostname hostname = $hostname
os_family = 'windows' os_family = 'windows'
} }
if (Get-Variable -Name ServerDisplayName -ErrorAction SilentlyContinue) { $displayLabel = $hostname
if (Get-Command -Name Get-MonitorServerLabelWithIp -ErrorAction SilentlyContinue) {
$displayLabel = [string](Get-MonitorServerLabelWithIp)
} elseif (Get-Variable -Name ServerDisplayName -ErrorAction SilentlyContinue) {
$label = (Get-Variable -Name ServerDisplayName -ValueOnly) $label = (Get-Variable -Name ServerDisplayName -ValueOnly)
if (-not [string]::IsNullOrWhiteSpace([string]$label)) { if (-not [string]::IsNullOrWhiteSpace([string]$label)) {
$hostBlock.display_name = [string]$label.Trim() $displayLabel = [string]$label.Trim()
} }
} }
if (-not [string]::IsNullOrWhiteSpace($displayLabel)) {
$hostBlock.display_name = $displayLabel
}
$ipv4 = Get-SacHostIPv4 $ipv4 = Get-SacHostIPv4
if (-not [string]::IsNullOrWhiteSpace($ipv4)) { if (-not [string]::IsNullOrWhiteSpace($ipv4)) {
$hostBlock.ipv4 = $ipv4 $hostBlock.ipv4 = $ipv4
+1 -1
View File
@@ -1 +1 @@
1.2.30-SAC 1.2.31-SAC