Harden hidden startup and sleep recovery

This commit is contained in:
seo-amugae committed 2026-10-08 01:14:17 +09:00
1 parent 4088f2299d
commit 2819d31490
3 files changed
+205 -13

No files matched your search

+172 -9
View File
@@ -54,6 +54,10 @@ param(
[switch]$ShowConfiguration,
[switch]$InstallDependenciesOnly,
[switch]$RunSimulationTests,
[switch]$Foreground,
[Parameter(DontShow = $true)]
[switch]$WatchdogWorker,
[Parameter(DontShow = $true)]
[switch]$AudioWorker,
@@ -833,24 +837,74 @@ function Join-ProcessArguments {
function Get-StartupArgumentList {
$values = @(
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $PSCommandPath,
"-ConfigPath", $ConfigPath
"-NoProfile", "-WindowStyle", "Hidden", "-ExecutionPolicy", "Bypass",
"-File", $PSCommandPath, "-ConfigPath", $ConfigPath, "-WatchdogWorker"
)
return Join-ProcessArguments $values
}
function Get-HiddenWorkerArgumentList {
$values = @(
"-NoProfile", "-WindowStyle", "Hidden", "-ExecutionPolicy", "Bypass",
"-File", $PSCommandPath, "-ConfigPath", $ConfigPath, "-WatchdogWorker",
"-EncoderPriorityCsv", ($EncoderPriority -join ","),
"-ChannelCount", [string]$ChannelCount,
"-FrameRate", [string]$FrameRate,
"-VideoBitrate", $VideoBitrate,
"-AudioBitrate", $AudioBitrate,
"-AudioEnabled", [string]$AudioEnabled,
"-AudioSignalThreshold", $AudioSignalThreshold.ToString(
[System.Globalization.CultureInfo]::InvariantCulture
),
"-NicDescription", $NicDescription,
"-HealthTarget", $HealthTarget,
"-StableTimeSec", [string]$StableTimeSec,
"-PollIntervalSec", [string]$PollIntervalSec,
"-RtspPort", [string]$RtspPort,
"-AutoInstallDependencies", [string]$AutoInstallDependencies,
"-FFmpegDownloadUrl", $FFmpegDownloadUrl,
"-FFmpegSha256Url", $FFmpegSha256Url,
"-MediaMTXDownloadUrl", $MediaMTXDownloadUrl,
"-MediaMTXSha256", $MediaMTXSha256
)
return Join-ProcessArguments $values
}
function Get-StartupTriggers {
$logonTrigger = New-ScheduledTaskTrigger -AtLogOn `
-User "$env:USERDOMAIN\$env:USERNAME"
$recoveryTrigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(1)) `
-RepetitionInterval (New-TimeSpan -Minutes 1)
return @($logonTrigger, $recoveryTrigger)
}
function Get-StartupSettings {
return New-ScheduledTaskSettingsSet -Hidden -StartWhenAvailable `
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) `
-ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew
}
function Start-HiddenWatchdog {
$process = Start-Process -FilePath $PowerShellPath `
-ArgumentList (Get-HiddenWorkerArgumentList) `
-WorkingDirectory $BaseDir -WindowStyle Hidden -PassThru
Write-Host "숨김 백그라운드 Watchdog를 시작했습니다. PID=$($process.Id)"
Write-Host "로그: $WatchdogLog"
}
function Install-StartupTask {
$action = New-ScheduledTaskAction -Execute $PowerShellPath -Argument (Get-StartupArgumentList) `
-WorkingDirectory $BaseDir
$trigger = New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\$env:USERNAME"
$triggers = @(Get-StartupTriggers)
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" `
-LogonType Interactive -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries -RestartCount 10 -RestartInterval (New-TimeSpan -Minutes 1) `
-ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
$settings = Get-StartupSettings
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $triggers `
-Principal $principal -Settings $settings -Force | Out-Null
Write-Host "자동 실행 작업을 등록했습니다: $TaskName"
Write-Host "숨김 실행: 사용"
Write-Host "복구: 로그인 시 시작 + 1분 간격 무기한 확인 + 실패 시 재시작"
Write-Host "설정 파일: $ConfigPath"
}
@@ -905,6 +959,15 @@ if ($StartupStatus) {
NextRunTime = $taskInfo.NextRunTime
Execute = $task.Actions.Execute
Arguments = $task.Actions.Arguments
Hidden = $task.Settings.Hidden
StartWhenAvailable = $task.Settings.StartWhenAvailable
RestartCount = $task.Settings.RestartCount
RestartInterval = $task.Settings.RestartInterval
MultipleInstances = $task.Settings.MultipleInstances
TriggerCount = @($task.Triggers).Count
RecoveryInterval = (@($task.Triggers | Where-Object {
$_.Repetition -and $_.Repetition.Interval
} | ForEach-Object { $_.Repetition.Interval }) -join ", ")
} | Format-List
exit 0
}
@@ -1759,6 +1822,29 @@ function Get-StreamingReadinessReason {
return $null
}
function Test-WatchdogResumeGap {
param(
[DateTime]$PreviousPollAt,
[DateTime]$CurrentPollAt
)
$resumeThresholdSec = [Math]::Max(10.0, [double]$PollIntervalSec * 3.0)
return (($CurrentPollAt - $PreviousPollAt).TotalSeconds -ge $resumeThresholdSec)
}
function Receive-WatchdogPowerResume {
param([string]$SourceIdentifier)
$resumeDetected = $false
foreach ($powerEvent in @(Get-Event -SourceIdentifier $SourceIdentifier `
-ErrorAction SilentlyContinue)) {
if ([string]$powerEvent.SourceEventArgs.Mode -eq "Resume") {
$resumeDetected = $true
}
Remove-Event -EventIdentifier $powerEvent.EventIdentifier -ErrorAction SilentlyContinue
}
return $resumeDetected
}
function Invoke-SimulationTests {
$results = New-Object 'System.Collections.Generic.List[object]'
@@ -1987,9 +2073,50 @@ function Invoke-SimulationTests {
(Get-EncoderDefinition "Intel").Name -eq "h264_qsv")
}
Invoke-SimulationCase "자동 실행은 config.ini 경로만 전달" {
Invoke-SimulationCase "자동 실행은 숨김 worker와 config.ini만 전달" {
$arguments = Get-StartupArgumentList
return ($arguments -match '-ConfigPath' -and $arguments -notmatch '-VideoBitrate')
return ($arguments -match '-WindowStyle\s+Hidden' -and
$arguments -match '-WatchdogWorker' -and
$arguments -match '-ConfigPath' -and
$arguments -notmatch '-VideoBitrate')
}
Invoke-SimulationCase "직접 숨김 실행은 최종 설정과 override를 전달" {
$arguments = Get-HiddenWorkerArgumentList
return ($arguments -match '-EncoderPriorityCsv' -and
$arguments -match '-ChannelCount\s+2' -and
$arguments -match '-AudioEnabled\s+[01]' -and
$arguments -match '-RtspPort\s+8554' -and
$arguments -match '-WatchdogWorker')
}
Invoke-SimulationCase "예약 작업은 1분 간격으로 무기한 복구 확인" {
$triggers = @(Get-StartupTriggers)
$recoveryTrigger = $triggers | Where-Object {
$_.Repetition -and $_.Repetition.Interval
} | Select-Object -First 1
return ($triggers.Count -eq 2 -and
$recoveryTrigger.Repetition.Interval -eq "PT1M" -and
[string]::IsNullOrWhiteSpace($recoveryTrigger.Repetition.Duration))
}
Invoke-SimulationCase "예약 작업 숨김/누락 실행/재시작 설정" {
$settings = Get-StartupSettings
return ($settings.Hidden -and $settings.StartWhenAvailable -and
$settings.RestartCount -eq 999 -and
$settings.RestartInterval -eq "PT1M" -and
$settings.ExecutionTimeLimit -eq "PT0S" -and
$settings.MultipleInstances -eq "IgnoreNew")
}
Invoke-SimulationCase "정상 polling 간격은 절전 복구로 오인하지 않음" {
$previous = [DateTime]::UtcNow
return (-not (Test-WatchdogResumeGap $previous $previous.AddSeconds(2)))
}
Invoke-SimulationCase "긴 polling 공백을 절전 해제로 감지" {
$previous = [DateTime]::UtcNow
return (Test-WatchdogResumeGap $previous $previous.AddMinutes(5))
}
Invoke-SimulationCase "2채널 FFmpeg/오디오 worker가 모두 생존" {
@@ -2066,13 +2193,24 @@ if ($RunSimulationTests) {
exit 0
}
if (-not $WatchdogWorker -and -not $Foreground) {
Start-HiddenWatchdog
exit 0
}
$mutex = New-Object System.Threading.Mutex($false, "Local\ScreenRtspWatchdog")
$hasMutex = $false
$powerEventSourceIdentifier = "ScreenRtspPowerMode-$PID"
$powerEventRegistered = $false
try {
$hasMutex = $mutex.WaitOne(0, $false)
if (-not $hasMutex) { throw "Screen RTSP Watchdog가 이미 실행 중입니다." }
Register-ObjectEvent -InputObject ([Microsoft.Win32.SystemEvents]) `
-EventName PowerModeChanged -SourceIdentifier $powerEventSourceIdentifier | Out-Null
$powerEventRegistered = $true
Assert-RequiredFiles
Ensure-MediaMTXConfig
$encoderList = Test-FFmpegCapabilities
@@ -2092,9 +2230,28 @@ try {
$healthySince = $null
$lastReason = ""
$lastGpuCheck = [DateTime]::MinValue
$lastPollAt = Get-Date
while ($true) {
try {
$currentPollAt = Get-Date
$powerResumeDetected = Receive-WatchdogPowerResume `
-SourceIdentifier $powerEventSourceIdentifier
$resumeGapDetected = Test-WatchdogResumeGap `
-PreviousPollAt $lastPollAt -CurrentPollAt $currentPollAt
if ($powerResumeDetected -or $resumeGapDetected) {
$pollGapSec = [Math]::Round(($currentPollAt - $lastPollAt).TotalSeconds, 1)
$resumeSource = if ($powerResumeDetected) { "Windows 절전 해제 이벤트" } `
else { "polling 공백 ${pollGapSec}초" }
Write-Log "$resumeSource 감지 - 전체 채널 재구성"
Stop-Streams
$lastSystemSignature = ""
$healthySince = $null
$lastReason = ""
$lastGpuCheck = [DateTime]::MinValue
}
$lastPollAt = $currentPollAt
$mediaReady = Start-MediaMTX
if (-not $mediaReady) {
Stop-Streams
@@ -2189,6 +2346,12 @@ catch {
}
finally {
Stop-Streams
if ($powerEventRegistered) {
Unregister-Event -SourceIdentifier $powerEventSourceIdentifier `
-ErrorAction SilentlyContinue
Get-Event -SourceIdentifier $powerEventSourceIdentifier -ErrorAction SilentlyContinue |
Remove-Event -ErrorAction SilentlyContinue
}
if ($OwnsMediaMTX -and (Test-ProcessRunning $MediaMTXProcess)) {
Stop-Process -Id $MediaMTXProcess.Id -Force -ErrorAction SilentlyContinue
}