Files
RDP-login-monitor/Deploy-LoginMonitor.ps1
T
PTah 577b8bc220 feat(Login): вынести локальные настройки в login_monitor.settings.ps1
Секреты, SMTP, 4740 и IP-исключения больше не в Login_Monitor.ps1 —
не слетают при автообновлении с NETLOGON. Deploy создаёт settings из example при первой установке.
2026-05-25 09:44:43 +10:00

291 lines
13 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<#
.SYNOPSIS
Доставка Login_Monitor.ps1 с файловой шары по версии (домен: ПК и серверы).
.DESCRIPTION
Читает version.txt на шаре, сравнивает с локальной меткой (или с версией в установленном скрипте).
При необходимости копирует Login_Monitor.ps1 в C:\ProgramData\RDP-login-monitor\, регистрирует задачи (-InstallTasks),
перезапускает процесс монитора.
Предназначен для GPO «Сценарий запуска компьютера» (SYSTEM); можно запускать вручную от администратора.
СТРУКТУРА НА ШАРЕ (пример):
\\dc\share\RDP-login-monitor\Login_Monitor.ps1
\\dc\share\RDP-login-monitor\version.txt — одна строка, например: 1.3.0
\\dc\share\RDP-login-monitor\Deploy-LoginMonitor.ps1
Если Deploy-LoginMonitor.ps1 запускают с этой шары, параметр -SourceShareRoot можно не указывать —
корень шары берётся из расположения этого файла.
.NOTES
Лог: C:\ProgramData\RDP-login-monitor\Logs\deploy.log
#>
[CmdletBinding()]
param(
# UNC-каталог, где лежат Login_Monitor.ps1 и version.txt. Пусто = родительский каталог этого скрипта.
[string]$SourceShareRoot = "",
[switch]$WhatIf,
# После обновления не стартовать монитор (только файлы и задачи).
[switch]$SkipStartMonitorAfterUpdate,
# Разрешить установку более старой версии с шары (по умолчанию откаты блокируются).
[switch]$AllowDowngrade
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Test-DeployRunningElevated {
try {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
# LocalSystem (GPO startup / задачи SYSTEM): не всегда даёт true на BuiltInRole::Administrator.
if ($null -ne $id.User -and $id.User.Value -eq 'S-1-5-18') { return $true }
$p = New-Object Security.Principal.WindowsPrincipal($id)
return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
} catch {
return $false
}
}
$InstallRoot = [System.IO.Path]::GetFullPath("$env:ProgramData\RDP-login-monitor")
$LocalScript = Join-Path $InstallRoot "Login_Monitor.ps1"
$VersionStampPath = Join-Path $InstallRoot "deployed_version.txt"
$DeployUpdateMarkerPath = Join-Path $InstallRoot "deploy_last_update.txt"
$DeployLogPath = Join-Path $InstallRoot "Logs\deploy.log"
$ScriptName = "Login_Monitor.ps1"
$VersionFileName = "version.txt"
$PsExe = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
$Utf8Bom = New-Object System.Text.UTF8Encoding $true
function Write-DeployLog {
param([string]$Message)
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $Message" + [Environment]::NewLine
try {
$dir = Split-Path $DeployLogPath -Parent
if (-not (Test-Path -LiteralPath $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
[System.IO.File]::AppendAllText($DeployLogPath, $line, $Utf8Bom)
} catch { }
if ($Host.Name -eq 'ConsoleHost' -or $Host.Name -eq 'Windows PowerShell ISE Host') {
Write-Host $line.TrimEnd("`r`n")
}
}
function Resolve-SourceShareRoot {
if (-not [string]::IsNullOrWhiteSpace($SourceShareRoot)) {
return [System.IO.Path]::GetFullPath($SourceShareRoot.TrimEnd('\'))
}
$here = $PSCommandPath
if ([string]::IsNullOrWhiteSpace($here)) { $here = $MyInvocation.MyCommand.Path }
if ([string]::IsNullOrWhiteSpace($here)) {
throw "Укажите -SourceShareRoot или запускайте Deploy-LoginMonitor.ps1 с путём к файлу (например с UNC-шары)."
}
return [System.IO.Path]::GetFullPath((Split-Path -Parent $here))
}
function Read-VersionLineFromFile {
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path)) { return $null }
$raw = (Get-Content -LiteralPath $Path -TotalCount 3 -ErrorAction Stop | Where-Object { $_ -match '\S' }) | Select-Object -First 1
if ($null -eq $raw) { return $null }
return ([string]$raw).Trim() -replace '^v', ''
}
function Get-LocalDeployedVersion {
$fromStamp = Read-VersionLineFromFile -Path $VersionStampPath
if (-not [string]::IsNullOrWhiteSpace($fromStamp)) { return $fromStamp }
if (-not (Test-Path -LiteralPath $LocalScript)) { return $null }
try {
$head = Get-Content -LiteralPath $LocalScript -TotalCount 120 -ErrorAction Stop
foreach ($ln in $head) {
if ($ln -match '^\s*\$ScriptVersion\s*=\s*["'']([0-9]+(?:\.[0-9]+){0,3})["'']') {
return $Matches[1]
}
}
} catch { }
return $null
}
function Normalize-VersionOrNull {
param([string]$Text)
if ([string]::IsNullOrWhiteSpace($Text)) { return $null }
$t = $Text.Trim() -replace '^v', ''
try {
return [version]$t
} catch {
return $null
}
}
function Compare-VersionStrings {
param([string]$Left, [string]$Right)
$a = Normalize-VersionOrNull -Text $Left
$b = Normalize-VersionOrNull -Text $Right
if ($null -eq $a -or $null -eq $b) { return $null }
return $a.CompareTo($b)
}
function Get-ScriptPathFromCommandLine {
param([string]$CommandLine)
if ([string]::IsNullOrWhiteSpace($CommandLine)) { return $null }
$m = [regex]::Match($CommandLine, '(?i)-File\s+"([^"]+)"')
if ($m.Success) {
try { return [System.IO.Path]::GetFullPath($m.Groups[1].Value) } catch { return $null }
}
$m2 = [regex]::Match($CommandLine, '(?i)-File\s+(\S+)')
if ($m2.Success) {
try { return [System.IO.Path]::GetFullPath($m2.Groups[1].Value) } catch { return $null }
}
return $null
}
function Test-CommandLineIsWatchdog {
param([string]$CommandLine)
return ($CommandLine -match '(?i)(^|\s)-Watchdog(\s|$)')
}
function Stop-RdpLoginMonitorMainProcesses {
$canonical = [System.IO.Path]::GetFullPath($LocalScript)
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 $canonical) { continue }
if ([int]$proc.ProcessId -eq $PID) { continue }
Write-DeployLog "Останавливаю процесс монитора PID $($proc.ProcessId) перед обновлением файла."
Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue
}
} catch {
Write-DeployLog "Предупреждение при остановке монитора: $($_.Exception.Message)"
}
}
# --- main ---
try {
$shareRoot = Resolve-SourceShareRoot
$sourceScript = Join-Path $shareRoot $ScriptName
$sourceVersionFile = Join-Path $shareRoot $VersionFileName
Write-DeployLog "Deploy: корень дистрибутива: $shareRoot"
$settingsLocal = Join-Path $InstallRoot 'login_monitor.settings.ps1'
$settingsExampleShare = Join-Path $shareRoot 'login_monitor.settings.example.ps1'
if (-not (Test-Path -LiteralPath $settingsLocal) -and (Test-Path -LiteralPath $settingsExampleShare)) {
if (-not (Test-Path -LiteralPath $InstallRoot)) {
New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null
}
Copy-Item -LiteralPath $settingsExampleShare -Destination $settingsLocal -Force
Write-DeployLog "Создан login_monitor.settings.ps1 из example (при следующих обновлениях не перезаписывается)."
}
if (-not (Test-Path -LiteralPath $sourceScript)) {
Write-DeployLog "ОШИБКА: на шаре нет файла: $sourceScript"
exit 0
}
if (-not (Test-Path -LiteralPath $sourceVersionFile)) {
Write-DeployLog "ОШИБКА: на шаре нет version.txt: $sourceVersionFile (добавьте одну строку с версией, как в Login_Monitor.ps1)."
exit 0
}
$shareVerRaw = Read-VersionLineFromFile -Path $sourceVersionFile
if ([string]::IsNullOrWhiteSpace($shareVerRaw)) {
Write-DeployLog "ОШИБКА: version.txt пустой или не читается: $sourceVersionFile"
exit 0
}
$localVerRaw = Get-LocalDeployedVersion
Write-DeployLog "Версия на шаре: $shareVerRaw; локально установлено: $(if ($localVerRaw) { $localVerRaw } else { '(нет)' })."
$cmp = Compare-VersionStrings -Left $shareVerRaw -Right $localVerRaw
if ($null -ne $cmp) {
if ($cmp -eq 0) {
Write-DeployLog "Актуально, копирование не требуется."
exit 0
}
if ($cmp -lt 0 -and -not $AllowDowngrade) {
Write-DeployLog "На шаре версия старше локальной — пропуск (используйте -AllowDowngrade для отката)."
exit 0
}
}
if (-not (Test-Path -LiteralPath $InstallRoot)) {
if ($WhatIf) {
Write-DeployLog "[WhatIf] Создать каталог $InstallRoot"
} else {
New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null
}
}
if ($WhatIf) {
Write-DeployLog "[WhatIf] Скопировать $sourceScript -> $LocalScript; InstallTasks; версия $shareVerRaw"
exit 0
}
if (-not (Test-DeployRunningElevated)) {
Write-DeployLog "ОШИБКА: запустите Deploy из повышенной консоли PowerShell («Запуск от имени администратора»). Без этого дочерний Login_Monitor.ps1 -InstallTasks завершится с кодом 1 (нет прав на регистрацию задач)."
exit 0
}
Stop-RdpLoginMonitorMainProcesses
Copy-Item -LiteralPath $sourceScript -Destination $LocalScript -Force
Write-DeployLog "Файл скопирован: $LocalScript"
$installArgs = @(
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript, '-InstallTasks'
)
$instOut = Join-Path $InstallRoot "Logs\deploy_installtasks_stdout.log"
$instErr = Join-Path $InstallRoot "Logs\deploy_installtasks_stderr.log"
$p = Start-Process -FilePath $PsExe -ArgumentList $installArgs -Wait -PassThru -WindowStyle Hidden `
-RedirectStandardOutput $instOut -RedirectStandardError $instErr
if ($p.ExitCode -ne 0) {
Write-DeployLog "Предупреждение: InstallTasks завершился с кодом $($p.ExitCode)."
foreach ($pair in @(@($instOut, 'stdout'), @($instErr, 'stderr'))) {
$lp = $pair[0]
$lbl = $pair[1]
if (Test-Path -LiteralPath $lp) {
$tail = Get-Content -LiteralPath $lp -Tail 40 -ErrorAction SilentlyContinue
if ($tail) {
Write-DeployLog "InstallTasks $lbl (хвост): $($tail -join ' | ')"
}
}
}
Write-DeployLog "Подсказка: полный вывод в Logs\deploy_installtasks_*.log; также см. login_monitor.log за это время."
} else {
Write-DeployLog "InstallTasks выполнен (код 0)."
}
[System.IO.File]::WriteAllText($VersionStampPath, "$shareVerRaw`r`n", $Utf8Bom)
Write-DeployLog "Записана метка версии: $VersionStampPath"
$updStamp = Get-Date -Format "dd.MM.yyyy HH:mm:ss"
$updMarker = @(
"Version=$shareVerRaw"
"UpdatedAt=$updStamp"
"PendingStartupNotice=1"
) -join "`r`n"
[System.IO.File]::WriteAllText($DeployUpdateMarkerPath, "$updMarker`r`n", $Utf8Bom)
Write-DeployLog "Записана метка обновления: $DeployUpdateMarkerPath (Version=$shareVerRaw; UpdatedAt=$updStamp)."
if (-not $SkipStartMonitorAfterUpdate) {
Start-Process -FilePath $PsExe -ArgumentList @(
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript
) -WindowStyle Hidden
Write-DeployLog "Запущен процесс монитора (новый файл)."
} else {
Write-DeployLog "Запуск монитора пропущен (-SkipStartMonitorAfterUpdate); поднимется при следующей загрузке или watchdog."
}
exit 0
} catch {
Write-DeployLog "КРИТИЧЕСКАЯ ОШИБКА: $($_.Exception.Message)"
exit 0
}