Skip to content

fix: stop the updater aborting on conditions it is supposed to survive - #12

Merged
StaticHumStudio merged 1 commit into
mainfrom
fix/updater-survives-pending-service-and-broken-gh
Aug 8, 2026
Merged

fix: stop the updater aborting on conditions it is supposed to survive#12
StaticHumStudio merged 1 commit into
mainfrom
fix/updater-survives-pending-service-and-broken-gh

Conversation

@StaticHumStudio

Copy link
Copy Markdown
Owner

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-HumWatch took the service control's exit code as the verdict:

& $nssm stop HumWatch 2>$null; if ($LASTEXITCODE -ne 0) { throw 'The HumWatch service could not be stopped' }

nssm returns nonzero for SERVICE_STOP_PENDING, which is a service shutting down normally. The observed failure, verbatim:

HumWatch: Unexpected status SERVICE_STOP_PENDING in response to STOP control.
[ERROR] Update failed and rollback needs operator attention. No success marker was written.

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-HumWatch had the same shape for SERVICE_START_PENDING, and the Stop-Service / Start-Service fallbacks 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:

non-200 OK status code: 401 Unauthorized body: "{\"message\": \"Requires authentication\" ...}"

The updater refused to update from a release that is public and anonymously downloadable, because the Invoke-WebRequest fallback only triggers when gh is absent, never when gh is broken.

Fix: probe gh with the metadata call, and clear $gh when 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 to 2.0.0 so 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:

BEFORE  version=2.0.0  service=Running
EXIT CODE: 0
AFTER   version=2.0.1  service=Running
AFTER   status marker present: False
AFTER   acl protected=True  owner=BUILTIN\Administrators
AFTER   unauthorized writable grants: 0
AFTER   leftover temp dirs: 0

update.bat launched clean, so #7's 8,191-character fix is confirmed on real hardware as well. The live install at C:\HumWatch was 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 status reports The 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

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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread scripts/update-core.ps1
$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 } }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@StaticHumStudio
StaticHumStudio merged commit e754655 into main Aug 8, 2026
7 checks passed
@StaticHumStudio
StaticHumStudio deleted the fix/updater-survives-pending-service-and-broken-gh branch August 8, 2026 16:34
StaticHumStudio added a commit that referenced this pull request Aug 8, 2026
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>
StaticHumStudio added a commit that referenced this pull request Aug 8, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant