fix(update): coordinate Windows gateway pause across three layers during desktop update (#74386) - #74419
Conversation
…rting (NousResearch#74326) The desktop's Update button has been unusable on gateway-enabled Windows installs since the updater redesign. Every click kills the app's own backend, then the venv-blocker preflight finds the always-running gateway and aborts. The CLI updater already has pause/resume logic, but the desktop preflight never reaches it. Add a two-layer gateway-aware preflight: 1. Pre-pause gateways via _gateway_update_lock.py (subprocess bridge to native CLI logic) 2. Fallback taskkill-based cleanup for gateways the pre-pause missed New files: - hermes_cli/_gateway_update_lock.py: subprocess-callable gateway pause/resume Modified files: - apps/desktop/electron/main.ts: pauseWindowsGatewaysForUpdate/resumeWindowsGatewaysAfterUpdate + modified preflight - apps/desktop/electron/venv-blocker-scan.ts: stopVenvBlockers/isGatewayProcess - apps/desktop/electron/venv-blocker-scan.test.ts: tests for new functions
…lpers (NousResearch#74326) Add GATEWAY_CMDLINE_MARKER constant, isGatewayProcess() to detect gateway processes from blocker scan output, and stopVenvBlockers() to force-kill them via taskkill. Full test coverage.
…se on Windows (NousResearch#74386) The CLI updater's venv-holder check after gateway pause treats ANY remaining venv process as a fatal blocker, even when those processes are gateways that _pause_windows_gateways_for_update() missed (e.g. gateways spawned via --replace or shim-wrapped children outside the PID-file registry). This undoes the pause (calls _resume) and aborts. Two fixes: 1. Check env var before pausing gateways. When set by the Desktop Electron preflight, use the Electron's token as the resume token instead of pausing again (the gateways are already gone). The CLI updater must still resume on exit using the same token. 2. When _detect_venv_python_processes() finds remaining holders after pause, classify each by cmdline. If ALL are gateways, force-kill them with terminate_pid(force=True) and proceed — the resume token will respawn them post-update. Only abort when a non-gateway holder (e.g. the Desktop backend) remains. Closes NousResearch#74386
|
Thanks for picking this up @webtecnica — I filed #74386 and walked the three-layer chain on a live gateway-enabled Windows install. This PR hits the right shape: Electron pre-pause before I read the full diff. A few notes from that path: Resume token drop on the taskkill path (real regression risk) In That is wrong when Suggested rule: if Classification mismatch (minor) Electron uses Empty / no-gateway token from the bridge
Heavy import on the preflight path The bridge imports Layer 2 (Rust bootstrapper) Issue also called out Happy to re-test on the same Windows gateway install once the resume-token handoff is tightened. Appreciate the thoroughness. |
1. Resume token: remove noGatewayResumeNeeded flag on taskkill path
(token always carries profile data needed for resume)
2. Classification: align gateway marker (gateway run → gateway)
to match Python CLI's broader detection
3. Empty token: return null when no gateways running, not {}
4. Import: hermes_cli.main → hermes_cli.update_cmd (lighter import)
|
Thanks for the thorough review @apoapostolov! Addressed all 4 points in 3d9d05a: 1. Resume token (🔴 regression risk) — FIXED 2. Classification mismatch (🟡 minor) — FIXED 3. Empty token (🟢 harmless) — FIXED 4. Heavy import (🟡 perf) — FIXED Happy to iterate if anything needs tightening further! |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for tracing the Electron and CLI sides of this Windows update failure. The underlying premise is still present on current main: apps/desktop/electron/main.ts:2936-2945 aborts any blocked venv scan, and hermes_cli/update_cmd.py:3128-3132 aborts any remaining holder after pause.
Problems
apps/desktop/electron/main.ts:2754fails the required desktop lint check because the new untypedPromiseis inferred as{}, sostdoutis not a valid property.apps/desktop/electron/venv-blocker-scan.ts:236treats any command line containinggatewayas killable. The added test atvenv-blocker-scan.test.ts:250demonstrates the mismatch:git log --grep=gatewayis expected to be non-gateway but is classified true in CI.
Suggested changes
- Give the pause subprocess Promise a concrete
{ stdout: string }result type (or use a typed promisified API). - Parse a Hermes gateway invocation rather than using a free-text substring, and retain coverage for arguments that merely mention
gateway. - Add coverage for the Python pre-paused-token and mixed-holder paths. The current Rust fallback remains limited to
hermes.exeatapps/bootstrap-installer/src-tauri/src/update.rs:738-758; scope or cover that residual path explicitly.
Automated hermes-sweeper review.
| } | ||
|
|
||
| try { | ||
| const { stdout } = await new Promise((resolve, reject) => { |
There was a problem hiding this comment.
This untyped Promise is inferred as Promise<{}>, so destructuring stdout fails the required desktop lint check. Give the resolved payload an explicit { stdout: string } type or use a typed promisified execFile result.
| * classification in ``update_cmd.py``). | ||
| */ | ||
| export function isGatewayProcess(proc: VenvBlockerProcess): boolean { | ||
| return proc.cmdline.toLowerCase().includes(GATEWAY_CMDLINE_MARKER) |
There was a problem hiding this comment.
A free-text gateway match is too broad for a force-kill decision: it makes the added git log --grep=gateway test fail in CI and can match non-gateway command arguments. Parse/validate the Hermes gateway invocation instead.
|
Closing in favor of #74618, which we're salvaging for the Windows gateway-pause dead-end (#74326/#74386). The Electron-side pre-pause can't reach already-bricked installs (it ships via the very updater that's broken), the desktop typecheck was failing, and the loose Appreciate the deep three-layer analysis — it maps the coordination problem better than the issue itself did. |
|
Makes sense — thanks for the clear call. The “already-bricked install” constraint is the decider: anything that only lands after a successful desktop rebuild cannot unstick the path that is currently broken. Putting the preflight exemption + single canonical matcher in repo Python ( I’ll watch #74618 for #74386 / #74326. Appreciate you and @iso2kx pushing the CLI-side salvage rather than stacking another TS matcher. |
Summary
Fixes #74386 — on Windows gateway-enabled installs, the desktop Update button triggers a three-layer chain (Electron → Rust bootstrapper → Python CLI) where each layer has a different gap in gateway process coordination, guaranteeing that the desktop updater can never complete on a gateway-enabled install without manual
hermes gateway stopintervention.Changes
Layer 1 — Electron (apps/desktop/electron/main.ts)
Add
pauseWindowsGatewaysForUpdate/resumeWindowsGatewaysAfterUpdatefunctions that run the Python_gateway_update_locksubprocess bridge to pause gateways before the venv-blocker scan. The pre-flight blocker scan was finding the running gateway and aborting before the CLI updater even started.The resume token is passed to the spawned CLI updater via
HERMES_WINDOWS_GATEWAY_RESUME_TOKENenv var so it does not double-pause. On error paths (blocked/probe-failure), gateways are resumed before the UI shows the error.Layer 2 — Fallback
stopVenvBlockersinvenv-blocker-scan.tsprovides a TypeScript-side fallback: when the blocker scan finds processes,isGatewayProcess()classifies them by cmdline, and gateway PIDs are force-killed viataskkill /Fbefore re-scanning. Non-gateway processes are still reported as blockers.Layer 3 — Python CLI (hermes_cli/update_cmd.py, hermes_cli/_gateway_update_lock.py)
Two fixes:
Env-var token passthrough: Before calling
_pause_windows_gateways_for_update(), checkHERMES_WINDOWS_GATEWAY_RESUME_TOKEN. When set by the Electron preflight, use the Electron's token as the resume token (skip re-pause — gateways are already gone) but still register the atexit resume handler.Graceful gateway handling in venv-holder check: When
_detect_venv_python_processes()finds remaining holders after gateway pause, classify each by cmdline. If all are gateways (e.g. gateways spawned via--replaceor shim-wrapped children thatfind_gateway_pids()missed), force-kill them withterminate_pid(force=True)and proceed instead of aborting. Only abort when a non-gateway holder (the Desktop backend) remains.New module: hermes_cli/_gateway_update_lock.py
Subprocess-callable bridge invoked by Electron:
python -m hermes_cli._gateway_update_lock pause— calls_pause_windows_gateways_for_update(), prints JSON resume tokenpython -m hermes_cli._gateway_update_lock resume <json-token>— calls_resume_windows_gateways_after_update()Files changed
apps/desktop/electron/main.tsapps/desktop/electron/venv-blocker-scan.tsapps/desktop/electron/venv-blocker-scan.test.tshermes_cli/_gateway_update_lock.pyhermes_cli/update_cmd.pyRelated