Harden Windows packaging: data-root paths, service health, safer updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-10-07 00:12:14 +08:00
parent d0b260b67d
commit d10d47e8f5
10 changed files with 228 additions and 56 deletions

View file

@ -1,10 +1,12 @@
param(
[string]$ProgramRoot = "",
[string]$DataRoot = ""
[string]$DataRoot = "",
[int]$HealthFailLimit = 6,
[int]$HealthIntervalSec = 10
)
# Long-running supervisor for Windows Service / Task Scheduler.
# Starts NetX (and bundled PG), waits until stopped, then tears down.
# Starts NetX, probes /health; repeated failures trigger restart (or exit for WinSW).
$ErrorActionPreference = "Stop"
. "$PSScriptRoot\_common.ps1"
@ -16,6 +18,7 @@ if (-not (Test-Path $logDir)) {
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
}
$logFile = Join-Path $logDir "service_run.log"
$stopFlag = Join-Path $logDir "service.stop"
function Write-SvcLog([string]$Msg) {
$line = "{0} {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Msg
@ -23,40 +26,75 @@ function Write-SvcLog([string]$Msg) {
Write-Host $line
}
$stopFlag = Join-Path $logDir "service.stop"
Remove-Item -Force $stopFlag -ErrorAction SilentlyContinue
function Get-BindInfo {
$envPath = Join-Path $data ".env"
$hostBind = "127.0.0.1"
$port = 8890
if (Test-Path $envPath) {
$map = Read-DotEnv -Path $envPath
if ($map["NETX_HOST"]) { $hostBind = $map["NETX_HOST"] }
if ($map["NETX_PORT"]) { try { $port = [int]$map["NETX_PORT"] } catch {} }
}
return @{ Host = $hostBind; Port = $port }
}
Remove-Item -Force $stopFlag -ErrorAction SilentlyContinue
Write-SvcLog "service_run starting program=$prog data=$data"
$startPs1 = Join-Path $PSScriptRoot "start_netx_app.ps1"
$stopPs1 = Join-Path $PSScriptRoot "stop_netx_app.ps1"
$bind = Get-BindInfo
$failStreak = 0
$restartCount = 0
try {
function Start-NetxChildren {
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $startPs1 `
-ProgramRoot $prog -DataRoot $data -SkipBrowser
-ProgramRoot $prog -DataRoot $data -SkipBrowser -Force
if ($LASTEXITCODE -ne 0) {
throw "start_netx_app_failed exit=$LASTEXITCODE"
}
Write-SvcLog "NetX started; entering watch loop"
}
function Stop-NetxChildren {
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $stopPs1 `
-ProgramRoot $prog -DataRoot $data
}
try {
Start-NetxChildren
Write-SvcLog "NetX started; health watch host=$($bind.Host) port=$($bind.Port)"
while ($true) {
if (Test-Path $stopFlag) {
Write-SvcLog "stop flag detected"
break
}
Start-Sleep -Seconds 5
if (Test-NetxApiHealthy -HostName $bind.Host -Port $bind.Port -TimeoutSec 3) {
$failStreak = 0
} else {
$failStreak++
Write-SvcLog "health fail streak=$failStreak/$HealthFailLimit"
if ($failStreak -ge $HealthFailLimit) {
$restartCount++
Write-SvcLog "restarting NetX (attempt=$restartCount)"
try { Stop-NetxChildren } catch { Write-SvcLog "stop warning: $($_.Exception.Message)" }
Start-Sleep -Seconds 3
Start-NetxChildren
$failStreak = 0
# Give API time to come up before counting failures again.
Start-Sleep -Seconds ([Math]::Max(15, $HealthIntervalSec))
continue
}
}
Start-Sleep -Seconds $HealthIntervalSec
}
} catch {
Write-SvcLog "ERROR: $($_.Exception.Message)"
throw
} finally {
Write-SvcLog "stopping NetX"
try {
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $stopPs1 `
-ProgramRoot $prog -DataRoot $data
} catch {
Write-SvcLog "stop warning: $($_.Exception.Message)"
}
try { Stop-NetxChildren } catch { Write-SvcLog "stop warning: $($_.Exception.Message)" }
Remove-Item -Force $stopFlag -ErrorAction SilentlyContinue
Write-SvcLog "service_run exited"
}