mirror of
https://github.com/hansjone/netx.git
synced 2026-10-14 19:50:53 +08:00
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:
parent
c8806c38bd
commit
a7cc1f502b
4 changed files with 53 additions and 9 deletions
|
|
@ -65,11 +65,21 @@ function Ensure-WinSW {
|
||||||
$stopPs1 = ConvertTo-WinSWPath (Join-Path $PSScriptRoot "stop_netx_app.ps1")
|
$stopPs1 = ConvertTo-WinSWPath (Join-Path $PSScriptRoot "stop_netx_app.ps1")
|
||||||
$progFs = ConvertTo-WinSWPath $prog
|
$progFs = ConvertTo-WinSWPath $prog
|
||||||
$dataFs = ConvertTo-WinSWPath $data
|
$dataFs = ConvertTo-WinSWPath $data
|
||||||
$logPath = ConvertTo-WinSWPath (Join-Path $data "data\runtime")
|
# Own folder: WinSW writes <id>.out.log / <id>.err.log = NetX.out.log, which on Windows is
|
||||||
if (-not (Test-Path (Join-Path $data "data\runtime"))) {
|
# the same file as start_netx.ps1's netx.out.log. WinSW keeps it open, so NetX could not
|
||||||
New-Item -ItemType Directory -Path (Join-Path $data "data\runtime") -Force | Out-Null
|
# truncate its log and every service start failed.
|
||||||
|
$svcLogDir = Join-Path $data "data\runtime\service"
|
||||||
|
$logPath = ConvertTo-WinSWPath $svcLogDir
|
||||||
|
if (-not (Test-Path $svcLogDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $svcLogDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Stop hook via cmd: NUL stdin + output to a file. Launched by WinSW directly,
|
||||||
|
# powershell.exe never reached the script's first line (stuck on the inherited
|
||||||
|
# std handles), so the supervisor was never told to exit and stops hung.
|
||||||
|
$stopLog = Join-Path $svcLogDir "stopexec.log"
|
||||||
|
$stopCmd = "/d /s /c `"powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$stopPs1`" -ProgramRoot `"$progFs`" -DataRoot `"$dataFs`" <NUL >>`"$stopLog`" 2>&1`""
|
||||||
|
|
||||||
$xml = @"
|
$xml = @"
|
||||||
<service>
|
<service>
|
||||||
<id>$svcName</id>
|
<id>$svcName</id>
|
||||||
|
|
@ -86,8 +96,8 @@ function Ensure-WinSW {
|
||||||
<onfailure action="restart" delay="30 sec"/>
|
<onfailure action="restart" delay="30 sec"/>
|
||||||
<resetfailure>1 hour</resetfailure>
|
<resetfailure>1 hour</resetfailure>
|
||||||
<stoptimeout>60sec</stoptimeout>
|
<stoptimeout>60sec</stoptimeout>
|
||||||
<stopexecutable>powershell.exe</stopexecutable>
|
<stopexecutable>cmd.exe</stopexecutable>
|
||||||
<stoparguments>-NoProfile -ExecutionPolicy Bypass -File "$stopPs1" -ProgramRoot "$progFs" -DataRoot "$dataFs"</stoparguments>
|
<stoparguments>$stopCmd</stoparguments>
|
||||||
<workingdirectory>$progFs</workingdirectory>
|
<workingdirectory>$progFs</workingdirectory>
|
||||||
</service>
|
</service>
|
||||||
"@
|
"@
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ function Stop-NetxRunners([switch]$Remove) {
|
||||||
$state | ConvertTo-Json | Set-Content -LiteralPath $stateFile -Encoding utf8
|
$state | ConvertTo-Json | Set-Content -LiteralPath $stateFile -Encoding utf8
|
||||||
if ($svc.Status -ne 'Stopped') {
|
if ($svc.Status -ne 'Stopped') {
|
||||||
Write-LifecycleLog 'Stopping Windows service NetX'
|
Write-LifecycleLog 'Stopping Windows service NetX'
|
||||||
|
Write-NetxServiceStopFlag -DataRoot $data
|
||||||
Stop-Service -Name 'NetX' -Force -ErrorAction Stop
|
Stop-Service -Name 'NetX' -Force -ErrorAction Stop
|
||||||
(Get-Service -Name 'NetX').WaitForStatus('Stopped', [TimeSpan]::FromSeconds(60))
|
(Get-Service -Name 'NetX').WaitForStatus('Stopped', [TimeSpan]::FromSeconds(60))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,16 @@ function Start-NetxChildren {
|
||||||
|
|
||||||
function Stop-NetxChildren {
|
function Stop-NetxChildren {
|
||||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $stopPs1 `
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $stopPs1 `
|
||||||
-ProgramRoot $prog -DataRoot $data
|
-ProgramRoot $prog -DataRoot $data -KeepSupervisor
|
||||||
|
}
|
||||||
|
|
||||||
|
function Wait-NetxStopFlag([int]$Seconds) {
|
||||||
|
# Sleep in 1s steps so a service stop (stop flag) is noticed quickly.
|
||||||
|
for ($i = 0; $i -lt $Seconds; $i++) {
|
||||||
|
if (Test-Path $stopFlag) { return $true }
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
}
|
||||||
|
return (Test-Path $stopFlag)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -83,11 +92,11 @@ try {
|
||||||
Start-NetxChildren
|
Start-NetxChildren
|
||||||
$failStreak = 0
|
$failStreak = 0
|
||||||
# Give API time to come up before counting failures again.
|
# Give API time to come up before counting failures again.
|
||||||
Start-Sleep -Seconds ([Math]::Max(15, $HealthIntervalSec))
|
$null = Wait-NetxStopFlag ([Math]::Max(15, $HealthIntervalSec))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Start-Sleep -Seconds $HealthIntervalSec
|
$null = Wait-NetxStopFlag $HealthIntervalSec
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
Write-SvcLog "ERROR: $($_.Exception.Message)"
|
Write-SvcLog "ERROR: $($_.Exception.Message)"
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,23 @@ param(
|
||||||
[string]$ProgramRoot = "",
|
[string]$ProgramRoot = "",
|
||||||
[string]$DataRoot = "",
|
[string]$DataRoot = "",
|
||||||
[switch]$KeepPostgres = $false,
|
[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"
|
$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"
|
. "$PSScriptRoot\_common.ps1"
|
||||||
Assert-NetxAdminOrRelaunch -ScriptPath $PSCommandPath -BoundParameters $PSBoundParameters -WindowStyle Hidden
|
Assert-NetxAdminOrRelaunch -ScriptPath $PSCommandPath -BoundParameters $PSBoundParameters -WindowStyle Hidden
|
||||||
|
|
||||||
|
|
@ -17,14 +30,24 @@ if (Test-Path $envPath) {
|
||||||
$map = Read-DotEnv -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"
|
$stopScript = Join-Path $prog "scripts\stop_netx.ps1"
|
||||||
if (-not (Test-Path $stopScript)) {
|
if (-not (Test-Path $stopScript)) {
|
||||||
$stopScript = Join-Path (Get-NetxRepoRoot) "scripts\stop_netx.ps1"
|
$stopScript = Join-Path (Get-NetxRepoRoot) "scripts\stop_netx.ps1"
|
||||||
}
|
}
|
||||||
if (Test-Path $stopScript) {
|
if (Test-Path $stopScript) {
|
||||||
Write-Host "==> Stopping NetX processes"
|
Write-Host "==> Stopping NetX processes"
|
||||||
|
Write-StopTrace "stop_netx.ps1 begin"
|
||||||
Start-NetxPowerShell -File $stopScript -Arguments @("-Force") `
|
Start-NetxPowerShell -File $stopScript -Arguments @("-Force") `
|
||||||
-WorkingDirectory $prog -WindowStyle Hidden -Wait | Out-Null
|
-WorkingDirectory $prog -WindowStyle Hidden -Wait | Out-Null
|
||||||
|
Write-StopTrace "stop_netx.ps1 end"
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[WARN] stop_netx.ps1 not found"
|
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"
|
Write-Host "==> Stop done"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue