재부팅 뒤 워치독이 5분마다 콘솔 창에 실패만 뿌리던 문제의 원인 세 가지를 고친다. - 워치독이 DB 다운을 감지하고 직접 복구한다. postgres(vignette-dev-db) 기동은 boot 담당이라 start-public-runtime.ps1 재호출로는 절대 복구되지 않았고, 그 결과 워치독은 고칠 수 없는 대상에 start를 무한 재시도하며 실패만 기록했다. db를 health check 항목에 넣고, 재시작 전에 컨테이너를 되살리며, 복구 실패 시에는 runtime 재시작을 시도하지 않고 종료한다. - 작업 액션을 wscript 런처(watch-public-runtime-task.vbs) 경유로 등록한다. powershell.exe를 직접 등록하면 -WindowStyle Hidden이어도 conhost 창이 매 실행 번쩍이고, 5분 주기에서는 그것이 곧 화면을 가리는 창이 된다. 런처는 pin 인자를 해석하지 않고 전달만 하며 provenance 검증은 기존대로 watchdog이 수행한다. - boot이 web preview 상태를 보고 -SkipWebRestart를 조건부로 붙인다. 무조건 스킵하면 재부팅 직후처럼 vite가 죽은 상태에서 boot 경로로는 web이 영영 복구되지 않았다. hidden trigger는 액션이 wscript 런처를 거치는지 함께 검증하도록 맞췄다.
164 lines
6.2 KiB
PowerShell
164 lines
6.2 KiB
PowerShell
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$StableSourceRoot,
|
|
[string]$TaskName = "VignettePublicRuntimeWatchdog",
|
|
[int]$IntervalMinutes = 5,
|
|
[string[]]$AdditionalPublicHealthUrls = @(),
|
|
[switch]$SkipPublicHealth,
|
|
[switch]$SkipCloudflaredRestart,
|
|
[switch]$RunNow
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
if ($IntervalMinutes -lt 1) {
|
|
throw "IntervalMinutes must be 1 or greater"
|
|
}
|
|
|
|
$resolvedSourceRoot = (Resolve-Path -LiteralPath $StableSourceRoot).Path
|
|
$installerScript = Join-Path $resolvedSourceRoot "scripts\install-public-runtime-task.ps1"
|
|
$watchScript = Join-Path $resolvedSourceRoot "scripts\watch-public-runtime.ps1"
|
|
$startScript = Join-Path $resolvedSourceRoot "scripts\start-public-runtime.ps1"
|
|
$voiceSidecarProbe = Join-Path $resolvedSourceRoot "scripts\probe-public-voice-sidecars.py"
|
|
$taskLauncher = Join-Path $resolvedSourceRoot "scripts\watch-public-runtime-task.vbs"
|
|
|
|
function Invoke-GitText {
|
|
param([string[]]$Arguments)
|
|
|
|
$value = & git.exe -C $resolvedSourceRoot @Arguments
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Stable source Git command failed (exit=$LASTEXITCODE): git $($Arguments -join ' ')"
|
|
}
|
|
return (@($value) -join [Environment]::NewLine).Trim()
|
|
}
|
|
|
|
foreach ($requiredScript in @($installerScript, $watchScript, $startScript, $voiceSidecarProbe, $taskLauncher)) {
|
|
if (!(Test-Path -LiteralPath $requiredScript -PathType Leaf)) {
|
|
throw "Public runtime script not found at $requiredScript"
|
|
}
|
|
}
|
|
|
|
$runningInstaller = (Resolve-Path -LiteralPath $PSCommandPath).Path
|
|
if (-not [string]::Equals(
|
|
$runningInstaller,
|
|
(Resolve-Path -LiteralPath $installerScript).Path,
|
|
[System.StringComparison]::OrdinalIgnoreCase
|
|
)) {
|
|
throw "Watchdog installer is not executing from the pinned stable source root"
|
|
}
|
|
|
|
$gitRoot = Invoke-GitText -Arguments @("rev-parse", "--show-toplevel")
|
|
$resolvedGitRoot = (Resolve-Path -LiteralPath $gitRoot).Path
|
|
if (-not [string]::Equals(
|
|
$resolvedGitRoot,
|
|
$resolvedSourceRoot,
|
|
[System.StringComparison]::OrdinalIgnoreCase
|
|
)) {
|
|
throw "Stable source root does not match its Git toplevel"
|
|
}
|
|
|
|
$symbolicHead = & git.exe -C $resolvedSourceRoot symbolic-ref --quiet HEAD
|
|
$symbolicHeadExit = $LASTEXITCODE
|
|
if ($symbolicHeadExit -eq 0) {
|
|
throw "Stable source must be a detached HEAD, not branch $symbolicHead"
|
|
}
|
|
if ($symbolicHeadExit -ne 1) {
|
|
throw "Could not prove detached HEAD (git exit=$symbolicHeadExit)"
|
|
}
|
|
|
|
$dirty = Invoke-GitText -Arguments @("status", "--porcelain=v1", "--untracked-files=normal")
|
|
if ($dirty) {
|
|
throw "Stable source is not clean; refusing watchdog installation"
|
|
}
|
|
foreach ($relativePath in @(
|
|
"scripts/install-public-runtime-task.ps1",
|
|
"scripts/watch-public-runtime.ps1",
|
|
"scripts/start-public-runtime.ps1",
|
|
"scripts/probe-public-voice-sidecars.py",
|
|
"scripts/watch-public-runtime-task.vbs"
|
|
)) {
|
|
Invoke-GitText -Arguments @("ls-files", "--error-unmatch", "--", $relativePath) | Out-Null
|
|
}
|
|
|
|
$sourceCommit = Invoke-GitText -Arguments @("rev-parse", "--verify", "HEAD")
|
|
$sourceTree = Invoke-GitText -Arguments @("rev-parse", "--verify", "HEAD^{tree}")
|
|
$watchdogSha256 = (Get-FileHash -LiteralPath $watchScript -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
$startScriptSha256 = (Get-FileHash -LiteralPath $startScript -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
|
|
$wscript = Join-Path $env:SystemRoot "System32\wscript.exe"
|
|
if (!(Test-Path -LiteralPath $wscript -PathType Leaf)) {
|
|
throw "wscript.exe not found at $wscript"
|
|
}
|
|
$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
|
|
# powershell.exe 를 액션으로 직접 등록하면 -WindowStyle Hidden 이어도 conhost 창이
|
|
# 매 실행 번쩍인다. 5분 주기 워치독에서는 그게 곧 "5분마다 뜨는 창"이 된다.
|
|
# wscript 런처를 거쳐 Run(cmd, 0) 으로 띄우면 창이 생성되지 않는다.
|
|
$actionArguments = @(
|
|
"-File `"$watchScript`"",
|
|
"-StableSourceRoot `"$resolvedSourceRoot`"",
|
|
"-ExpectedSourceCommit $sourceCommit",
|
|
"-ExpectedSourceTree $sourceTree",
|
|
"-ExpectedWatchdogSha256 $watchdogSha256",
|
|
"-ExpectedStartScriptSha256 $startScriptSha256"
|
|
)
|
|
if ($SkipPublicHealth) {
|
|
$actionArguments += "-SkipPublicHealth"
|
|
}
|
|
if ($SkipCloudflaredRestart) {
|
|
$actionArguments += "-SkipCloudflaredRestart"
|
|
}
|
|
if ($AdditionalPublicHealthUrls.Count -gt 0) {
|
|
$escapedUrls = $AdditionalPublicHealthUrls | ForEach-Object { "`"$_`"" }
|
|
$actionArguments += "-AdditionalPublicHealthUrls $($escapedUrls -join ',')"
|
|
}
|
|
|
|
$action = New-ScheduledTaskAction `
|
|
-Execute $wscript `
|
|
-Argument ("`"$taskLauncher`" " + ($actionArguments -join " ")) `
|
|
-WorkingDirectory $resolvedSourceRoot
|
|
|
|
$logonTrigger = New-ScheduledTaskTrigger -AtLogOn -User $userId
|
|
$repeatTrigger = New-ScheduledTaskTrigger `
|
|
-Once `
|
|
-At (Get-Date).AddMinutes(1) `
|
|
-RepetitionInterval (New-TimeSpan -Minutes $IntervalMinutes)
|
|
|
|
$settings = New-ScheduledTaskSettingsSet `
|
|
-AllowStartIfOnBatteries `
|
|
-DontStopIfGoingOnBatteries `
|
|
-ExecutionTimeLimit (New-TimeSpan -Minutes 10) `
|
|
-MultipleInstances IgnoreNew `
|
|
-RestartCount 3 `
|
|
-RestartInterval (New-TimeSpan -Minutes 1) `
|
|
-StartWhenAvailable `
|
|
-WakeToRun
|
|
|
|
$principal = New-ScheduledTaskPrincipal `
|
|
-UserId $userId `
|
|
-LogonType Interactive `
|
|
-RunLevel Limited
|
|
|
|
$description = "Runs Vignette public runtime watchdog as $userId from detached clean commit $sourceCommit. Secrets stay in the user profile and apps/api/.env; the task command stores no secrets."
|
|
$task = New-ScheduledTask `
|
|
-Action $action `
|
|
-Trigger @($logonTrigger, $repeatTrigger) `
|
|
-Settings $settings `
|
|
-Principal $principal `
|
|
-Description $description
|
|
|
|
Register-ScheduledTask -TaskName $TaskName -InputObject $task -Force | Out-Null
|
|
|
|
Write-Output "Installed scheduled task '$TaskName' for $userId"
|
|
Write-Output "Action: $wscript `"$taskLauncher`" $($actionArguments -join ' ')"
|
|
Write-Output "Pinned source: root=$resolvedSourceRoot commit=$sourceCommit tree=$sourceTree"
|
|
Write-Output "Pinned scripts: watchdog_sha256=$watchdogSha256 start_sha256=$startScriptSha256"
|
|
Write-Output "Interval: every $IntervalMinutes minute(s), plus at user logon"
|
|
if ($AdditionalPublicHealthUrls.Count -gt 0) {
|
|
Write-Output "Additional public health URLs: $($AdditionalPublicHealthUrls -join ', ')"
|
|
}
|
|
|
|
if ($RunNow) {
|
|
Start-ScheduledTask -TaskName $TaskName
|
|
Write-Output "Started scheduled task '$TaskName'"
|
|
}
|