fix: stop the updater aborting on conditions it is supposed to survive - #12
Conversation
A portable upgrade test on a live v1.0.1 station failed twice, in two different places, before it ever reached the code the last release changed. Stop-HumWatch treated the service control's exit code as the verdict. nssm returns nonzero for SERVICE_STOP_PENDING, which is a service shutting down normally, and the throw preempted the 30 second poll sitting directly below it that exists to wait for exactly that. The update aborted, the rollback could not start a service it had just been told it failed to stop, and the station was left with HumWatch down and "rollback needs operator attention" on screen. Start-HumWatch had the same shape for SERVICE_START_PENDING, and the Stop-Service and Start-Service paths raise on a pending transition too. The poll is now the only authority and the only thing that throws, and the control's output survives into the timeout message instead of being discarded (with nssm's UTF-16 NULs stripped so it is readable). The release lookup chose the gh path on the mere presence of the executable. This station has gh installed with an expired token, so every call returned 401 and the updater refused to update from a public release it could have downloaded anonymously. gh is now probed with the metadata call and cleared when it cannot answer, which moves the archive download onto the same anonymous path rather than back into the same 401. Verified on the same station, portable v2.0.1 tree backdated to 2.0.0: update.bat now exits 0, the tree goes 2.0.0 to 2.0.1, the service stops and comes back Running, the tree ends protected and owned by Administrators with zero unauthorized writable grants, and no temp directories are left behind. 241 passed, 5 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e397c6bde5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| $current = Get-Service -Name HumWatch -ErrorAction SilentlyContinue; if (-not $current -or $current.Status -eq 'Stopped') { return } | ||
| if (Test-Path -LiteralPath $nssm -PathType Leaf) { & $nssm stop HumWatch 2>$null; if ($LASTEXITCODE -ne 0) { throw 'The HumWatch service could not be stopped' } } else { Stop-Service -Name HumWatch -Force -ErrorAction Stop } | ||
| $detail = '' | ||
| if (Test-Path -LiteralPath $nssm -PathType Leaf) { $controlOutput = & $nssm stop HumWatch 2>&1; if ($LASTEXITCODE -ne 0) { $detail = Get-ControlDetail $controlOutput } } else { try { Stop-Service -Name HumWatch -Force -ErrorAction Stop } catch { $detail = $_.Exception.Message } } |
There was a problem hiding this comment.
Suppress native stderr before polling NSSM
On Windows PowerShell 5.1, when NSSM emits stderr—including the pending-transition diagnostic this change intends to tolerate—2>&1 converts it into an error record, and the script-wide $ErrorActionPreference = 'Stop' terminates this assignment before $LASTEXITCODE is inspected or the poll loop runs. The same pattern occurs in Start-HumWatch, so both paths can still abort on a successful slow transition; temporarily set the error preference to SilentlyContinue around the native invocation, as Invoke-Nssm already does in installer/service-setup.ps1.
Useful? React with 👍 / 👎.
The portable installation job already builds the exact zip and SHA256 sidecar the updater expects, runs the tamper-rejection test against them, and then threw both away. Every release since has had them attached by hand. update-core.ps1 resolves HumWatch-vX.Y.Z.zip and its .sha256 by exact name on the latest release and throws when either is missing. So a forgotten upload does not degrade the updater, it disables it on every portable install pointed at that release. That is a worse outcome than the bug #12 just fixed, and it was one distracted release away at any moment. The release job now attaches the same bytes the tamper test just validated, alongside the installer. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Carries the updater fixes from #12. A v2.0.1 station whose service reports SERVICE_STOP_PENDING, or whose gh login has expired, cannot currently update itself, and the first of those can leave the machine with HumWatch stopped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by running the portable upgrade test on a real station (
fuckroach, live v1.0.1 install). The updater failed twice, in two different places, before it ever reached the code #7 changed.Bug 1: a pending stop is treated as a refusal
Stop-HumWatchtook the service control's exit code as the verdict:nssm returns nonzero for
SERVICE_STOP_PENDING, which is a service shutting down normally. The observed failure, verbatim:The throw preempted the 30-second poll sitting directly below it, which exists to wait for exactly this. Worse, the rollback then could not start a service it had just been told it failed to stop, so the station was left with HumWatch down and a banner telling the operator to sort it out. On a monitoring agent that is the worst available outcome.
Start-HumWatchhad the same shape forSERVICE_START_PENDING, and theStop-Service/Start-Servicefallbacks raise on a pending transition too.Fix: the poll is the only authority and the only thing that throws. The control's output survives into the timeout message rather than being discarded, with nssm's UTF-16 NULs stripped so it is legible (that message arrived as
H u m W a t c h : U n e x p e c t e d ...).Bug 2: a broken gh strands the installation
The release lookup chose the gh path on the presence of the executable alone. That station has gh installed with an expired token, so:
The updater refused to update from a release that is public and anonymously downloadable, because the
Invoke-WebRequestfallback only triggers when gh is absent, never when gh is broken.Fix: probe gh with the metadata call, and clear
$ghwhen it cannot answer, which moves the archive download onto the same anonymous path instead of straight back into the same 401. A broken gh is not a reason to refuse an update, and it is also not trusted for the rest of the run.Verification
Same station, portable v2.0.1 tree with
__version__backdated to2.0.0so the updater had real work to do. Before the fix, two consecutive failures (401, then the pending-stop abort that took the live service down). After:update.batlaunched clean, so #7's 8,191-character fix is confirmed on real hardware as well. The live install atC:\HumWatchwas never touched and its agent is healthy ({"status":"ok","version":"1.0.1"}). Test tree and helper scripts removed.Two regression tests added,
241 passed, 5 skipped.Not fixed here
The station's
gh auth statusreportsThe token in default is invalid. That is a machine problem, not a repo problem, but it is what surfaced bug 2.🤖 Generated with Claude Code