Fix Windows service mode: it could neither start nor stop.

- Start: WinSW's NetX.out.log/err.log in data\runtime are the same files as
  start_netx.ps1's netx.out.log (case-insensitive) and stay open, so NetX
  could not truncate its log and every start failed. WinSW logs now go to
  data\runtime\service.
- Stop: nothing told the service_run.ps1 supervisor to exit, so it kept
  restarting NetX and WinSW waited forever. stop_netx_app.ps1 writes the
  service.stop flag (-KeepSupervisor for the supervisor's own restarts),
  service_run checks it every second, and runtime_ctl / service_lifecycle
  write it before stopping the service.
- WinSW launched powershell.exe as stop hook never reached the script
  (stuck on inherited std handles); run it via cmd with NUL stdin and output
  to data\runtime\service\stopexec.log. stop_netx_app.ps1 keeps a
  timestamped trace there.

Offline Windows Sandbox: service install -> healthy in 28s, stop in 20s.
Existing service installs need "install service" re-run to pick this up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-14 00:46:44 +08:00
parent c8806c38bd
commit a7cc1f502b
4 changed files with 53 additions and 9 deletions

View file

@ -2,10 +2,23 @@ param(
[string]$ProgramRoot = "",
[string]$DataRoot = "",
[switch]$KeepPostgres = $false,
[switch]$KillTray = $false
[switch]$KillTray = $false,
# Used by service_run.ps1 when it restarts NetX itself: do not ask it to exit.
[switch]$KeepSupervisor = $false
)
$ErrorActionPreference = "Continue"
# Timestamped trace (service stop path is hard to observe otherwise).
$script:stopTrace = $null
function Write-StopTrace([string]$Msg) {
if (-not $script:stopTrace) { return }
try { Add-Content -Path $script:stopTrace -Value ("{0} [{1}] {2}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"), $PID, $Msg) -Encoding utf8 } catch {}
}
if ($DataRoot) {
$traceDir = Join-Path $DataRoot "data\runtime\service"
if (Test-Path $traceDir) { $script:stopTrace = Join-Path $traceDir "stop_netx_app.log" }
}
Write-StopTrace "begin user=$([Environment]::UserName) keepSupervisor=$KeepSupervisor"
. "$PSScriptRoot\_common.ps1"
Assert-NetxAdminOrRelaunch -ScriptPath $PSCommandPath -BoundParameters $PSBoundParameters -WindowStyle Hidden
@ -17,14 +30,24 @@ if (Test-Path $envPath) {
$map = Read-DotEnv -Path $envPath
}
# Tell the service supervisor (service_run.ps1) to exit first. WinSW runs this script as
# <stopexecutable> and then waits for the supervisor to exit; without the flag it kept
# looping, restarted NetX, and the service never stopped.
if (-not $KeepSupervisor) {
Write-NetxServiceStopFlag -DataRoot $data
Write-StopTrace "stop flag written"
}
$stopScript = Join-Path $prog "scripts\stop_netx.ps1"
if (-not (Test-Path $stopScript)) {
$stopScript = Join-Path (Get-NetxRepoRoot) "scripts\stop_netx.ps1"
}
if (Test-Path $stopScript) {
Write-Host "==> Stopping NetX processes"
Write-StopTrace "stop_netx.ps1 begin"
Start-NetxPowerShell -File $stopScript -Arguments @("-Force") `
-WorkingDirectory $prog -WindowStyle Hidden -Wait | Out-Null
Write-StopTrace "stop_netx.ps1 end"
} else {
Write-Host "[WARN] stop_netx.ps1 not found"
}
@ -51,4 +74,5 @@ if ($mode -eq "bundled" -and -not $KeepPostgres) {
}
}
Write-StopTrace "done"
Write-Host "==> Stop done"