From c8806c38bd4ba09953702632ea5477a322e8f33a Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 14 Oct 2026 00:46:44 +0800 Subject: [PATCH] Never wait on the process tree when starting NetX. Windows PowerShell 5.1 Start-Process -Wait (and `& exe` with captured output) waits for every descendant / until the stdout pipe closes. The postgres and NetX processes a start leaves running made runtime_ctl.ps1 never return: tray stuck on "Starting", Start Menu "Start NetX" never opened the UI. Start-NetxPowerShell -Wait, the UAC relaunch and the Setup wait in check_update now wait for the process itself (WaitForExit). Service mode in runtime_ctl: start via sc.exe without blocking on SERVICE_RUNNING; stop writes the supervisor stop flag, polls up to 120s, then kills the WinSW tree as a last resort. Verified in an offline Windows Sandbox (no VC++ redist, no Python). Co-Authored-By: Claude Opus 5.5 --- packaging/_common.ps1 | 24 ++++++++++++++++++--- packaging/check_update.ps1 | 6 ++++-- packaging/runtime_ctl.ps1 | 44 +++++++++++++++++++++++++++++++------- 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/packaging/_common.ps1 b/packaging/_common.ps1 index 085306b..355593a 100644 --- a/packaging/_common.ps1 +++ b/packaging/_common.ps1 @@ -313,6 +313,14 @@ function Get-NetxManagedRunner { return "" } +function Write-NetxServiceStopFlag { + param([Parameter(Mandatory = $true)][string]$DataRoot) + # service_run.ps1 (the WinSW supervisor) exits when this file appears. + $runtimeDir = Join-Path $DataRoot "data\runtime" + if (-not (Test-Path $runtimeDir)) { return } + Set-Content -Path (Join-Path $runtimeDir "service.stop") -Value (Get-Date -Format o) -Encoding ascii -ErrorAction SilentlyContinue +} + function Wait-NetxApiHealthy { param( [string]$HostName = "127.0.0.1", @@ -371,10 +379,19 @@ function Start-NetxPowerShell { $sp.WindowStyle = $WindowStyle } if ($WorkingDirectory) { $sp.WorkingDirectory = $WorkingDirectory } - if ($Wait) { $sp.Wait = $true } if ($PassThru -or $Wait) { $sp.PassThru = $true } if ($RunAs) { $sp.Verb = "RunAs" } - return Start-Process @sp + $p = Start-Process @sp + if ($Wait -and $p) { Wait-NetxProcessExit -Process $p } + return $p +} + +function Wait-NetxProcessExit { + param([Parameter(Mandatory = $true)]$Process) + # Windows PowerShell 5.1 Start-Process -Wait also waits for every descendant; the + # postgres / NetX processes a start script leaves running would block it forever. + $null = $Process.Handle # keep a handle so ExitCode survives the exit + $Process.WaitForExit() } function Test-NetxIsAdmin { @@ -449,7 +466,8 @@ function Assert-NetxAdminOrRelaunch { $p = Start-Process -FilePath "powershell.exe" ` -ArgumentList (ConvertTo-NetxProcessArgumentString -Arguments $parts) ` -WorkingDirectory $wd ` - -Verb RunAs -Wait -PassThru + -Verb RunAs -PassThru + if ($p) { Wait-NetxProcessExit -Process $p } } catch { if ($null -ne $prevRel) { $env:NETX_ELEVATION_RELAUNCH = $prevRel } else { Remove-Item Env:NETX_ELEVATION_RELAUNCH -ErrorAction SilentlyContinue } throw ("admin_required: UAC cancelled or elevation failed. Run NetX as Administrator. {0}" -f $_.Exception.Message) diff --git a/packaging/check_update.ps1 b/packaging/check_update.ps1 index cce64ff..c8ecae8 100644 --- a/packaging/check_update.ps1 +++ b/packaging/check_update.ps1 @@ -429,7 +429,8 @@ try { Write-Host "==> Program directory is not writable; relaunching update as Administrator (UAC)..." -ForegroundColor Yellow $arg = "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`" -ProgramRoot `"$prog`" -DataRoot `"$data`" -Apply" $ep = Start-Process -FilePath "powershell.exe" -ArgumentList $arg -WorkingDirectory $prog ` - -Verb RunAs -Wait -PassThru + -Verb RunAs -PassThru + Wait-NetxProcessExit -Process $ep if ($null -eq $ep.ExitCode) { exit 1 } exit $ep.ExitCode } @@ -444,7 +445,8 @@ try { $trayWasRunning = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -and $_.CommandLine -match 'netx_tray\.ps1' }).Count -gt 0 - $sp = Start-Process -FilePath $pkgPath -ArgumentList $setupArgs -Wait -PassThru + $sp = Start-Process -FilePath $pkgPath -ArgumentList $setupArgs -PassThru + Wait-NetxProcessExit -Process $sp if ($null -eq $sp.ExitCode -or $sp.ExitCode -ne 0) { $code = if ($null -eq $sp.ExitCode) { "null" } else { $sp.ExitCode } throw "setup_update_failed: exit $code" diff --git a/packaging/runtime_ctl.ps1 b/packaging/runtime_ctl.ps1 index 6a7e5f3..a7e4a10 100644 --- a/packaging/runtime_ctl.ps1 +++ b/packaging/runtime_ctl.ps1 @@ -16,19 +16,47 @@ $prog = Get-NetxProgramRoot -Override $ProgramRoot $data = Get-NetxDataRoot -ProgramRoot $prog -Override $DataRoot function Invoke-NetxChildScript([string]$Name, [string[]]$Extra = @()) { - & powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot $Name) ` - -ProgramRoot $prog -DataRoot $data @Extra - return $LASTEXITCODE + # Never read the child's stdout: postgres / NetX processes it starts inherit the + # pipe, so `& powershell.exe` with captured output blocks until they exit (forever). + # Start-Process -Wait is no good either (it waits for the whole process tree). + $p = Start-NetxPowerShell -File (Join-Path $PSScriptRoot $Name) ` + -Arguments (@("-ProgramRoot", $prog, "-DataRoot", $data) + $Extra) ` + -WorkingDirectory $prog -WindowStyle Hidden -PassThru + $null = $p.Handle # keep a handle so ExitCode is available after exit + $p.WaitForExit() + return $p.ExitCode +} + +function Stop-NetxService { + # Stop-Service waits forever on a service that will not stop; poll with a deadline + # and kill the WinSW process tree as a last resort. + $svc = Get-Service -Name "NetX" -ErrorAction SilentlyContinue + if (-not $svc -or $svc.Status -eq "Stopped") { return } + # Ask service_run.ps1 to exit ourselves rather than relying only on WinSW's stop hook. + Write-NetxServiceStopFlag -DataRoot $data + & sc.exe stop NetX | Out-Null + $deadline = (Get-Date).AddSeconds(120) + while ((Get-Date) -lt $deadline) { + Start-Sleep -Seconds 1 + $svc.Refresh() + if ($svc.Status -eq "Stopped") { return } + } + $wmi = Get-CimInstance Win32_Service -Filter "Name='NetX'" -ErrorAction SilentlyContinue + if ($wmi -and $wmi.ProcessId) { & taskkill.exe /PID $wmi.ProcessId /T /F | Out-Null } + $null = Invoke-NetxChildScript "stop_netx_app.ps1" +} + +function Start-NetxService { + # Don't block on SERVICE_RUNNING: callers wait for /health instead. + $svc = Get-Service -Name "NetX" -ErrorAction SilentlyContinue + if ($svc -and $svc.Status -eq "Stopped") { & sc.exe start NetX | Out-Null } } $runner = Get-NetxManagedRunner switch ($runner) { "service" { - switch ($Action) { - "start" { Start-Service -Name "NetX" } - "stop" { Stop-Service -Name "NetX" -Force } - "restart" { Restart-Service -Name "NetX" -Force } - } + if ($Action -ne "start") { Stop-NetxService } + if ($Action -ne "stop") { Start-NetxService } } "task" { if ($Action -ne "start") {