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

+29 -4
View File
@@ -31,10 +31,13 @@ RTSP는 `ddagrab` 기반 Desktop Duplication API로 화면과 마우스 커서
- 기존 MediaMTX가 RTSP 포트를 사용 중이면 해당 프로세스 재사용 - 기존 MediaMTX가 RTSP 포트를 사용 중이면 해당 프로세스 재사용
- 자동 생성 MediaMTX 설정은 RTSP over TCP만 사용해 UDP RTP 포트 충돌 방지 - 자동 생성 MediaMTX 설정은 RTSP over TCP만 사용해 UDP RTP 포트 충돌 방지
- 로그인 시 자동 실행 작업 등록 및 제거 - 로그인 시 자동 실행 작업 등록 및 제거
- 기본 실행과 예약 작업 모두 PowerShell 창을 표시하지 않는 숨김 백그라운드 실행
- 절전 해제나 장시간 polling 중단 감지 시 전체 채널 강제 재구성
- 로그인 트리거와 1분 간격 무기한 복구 트리거를 이용한 Watchdog 지속 복구
- `config.ini` 기반 기능별 설정과 명령줄 override - `config.ini` 기반 기능별 설정과 명령줄 override
- 오디오 캡처/AAC 트랙을 `1` 또는 `0`으로 활성화·비활성화 - 오디오 캡처/AAC 트랙을 `1` 또는 `0`으로 활성화·비활성화
- FFmpeg와 MediaMTX 부재 시 HTTPS 다운로드, SHA-256 검증, 자동 설치 - FFmpeg와 MediaMTX 부재 시 HTTPS 다운로드, SHA-256 검증, 자동 설치
- 모니터·오디오·NIC·프로세스·설정·안전장치용 내장 시뮬레이션 30건 - 모니터·오디오·NIC·프로세스·절전 복구·설정·안전장치용 내장 시뮬레이션 35건
## RTSP URL ## RTSP URL
@@ -132,7 +135,7 @@ INI를 해석한 최종 설정값만 확인하고 종료할 수도 있습니다.
## 내장 시뮬레이션 ## 내장 시뮬레이션
실제 송출 프로세스나 예약 작업을 만들지 않고 주요 판단 로직 30건을 검사합니다. 실제 송출 프로세스나 예약 작업을 만들지 않고 주요 판단 로직 35건을 검사합니다.
```powershell ```powershell
.\screen-rtsp.ps1 -RunSimulationTests .\screen-rtsp.ps1 -RunSimulationTests
@@ -153,20 +156,38 @@ INI를 해석한 최종 설정값만 확인하고 종료할 수도 있습니다.
## 설치 및 실행 ## 설치 및 실행
관리자 PowerShell에서 실행 정책을 우회해 시작합니다. 관리자 PowerShell에서 실행 정책을 우회해 시작합니다. 기본 실행은 최종 설정값과 명령줄 override를 숨김 worker에 전달한 뒤 현재 PowerShell 프롬프트로 즉시 돌아옵니다. 현재 창을 닫아도 송출은 계속됩니다.
```powershell ```powershell
Set-Location C:\RTSP Set-Location C:\RTSP
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1
``` ```
진단 목적으로 현재 콘솔에서 직접 로그를 확인할 때만 `-Foreground`를 사용합니다. 이 경우 창을 닫거나 `Ctrl+C`를 누르면 송출도 종료됩니다.
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -Foreground
```
로그인 자동 실행은 대화형 사용자 세션에 예약 작업을 등록합니다. Desktop Duplication과 사용자 재생 오디오를 캡처해야 하므로 Windows 서비스가 아니라 로그인 트리거를 사용합니다. 로그인 자동 실행은 대화형 사용자 세션에 예약 작업을 등록합니다. Desktop Duplication과 사용자 재생 오디오를 캡처해야 하므로 Windows 서비스가 아니라 로그인 트리거를 사용합니다.
```powershell ```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -InstallStartup powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -InstallStartup
Start-ScheduledTask -TaskName "Screen RTSP Watchdog"
``` ```
등록된 작업은 `config.ini` 경로만 전달하므로 설정 파일을 수정한 뒤 작업을 다시 시작하면 변경값이 적용됩니다. 등록된 작업은 다음 복구 정책을 사용합니다.
- PowerShell 작업 인자와 프로세스 창 스타일을 모두 `Hidden`으로 지정
- 로그인 즉시 시작
- 1분마다 작업 생존을 무기한 확인하고, 실행 중이면 `IgnoreNew`로 중복 실행 방지
- 작업 실패 시 1분 간격으로 최대 999회 재시작하고, 횟수가 소진돼도 무기한 반복 트리거가 계속 복구
- 절전 중 놓친 실행은 `StartWhenAvailable`로 절전 해제 후 시작
- 배터리 전환 시에도 중지하지 않으며 실행 시간 제한 없음
Watchdog 내부에서는 Windows `PowerModeChanged/Resume` 이벤트를 감시하고, 이벤트 누락에 대비해 polling 시간 공백도 함께 확인합니다. 절전 해제가 감지되면 FFmpeg 2채널을 모두 종료하고 모니터·NIC·오디오·GPU 구성을 다시 읽은 뒤 안정화 시간 이후 재시작합니다. 모니터 분리 시에는 채널을 종료하고 계속 대기하며, 필요한 수만큼 다시 연결되면 동일하게 자동 복구합니다.
등록된 작업은 `config.ini` 경로만 전달하므로 설정 파일을 수정한 뒤 작업을 다시 시작하면 변경값이 적용됩니다. 기존 작업이 등록돼 있다면 업데이트된 복구 설정을 적용하기 위해 `-InstallStartup`을 다시 실행해야 합니다.
```powershell ```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -StartupStatus powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -StartupStatus
@@ -174,6 +195,10 @@ Stop-ScheduledTask -TaskName "Screen RTSP Watchdog"
Start-ScheduledTask -TaskName "Screen RTSP Watchdog" Start-ScheduledTask -TaskName "Screen RTSP Watchdog"
``` ```
`Stop-ScheduledTask`만 실행하면 무기한 반복 트리거가 1분 이내에 다시 시작할 수 있습니다. 완전히 중지하려면 아래의 `-UninstallStartup`을 사용합니다.
무기한 복구는 사용자가 로그인한 대화형 세션에서 예약 작업이 등록·활성화된 동안 적용됩니다. 작업 자체를 삭제하거나 비활성화한 경우와 전원이 꺼진 동안에는 실행할 수 없으며, 다음 로그인 때 다시 시작합니다.
자동 실행 제거: 자동 실행 제거:
```powershell ```powershell
+4
View File
@@ -13,7 +13,10 @@
// 슬래시 두 개로 시작하는 이 줄도 설명으로 처리됩니다. // 슬래시 두 개로 시작하는 이 줄도 설명으로 처리됩니다.
# #
# 실행 # 실행
# 기본 실행은 즉시 숨김 백그라운드 worker를 시작하고 현재 창으로 돌아옵니다.
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 # powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1
# 진단 중 콘솔 로그를 직접 보려는 경우에만 -Foreground를 사용합니다.
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -Foreground
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -ShowConfiguration # powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -ShowConfiguration
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -InstallDependenciesOnly # powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -InstallDependenciesOnly
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -RunSimulationTests # powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -RunSimulationTests
@@ -25,6 +28,7 @@
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -InstallStartup # powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -InstallStartup
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -StartupStatus # powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -StartupStatus
# Start-ScheduledTask -TaskName "Screen RTSP Watchdog" # Start-ScheduledTask -TaskName "Screen RTSP Watchdog"
# Stop-ScheduledTask는 1분 이내 자동 복구되므로 완전 중지는 -UninstallStartup을 사용합니다.
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -UninstallStartup # powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -UninstallStartup
# #
# GPU와 FFmpeg 지원 확인 # GPU와 FFmpeg 지원 확인
+172 -9
View File
@@ -54,6 +54,10 @@ param(
[switch]$ShowConfiguration, [switch]$ShowConfiguration,
[switch]$InstallDependenciesOnly, [switch]$InstallDependenciesOnly,
[switch]$RunSimulationTests, [switch]$RunSimulationTests,
[switch]$Foreground,
[Parameter(DontShow = $true)]
[switch]$WatchdogWorker,
[Parameter(DontShow = $true)] [Parameter(DontShow = $true)]
[switch]$AudioWorker, [switch]$AudioWorker,
@@ -833,24 +837,74 @@ function Join-ProcessArguments {
function Get-StartupArgumentList { function Get-StartupArgumentList {
$values = @( $values = @(
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $PSCommandPath, "-NoProfile", "-WindowStyle", "Hidden", "-ExecutionPolicy", "Bypass",
"-ConfigPath", $ConfigPath "-File", $PSCommandPath, "-ConfigPath", $ConfigPath, "-WatchdogWorker"
) )
return Join-ProcessArguments $values 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 { function Install-StartupTask {
$action = New-ScheduledTaskAction -Execute $PowerShellPath -Argument (Get-StartupArgumentList) ` $action = New-ScheduledTaskAction -Execute $PowerShellPath -Argument (Get-StartupArgumentList) `
-WorkingDirectory $BaseDir -WorkingDirectory $BaseDir
$trigger = New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\$env:USERNAME" $triggers = @(Get-StartupTriggers)
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" ` $principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" `
-LogonType Interactive -RunLevel Highest -LogonType Interactive -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries ` $settings = Get-StartupSettings
-DontStopIfGoingOnBatteries -RestartCount 10 -RestartInterval (New-TimeSpan -Minutes 1) ` Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $triggers `
-ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
-Principal $principal -Settings $settings -Force | Out-Null -Principal $principal -Settings $settings -Force | Out-Null
Write-Host "자동 실행 작업을 등록했습니다: $TaskName" Write-Host "자동 실행 작업을 등록했습니다: $TaskName"
Write-Host "숨김 실행: 사용"
Write-Host "복구: 로그인 시 시작 + 1분 간격 무기한 확인 + 실패 시 재시작"
Write-Host "설정 파일: $ConfigPath" Write-Host "설정 파일: $ConfigPath"
} }
@@ -905,6 +959,15 @@ if ($StartupStatus) {
NextRunTime = $taskInfo.NextRunTime NextRunTime = $taskInfo.NextRunTime
Execute = $task.Actions.Execute Execute = $task.Actions.Execute
Arguments = $task.Actions.Arguments 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 } | Format-List
exit 0 exit 0
} }
@@ -1759,6 +1822,29 @@ function Get-StreamingReadinessReason {
return $null 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 { function Invoke-SimulationTests {
$results = New-Object 'System.Collections.Generic.List[object]' $results = New-Object 'System.Collections.Generic.List[object]'
@@ -1987,9 +2073,50 @@ function Invoke-SimulationTests {
(Get-EncoderDefinition "Intel").Name -eq "h264_qsv") (Get-EncoderDefinition "Intel").Name -eq "h264_qsv")
} }
Invoke-SimulationCase "자동 실행은 config.ini 경로만 전달" { Invoke-SimulationCase "자동 실행은 숨김 worker와 config.ini만 전달" {
$arguments = Get-StartupArgumentList $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가 모두 생존" { Invoke-SimulationCase "2채널 FFmpeg/오디오 worker가 모두 생존" {
@@ -2066,13 +2193,24 @@ if ($RunSimulationTests) {
exit 0 exit 0
} }
if (-not $WatchdogWorker -and -not $Foreground) {
Start-HiddenWatchdog
exit 0
}
$mutex = New-Object System.Threading.Mutex($false, "Local\ScreenRtspWatchdog") $mutex = New-Object System.Threading.Mutex($false, "Local\ScreenRtspWatchdog")
$hasMutex = $false $hasMutex = $false
$powerEventSourceIdentifier = "ScreenRtspPowerMode-$PID"
$powerEventRegistered = $false
try { try {
$hasMutex = $mutex.WaitOne(0, $false) $hasMutex = $mutex.WaitOne(0, $false)
if (-not $hasMutex) { throw "Screen RTSP Watchdog가 이미 실행 중입니다." } if (-not $hasMutex) { throw "Screen RTSP Watchdog가 이미 실행 중입니다." }
Register-ObjectEvent -InputObject ([Microsoft.Win32.SystemEvents]) `
-EventName PowerModeChanged -SourceIdentifier $powerEventSourceIdentifier | Out-Null
$powerEventRegistered = $true
Assert-RequiredFiles Assert-RequiredFiles
Ensure-MediaMTXConfig Ensure-MediaMTXConfig
$encoderList = Test-FFmpegCapabilities $encoderList = Test-FFmpegCapabilities
@@ -2092,9 +2230,28 @@ try {
$healthySince = $null $healthySince = $null
$lastReason = "" $lastReason = ""
$lastGpuCheck = [DateTime]::MinValue $lastGpuCheck = [DateTime]::MinValue
$lastPollAt = Get-Date
while ($true) { while ($true) {
try { 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 $mediaReady = Start-MediaMTX
if (-not $mediaReady) { if (-not $mediaReady) {
Stop-Streams Stop-Streams
@@ -2189,6 +2346,12 @@ catch {
} }
finally { finally {
Stop-Streams 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)) { if ($OwnsMediaMTX -and (Test-ProcessRunning $MediaMTXProcess)) {
Stop-Process -Id $MediaMTXProcess.Id -Force -ErrorAction SilentlyContinue Stop-Process -Id $MediaMTXProcess.Id -Force -ErrorAction SilentlyContinue
} }