deploy/windows/run.ps1
Rama master · réplica de lectura del repositorio original.
# WindsurfAPI 监督循环内核。
# 阻塞式跑 `node src/index.js`,按退出码路由:75/0 重启,其它退避后停。
# 由 start.ps1(前台)、run.bat(前台)、run.vbs(隐藏后台)共用。
#
# ⚠️ 退出码语义与 KiroStudio 相反(见 README):
# 75 = 自更新/面板 Update 请求重启(EX_TEMPFAIL, api.js:2055)
# 0 = 优雅停机(index.js exit(0),已抽干 SSE;K7 起关机只 drain 不回写池,
# 盘上池态由 30s dirty-flush 保持最新)
# 其它非零 = 崩溃/启动错/端口占用重试耗尽(index.js/server.js exit(1))
#
# 关键红线:阻塞等子进程完全退出再重启,杜绝两个 node 抢写 accounts.json。
# 绝不用 taskkill /F 来「重启」(那是外部写者红线;监督者靠等子退出即零写重叠)。
$ErrorActionPreference = 'Stop'
# 控制台按 UTF-8 输出,否则中文文案在默认 GBK/936 码页的窗口里显示为乱码。
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { }
# 项目根 = 本脚本上两级(deploy/windows/ → 根)。config.js 按模块路径解析
# .env/数据文件(ROOT=resolve(__dirname,'..')),cwd 必须是根以保 git/自更新正确。
$Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
Set-Location $Root
$LogDir = Join-Path $Root 'logs'
if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir -Force | Out-Null }
$PidFile = Join-Path $LogDir 'windsurfapi.pid'
$Date = Get-Date -Format 'yyyy-MM-dd'
$SupLog = Join-Path $LogDir "supervisor-$Date.log"
# DEVIN_CONNECT 必需(language_server 仅 Linux/macOS)。仅在未设时补默认,
# 不覆盖用户显式值(config.js:27 环境变量优先于 .env)。
if (-not $env:DEVIN_CONNECT -and -not $env:DEVIN_ONLY) { $env:DEVIN_CONNECT = '1' }
if (-not $env:LOG_LEVEL) { $env:LOG_LEVEL = 'info' }
function Write-Sup([string]$msg) {
$line = "[{0}] {1}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $msg
Write-Host $line
Add-Content -Path $SupLog -Value $line -Encoding UTF8
}
$MaxCrashLoop = 5
$crashStreak = 0
Write-Sup "supervisor start (root=$Root)"
while ($true) {
# 原始 stdout/stderr 必须落盘:app 的 JSONL 日志不含启动 banner(index.js:78)
# 与 pre-logger 致命 console.error(index.js:214)。给子进程单独的 node 日志,
# 与监督者自己的 Write-Sup 记录分离(否则同一文件被 stdout 重定向截断,监督
# 记录会被 app 输出覆盖 —— 老版 Start-Process 方案的隐患)。
$nodeOut = Join-Path $LogDir "node-$Date.log"
# 关键:用 [Diagnostics.Process] 而非 Start-Process -PassThru。后者在
# -RedirectStandardOutput 到文件时,$proc.ExitCode 会返回 $null(Windows
# PowerShell 已知缺陷),导致 75/0 全部落入 else 崩溃分支 —— 热更新与优雅
# 重启彻底失效。.NET Process 的 ExitCode 可靠,且能拿到 PID 写 pidfile
# (stop.ps1/status.ps1 依赖)。异步抽干 stdout/stderr 到 node 日志,避免
# 管道缓冲写满导致子进程阻塞(经典死锁),同时满足"等子退出再重启"红线。
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = 'node'
$psi.Arguments = 'src/index.js'
$psi.WorkingDirectory = $Root
$psi.UseShellExecute = $false
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$proc = New-Object System.Diagnostics.Process
$proc.StartInfo = $psi
$sink = { if ($EventArgs.Data -ne $null) { Add-Content -Path $Event.MessageData -Value $EventArgs.Data -Encoding UTF8 } }
$so = Register-ObjectEvent -InputObject $proc -EventName OutputDataReceived -Action $sink -MessageData $nodeOut
$se = Register-ObjectEvent -InputObject $proc -EventName ErrorDataReceived -Action $sink -MessageData $nodeOut
[void]$proc.Start()
$proc.BeginOutputReadLine()
$proc.BeginErrorReadLine()
Set-Content -Path $PidFile -Value $proc.Id -Encoding ASCII
Write-Sup "node started pid=$($proc.Id)"
$proc.WaitForExit()
$code = $proc.ExitCode
# 注销事件订阅,防止跨循环泄漏。
Unregister-Event -SourceIdentifier $so.Name -ErrorAction SilentlyContinue
Unregister-Event -SourceIdentifier $se.Name -ErrorAction SilentlyContinue
$proc.Dispose()
Write-Sup "node exited code=$code"
if ($code -eq 75) {
# 自更新/面板 Update:重启,重置崩溃计数。
$crashStreak = 0
Write-Sup 'exit 75 (self-update/restart requested) -> restarting'
Start-Sleep -Milliseconds 1000
continue
}
elseif ($code -eq 0) {
# 优雅停机。真实 Ctrl-C/关窗会同时终结本监督者,走不到重拉;这段可打断的
# 等待只是兜底(例如程序内部主动 exit(0))。
$crashStreak = 0
Write-Sup 'exit 0 (graceful) -> restarting after 2s (Ctrl-C to stop)'
Start-Sleep -Seconds 2
continue
}
else {
# 崩溃/启动错/端口占用重试耗尽。退避,连续 MaxCrashLoop 次后停机。
$crashStreak++
if ($crashStreak -ge $MaxCrashLoop) {
Write-Sup "CRASH LOOP: $crashStreak consecutive non-zero exits (last=$code). Stopping."
Write-Host ''
Write-Host '============================================================' -ForegroundColor Red
Write-Host " WindsurfAPI 连续 $crashStreak 次异常退出,监督循环已停止。" -ForegroundColor Red
Write-Host " 最近日志:$SupLog" -ForegroundColor Red
Write-Host '============================================================' -ForegroundColor Red
if (Test-Path $outLog) { Write-Host '--- 日志尾 ---'; Get-Content $outLog -Tail 30 }
break
}
$backoff = [Math]::Min(2 * $crashStreak, 10)
Write-Sup "non-zero exit (code=$code), crashStreak=$crashStreak -> backoff ${backoff}s"
Start-Sleep -Seconds $backoff
continue
}
}
# 干净退出:删 pidfile。
if (Test-Path $PidFile) { Remove-Item $PidFile -Force -ErrorAction SilentlyContinue }
Write-Sup 'supervisor stopped'