공개 런타임 워치독 창·복구 결함 3건 수정

재부팅 뒤 워치독이 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 런처를 거치는지 함께 검증하도록 맞췄다.
This commit is contained in:
Yun Chan 2026-08-12 21:17:04 +09:00
parent 80bcacc723
commit 5cb530e153
7 changed files with 184 additions and 28 deletions

View file

@ -18,6 +18,8 @@
[int]$EnginePort = 9099,
[int]$WhisperPort = 9882,
[int]$MeloTtsPort = 9883,
[int]$DbPort = 55432,
[string]$DbContainer = "vignette-dev-db",
[string]$Python = "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe",
[string]$Cloudflared = "$env:LOCALAPPDATA\Microsoft\WinGet\Links\cloudflared.exe",
[string]$CloudflaredConfig = "$env:USERPROFILE\.cloudflared\vignette-config.yml",
@ -285,6 +287,48 @@ function Test-CloudflaredProcess {
}
}
function Test-DatabaseListening {
$ok = $false
try {
$client = New-Object System.Net.Sockets.TcpClient
$async = $client.BeginConnect("127.0.0.1", $DbPort, $null, $null)
$ok = $async.AsyncWaitHandle.WaitOne(3000) -and $client.Connected
$client.Close()
} catch {
$ok = $false
}
[pscustomobject]@{
Name = "db"
Ok = $ok
Detail = if ($ok) { "127.0.0.1:$DbPort" } else { "not listening on 127.0.0.1:$DbPort" }
}
}
# postgres(docker: vignette-dev-db)는 start-public-runtime.ps1의 복구 범위 밖이다(boot 담당).
# DB가 내려간 채로 start만 반복 호출하면 API가 매번 startup에서 죽어 워치독은 영원히
# 실패만 기록한다 — 2026-08-12 PC 비정상 종료 후 5분마다 실패 창이 뜬 사건의 구조다.
# 재시작 전에 컨테이너를 직접 되살린다.
function Restore-DatabaseContainer {
try {
$output = & docker.exe start $DbContainer 2>&1
$output | ForEach-Object { Write-WatchdogLog " docker> $_" }
if ($LASTEXITCODE -ne 0) {
Write-WatchdogLog "db restore: docker start exit=$LASTEXITCODE"
return $false
}
} catch {
Write-WatchdogLog "db restore failed: $($_.Exception.Message)"
return $false
}
for ($i = 1; $i -le 30; $i++) {
if ((Test-DatabaseListening).Ok) { return $true }
Start-Sleep -Seconds 2
}
return $false
}
# engine 판정은 /health(프로세스 liveness)가 아니라 /ready(실제 claude -p 생성)로 한다.
# /health는 ok:true만 보므로 "프로세스는 살아 있고 그 프로세스의 claude 세션만 죽은"
# 상태를 통과시킨다(2026-08-07 공개 런타임: engine=false인데 워치독 lastResult=0).
@ -312,7 +356,8 @@ $checks = @(
-Uri "http://127.0.0.1:$WebPort/" `
-IsHealthy { param($body) $true }),
(Test-VoiceSidecarStack),
(Test-CloudflaredProcess)
(Test-CloudflaredProcess),
(Test-DatabaseListening)
)
if (!$SkipPublicHealth) {
@ -352,6 +397,19 @@ if ($failCount -lt $FailuresBeforeRestart) {
exit 1
}
# DB가 죽어 있으면 start-public-runtime.ps1으로는 절대 복구되지 않는다(DB 기동은 boot 담당).
# 먼저 되살리고, 그래도 안 되면 재시작을 아예 시도하지 않는다 — 고칠 수 없는 대상에
# start를 반복 호출하는 것이 5분 주기 실패 로그의 정체였다.
if (-not (@($checks | Where-Object { $_.Name -eq "db" })[0]).Ok) {
Write-WatchdogLog "db down; restoring container '$DbContainer' before runtime restart"
if (Restore-DatabaseContainer) {
Write-WatchdogLog "db restored: 127.0.0.1:$DbPort"
} else {
Write-WatchdogLog "ERROR: db restore failed; skipping runtime restart (start script cannot fix a missing DB)"
exit 1
}
}
$startArgs = @{
Workspace = $resolvedSourceRoot
ApiPort = $ApiPort