Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74ebbbe161 | |||
| 5d9176f958 | |||
| eff265ded3 |
@@ -49,6 +49,10 @@ Watchdog регистрируется через **`schtasks.exe /Create /SC MIN
|
|||||||
|
|
||||||
Сразу после успешного **`/Create`** вызывается **`schtasks /Run`** для watchdog: первый прогон не ждёт следующего 5‑минутного окна расписания.
|
Сразу после успешного **`/Create`** вызывается **`schtasks /Run`** для watchdog: первый прогон не ждёт следующего 5‑минутного окна расписания.
|
||||||
|
|
||||||
|
Если watchdog срабатывает, а основной монитор «не поднимается», смотрите **`Logs\watchdog.log`** (сообщение о PID и предупреждение, если процесс сразу завершился) и **конец `Logs\login_monitor.log`** — частая причина (исправлено в **1.3.5+**): пустой **`$PSCommandPath`** у процесса, запущенного через **`Start-Process`**; скрипт теперь подставляет путь через **`$PSScriptRoot`**.
|
||||||
|
|
||||||
|
Один экземпляр монитора фиксируется **файлом блокировки** в **`C:\ProgramData\RDP-login-monitor\.login_monitor_single_instance.lock`** (не Global mutex): так и **SYSTEM** (задача планировщика), и **интерактивный администратор** могут корректно запускать скрипт вручную без «Отказано в доступе» к mutex (версия **1.3.6+**).
|
||||||
|
|
||||||
Проверка:
|
Проверка:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|||||||
+86
-14
@@ -61,15 +61,15 @@ $script:InstallRoot = [System.IO.Path]::GetFullPath("$env:ProgramData\RDP-login-
|
|||||||
$script:CanonicalScriptName = "Login_Monitor.ps1"
|
$script:CanonicalScriptName = "Login_Monitor.ps1"
|
||||||
$script:ScheduledTaskNameMain = "RDP-Login-Monitor"
|
$script:ScheduledTaskNameMain = "RDP-Login-Monitor"
|
||||||
$script:ScheduledTaskNameWatchdog = "RDP-Login-Monitor-Watchdog"
|
$script:ScheduledTaskNameWatchdog = "RDP-Login-Monitor-Watchdog"
|
||||||
$script:MutexName = "Global\RDP-Login-Monitor-Singleton-v1"
|
# Один экземпляр: эксклюзивная блокировка файла в InstallRoot (SYSTEM и интерактивный админ — одинаково; Global mutex давал «Отказано в доступе» между контекстами).
|
||||||
$script:RdpInstanceMutex = $null
|
$script:MonitorSingletonLockStream = $null
|
||||||
|
|
||||||
# Версия: пишется в лог и в 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.3.4"
|
$ScriptVersion = "1.3.11"
|
||||||
|
|
||||||
# Логи (все под InstallRoot)
|
# Логи (все под InstallRoot)
|
||||||
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
||||||
@@ -192,6 +192,10 @@ function Write-Log {
|
|||||||
function Get-RdpMonitorThisScriptPath {
|
function Get-RdpMonitorThisScriptPath {
|
||||||
$p = $PSCommandPath
|
$p = $PSCommandPath
|
||||||
if ([string]::IsNullOrWhiteSpace($p)) { $p = $MyInvocation.MyCommand.Path }
|
if ([string]::IsNullOrWhiteSpace($p)) { $p = $MyInvocation.MyCommand.Path }
|
||||||
|
# При запуске дочернего процесса через Start-Process иногда нет PSCommandPath — используем каталог скрипта.
|
||||||
|
if ([string]::IsNullOrWhiteSpace($p) -and -not [string]::IsNullOrWhiteSpace($PSScriptRoot)) {
|
||||||
|
$p = Join-Path $PSScriptRoot $script:CanonicalScriptName
|
||||||
|
}
|
||||||
if ([string]::IsNullOrWhiteSpace($p)) { return $null }
|
if ([string]::IsNullOrWhiteSpace($p)) { return $null }
|
||||||
return [System.IO.Path]::GetFullPath($p)
|
return [System.IO.Path]::GetFullPath($p)
|
||||||
}
|
}
|
||||||
@@ -273,11 +277,47 @@ function Invoke-RdpMonitorProcessMigrationAndRelaunch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Lock-RdpMonitorSingleInstance {
|
function Lock-RdpMonitorSingleInstance {
|
||||||
$mtx = New-Object System.Threading.Mutex($false, $script:MutexName)
|
# Эксклюзивный поток (FileShare.None): пока монитор работает, файл нельзя удалить вручную —
|
||||||
$script:RdpInstanceMutex = $mtx
|
# это нормально. DeleteOnClose: при любом завершении процесса ОС удалит файл при закрытии
|
||||||
if (-not $mtx.WaitOne(0)) {
|
# дескриптора (в т.ч. при «Снять задачу»), без «вечных» сирот на диске.
|
||||||
Write-Log "Экземпляр монитора уже активен (mutex). Выход без дублирования."
|
$lockPath = Join-Path $script:InstallRoot '.login_monitor_single_instance.lock'
|
||||||
|
try {
|
||||||
|
$script:MonitorSingletonLockStream = New-Object System.IO.FileStream(
|
||||||
|
$lockPath,
|
||||||
|
[System.IO.FileMode]::OpenOrCreate,
|
||||||
|
[System.IO.FileAccess]::ReadWrite,
|
||||||
|
[System.IO.FileShare]::None,
|
||||||
|
4096,
|
||||||
|
[System.IO.FileOptions]::DeleteOnClose
|
||||||
|
)
|
||||||
|
} catch [System.IO.IOException] {
|
||||||
|
Write-Log "Экземпляр монитора уже активен (блокировка файла). Выход без дублирования."
|
||||||
exit 0
|
exit 0
|
||||||
|
} catch {
|
||||||
|
Write-Log "Не удалось занять блокировку экземпляра (${lockPath}): $($_.Exception.Message)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Release-RdpMonitorSingletonLock {
|
||||||
|
$lockPath = Join-Path $script:InstallRoot '.login_monitor_single_instance.lock'
|
||||||
|
if ($null -ne $script:MonitorSingletonLockStream) {
|
||||||
|
try {
|
||||||
|
$script:MonitorSingletonLockStream.Close()
|
||||||
|
} catch { }
|
||||||
|
try {
|
||||||
|
$script:MonitorSingletonLockStream.Dispose()
|
||||||
|
} catch { }
|
||||||
|
$script:MonitorSingletonLockStream = $null
|
||||||
|
}
|
||||||
|
if (Test-Path -LiteralPath $lockPath) {
|
||||||
|
try {
|
||||||
|
Remove-Item -LiteralPath $lockPath -Force -ErrorAction Stop
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
[System.IO.File]::Delete($lockPath)
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +381,22 @@ function Register-RdpMonitorScheduledTasksCore {
|
|||||||
}
|
}
|
||||||
Write-Log "Задача планировщика: $($script:ScheduledTaskNameWatchdog) (schtasks, каждые 5 минут, контроль процесса)."
|
Write-Log "Задача планировщика: $($script:ScheduledTaskNameWatchdog) (schtasks, каждые 5 минут, контроль процесса)."
|
||||||
|
|
||||||
# Первый запуск по расписанию может быть почти через 5 мин — сразу ставим одноразовый прогон в очередь.
|
# Не ждём перезагрузку: сразу запускаем основную задачу.
|
||||||
|
$runMainEa = $ErrorActionPreference
|
||||||
|
try {
|
||||||
|
$ErrorActionPreference = 'SilentlyContinue'
|
||||||
|
$runMainOut = & $schtasksExe /Run /TN $script:ScheduledTaskNameMain 2>&1
|
||||||
|
foreach ($line in @($runMainOut)) {
|
||||||
|
if ($null -ne $line -and "$line".Trim().Length -gt 0) {
|
||||||
|
Write-Log "schtasks main /Run: $line"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$ErrorActionPreference = $runMainEa
|
||||||
|
}
|
||||||
|
Write-Log "Немедленный прогон основной задачи запрошен (schtasks /Run)."
|
||||||
|
|
||||||
|
# Первый запуск watchdog по расписанию может быть почти через 5 мин — ставим одноразовый прогон в очередь.
|
||||||
$runEa = $ErrorActionPreference
|
$runEa = $ErrorActionPreference
|
||||||
try {
|
try {
|
||||||
$ErrorActionPreference = 'SilentlyContinue'
|
$ErrorActionPreference = 'SilentlyContinue'
|
||||||
@@ -409,9 +464,18 @@ function Start-RdpMonitorWatchdogMain {
|
|||||||
|
|
||||||
if (-not $mainRunning) {
|
if (-not $mainRunning) {
|
||||||
Write-WdLog "Монитор не найден — запуск $canonicalScript"
|
Write-WdLog "Монитор не найден — запуск $canonicalScript"
|
||||||
Start-Process -FilePath (Get-RdpMonitorPowerShellExe) -ArgumentList @(
|
$psExeWd = Get-RdpMonitorPowerShellExe
|
||||||
|
$started = Start-Process -FilePath $psExeWd -WorkingDirectory $script:InstallRoot -ArgumentList @(
|
||||||
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $canonicalScript
|
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $canonicalScript
|
||||||
) -WindowStyle Hidden
|
) -WindowStyle Hidden -PassThru
|
||||||
|
Write-WdLog "Монитор: создан процесс PID=$($started.Id)."
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
$still = Get-Process -Id $started.Id -ErrorAction SilentlyContinue
|
||||||
|
if (-not $still) {
|
||||||
|
Write-WdLog "ПРЕДУПРЕЖДЕНИЕ: процесс монитора PID $($started.Id) завершился за несколько секунд — см. конец $script:LogFile (ошибка до цикла мониторинга)."
|
||||||
|
} else {
|
||||||
|
Write-WdLog "Монитор: процесс PID $($started.Id) ещё работает после старта."
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Write-WdLog "Монитор работает (канонический экземпляр найден)."
|
Write-WdLog "Монитор работает (канонический экземпляр найден)."
|
||||||
}
|
}
|
||||||
@@ -821,6 +885,7 @@ function Send-Heartbeat {
|
|||||||
|
|
||||||
if ($IsStartup) {
|
if ($IsStartup) {
|
||||||
$message = "<b>✅ Мониторинг логинов ЗАПУЩЕН</b>`r`n"
|
$message = "<b>✅ Мониторинг логинов ЗАПУЩЕН</b>`r`n"
|
||||||
|
$message += "🏷️ Версия скрипта: $(ConvertTo-TelegramHtml $ScriptVersion)`r`n"
|
||||||
$message += "🖥️ Сервер: $hHost`r`n"
|
$message += "🖥️ Сервер: $hHost`r`n"
|
||||||
$message += "🕐 Время запуска: $timestamp"
|
$message += "🕐 Время запуска: $timestamp"
|
||||||
if ($script:OsInstallKindLabel) {
|
if ($script:OsInstallKindLabel) {
|
||||||
@@ -1446,6 +1511,7 @@ function Start-LoginMonitor {
|
|||||||
$nextReportCheck = Check-AndSendDailyReport
|
$nextReportCheck = Check-AndSendDailyReport
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) { throw }
|
||||||
Write-Log "Ошибка цикла мониторинга: $($_.Exception.Message)"
|
Write-Log "Ошибка цикла мониторинга: $($_.Exception.Message)"
|
||||||
}
|
}
|
||||||
Start-Sleep -Seconds $MonitorInterval
|
Start-Sleep -Seconds $MonitorInterval
|
||||||
@@ -1457,11 +1523,17 @@ try {
|
|||||||
Test-TelegramConnection | Out-Null
|
Test-TelegramConnection | Out-Null
|
||||||
Start-LoginMonitor -MonitorInterval 5 -MonitorInteractiveOnly
|
Start-LoginMonitor -MonitorInterval 5 -MonitorInteractiveOnly
|
||||||
} catch {
|
} catch {
|
||||||
Write-Log "Критическая ошибка: $($_.Exception.Message)"
|
if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) {
|
||||||
Send-StopNotification -Reason "Критическая ошибка: $($_.Exception.Message)"
|
Write-Log "Выполнение прервано (Ctrl+C / Stop-Pipeline)."
|
||||||
$script:StopNotificationSent = $true
|
$script:StopNotificationSent = $true
|
||||||
throw
|
} else {
|
||||||
|
Write-Log "Критическая ошибка: $($_.Exception.Message)"
|
||||||
|
Send-StopNotification -Reason "Критическая ошибка: $($_.Exception.Message)"
|
||||||
|
$script:StopNotificationSent = $true
|
||||||
|
throw
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
Release-RdpMonitorSingletonLock
|
||||||
if (-not $script:StopNotificationSent) {
|
if (-not $script:StopNotificationSent) {
|
||||||
Send-StopNotification -Reason "Скрипт завершил работу"
|
Send-StopNotification -Reason "Скрипт завершил работу"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1.3.4
|
1.3.11
|
||||||
|
|||||||
Reference in New Issue
Block a user