Fix desktop updater runtime cleanup - #40558
Conversation
alpindiay
left a comment
There was a problem hiding this comment.
Code Review: Fix desktop updater runtime cleanup
Summary
Adds update-processes.cjs to detect and kill Hermes runtime processes before an update, plus a Windows smoke test (update-handoff-smoke.cjs) that validates the full update handoff flow including process cleanup, updater launch, and relaunch.
Issues Found
1. Incomplete process detection coverage (medium)
commandLooksHermesOwned only matches three entrypoints:
hermes_cli.main
hermes_cli.gateway
scripts/whatsapp-bridge
Missing: hermes_cli.cron (scheduler), plugins that import hermes_cli, any user script invoking hermes_cli.*, and the desktop bootstrap itself. Consider broadening to match on hermes_cli. prefix or adding additional patterns. If a cron daemon or plugin worker is running in the venv, it will survive the update and potentially lock files.
2. killHermesRuntimeProcessesForUpdate silently no-ops without killTree (low)
If called without the killTree option, the function collects PIDs but never kills anything. The return value still reports the PIDs as if they were handled. In the current call site (main.cjs) this is fine because forceKillProcessTree is always passed, but the exported API is misleading. Consider throwing or logging a warning when killTree is missing.
3. listWindowsProcesses swallows all errors (low)
If PowerShell fails (e.g., not installed, permissions issue), the function silently returns []. This means the updater proceeds believing no runtime processes exist when in reality it just can not enumerate them. Consider logging the failure or returning a distinct sentinel (e.g., null) to signal "unknown" vs "empty."
4. Smoke test: verbose setup, fragile WebSocket dep (low)
The smoke test compiles a .NET fake updater and uses CDP via WebSocket. This requires dotnet SDK and Node 21+ (global WebSocket). The check typeof WebSocket !== "function" throws an error; consider skipping gracefully with a diagnostic message instead. Also DOTNET_CLI_TELEMETRY_OPTOUT is misspelled (OPTOUT vs OPTOUT) — should be DOTNET_CLI_TELEMETRY_OPTOUT (though the standard env var is actually DOTNET_CLI_TELEMETRY_OPTOUT).
What Looks Good
- Thorough process matching: venv path prefix checks + sibling checkout exclusion
currentPidexclusion prevents self-kill- Well-structured path normalization with
normalizePathTexthandling both backslash and forward slash - Comprehensive unit tests in
update-processes.test.cjs: targets, false positives, sibling exclusion, parsing, collection - Smoke test validates the full end-to-end flow: lock detection, updater args, relaunch, log assertions
forceKillProcessTreeis the production kill mechanism (tree kill, not single PID)- Cleanup in smoke test
catchblock handles teardown properly
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
What this PR does
Refactors the desktop updater's pre-update process-killing logic. Instead of scattered taskkill invocations, a new killHermesRuntimeProcessesForUpdate() routine in update-processes.cjs identifies Hermes-owned runtime processes (venv python, node, npm, uv, etc.) by path + command-line heuristics and kills them before the update proceeds. A new smoke-test (update-handoff-smoke.cjs) exercises the handoff logic on Windows; main.cjs wires the new routine into the existing releaseBackendLockForUpdate path.
Looks Good
- Well-scoped concern: process-detection is now unit-testable, file-match helper (
isUnderPath) is deterministic. - Terrific Windows context awareness: handles both forward and backslash directory separators,
Win32_ProcessJSON parsing withProcessId/ExecutablePath/CommandLinecasing variants,maxBuffer: 16MBfor dense process dumps, andwindowsHide: true. - Defensive
try/catch+ no hard failures if PowerShell listing returns nothing. - New
RUNTIME_PROCESS_NAMESSet is a good focal point for future additions. update-handoff-smoke.cjsis valuable — thepostMessagehandshake pattern cleanly verifies child-process lifecycle with the parent Electron process.
Minor notes (non-blocking)
execFileSync('powershell.exe', [...], { timeout: 5000 })will throw synchronously if PowerShell is slow (>5s) on very loaded machines. An asyncspawn+ timeout path is more robust in edge cases, but the existing try/catch already degrades gracefully.commandLooksHermesOwnedhardcodeswhatsapp-bridge— that's fine but worth documenting why it's included (it runs alongside the runtime but outside the venv).
Reviewed by Hermes Agent
48cfefd to
b543e40
Compare
|
Addressed the review follow-ups in the amended commit
Verification run on Windows:
Smoke confirmed runtime stopped, venv shim unlocked, updater launched, relaunch happened, and no smoke-root processes remained. |
b543e40 to
b9960c2
Compare
|
Rebased this PR onto current New head: No rebase conflicts. Verification rerun on Windows from a clean PR worktree:
Smoke again confirmed runtime stopped, venv shim unlocked, updater launched with |
b9960c2 to
b152dbe
Compare
|
Rebased this PR onto current New head: Conflict resolved in Fresh Windows verification from the PR branch:
Smoke confirmed runtime process cleanup, venv shim unlock, updater launch with |
b152dbe to
3110bad
Compare
|
Review follow-up pushed. What changed since the last review pass:
Fresh validation on Windows:
Smoke proof from the latest run:
Live Windows proof from the local install:
One note from live testing: the first live attempt exposed an unrelated external file lock from a PyCharm Tailwind helper holding |
austinpickett
left a comment
There was a problem hiding this comment.
Hermes Agent Review — ✅ Approve
A large (+1281) but cohesive, well-tested PR. Verified novel vs origin/main (both new .cjs modules absent on main), checked out locally and ran the full new test surface.
What it does
Hardens the desktop→CLI update handoff with three additions, all cleanly extracted into testable modules:
-
update-processes.cjs— before an update overwrites files, enumerate and kill Hermes-owned runtime processes (node/python/uv/npm under the install or venv root) so Windows file locks don't fail the update. The match heuristic is appropriately conservative: a process-name allowlist AND (executable under updateRoot/venvORcommandLine mentions updateRoot AND looks Hermes-owned), and it excludes the current PID.killTreeis injected (testable) and the function throws if matches exist but no killTree is supplied — fail-safe rather than silently skipping. Path normalization handles Windows backslash/case correctly. -
update-handoff.cjs— on Windows, instead of a silent detached spawn, it writes a visible.cmdupdater (progress echoes, pause-on-failure so users see errors) intoHERMES_HOME/logs, with correct batch quoting (%%,""). Centralizes--backup/--brancharg building shared by the GUI button and the manual-command card. POSIX path unchanged (plain detached spawn). -
update.rs(Tauri installer) — refactors the inline arg vec into a testablebuild_update_args()and adds--backupso a desktop update (which replaces the app underneath the user) always creates a restore point — matching the JS side.--force/--branchrationale comments preserved; new Rust unit test added.
Verification
node --test apps/desktop/electron/update-processes.test.cjs => 10 pass, 1 skip, 0 fail
node --test apps/desktop/electron/update-handoff.test.cjs => 5 pass, 0 fail
node --check apps/desktop/electron/main.cjs => OK
The 1 skipped test is the PowerShell Get-CimInstance integration case (correctly platform-gated to Windows — not broken). Both unit suites are registered in test:desktop:platforms; the 626-line update-handoff-smoke.cjs is wired as a separate smoke:desktop:update-handoff script, so it doesn't bloat the normal test run — good separation. (Did not compile the Rust test here — no Rust toolchain in this env — but build_update_args + its assertion are straightforward and self-evidently correct.)
main.cjs wiring
Clean: killHermesRuntimeProcessesForUpdate is called in releaseBackendLock with injected forceKillProcessTree + rememberLog callbacks for list/kill errors; createUpdaterLaunchPlan replaces the inline spawn and the result drives spawn(launch.command, launch.args, {detached: launch.detached, windowsHide: launch.windowsHide}). Behavior preserved on POSIX.
Cluster note: part of the desktop module-extraction family — will need a trivial rebase against whichever sibling (#37471/#39554/#38292/#38589/#42901) lands first (textual conflicts on main.cjs imports + the test:desktop:platforms line), no semantic overlap.
Reviewed by Hermes Agent (local node --test on both suites + node --check; verified vs origin/main).
# Conflicts: # apps/bootstrap-installer/src-tauri/src/update.rs # apps/desktop/package.json
|
@austinpickett I rebased this onto current |
…ntime-cleanup # Conflicts: # apps/desktop/package.json
|
Repair update after rebasing against current
Local QA on Windows:
I also re-read |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the focused Windows updater coverage. The current desktop still only kills backend PIDs it owns (apps/desktop/electron/main.ts:2410-2436), so the process-discovery idea addresses a real remaining gap.
Problems
apps/desktop/electron/update-processes.cjs:79treats every allowlisted runtime executable under the target checkout or its venv as Hermes-owned, even without a Hermes command line. That can kill a user terminal runningvenv\\Scripts\\python.exe script.pyor another local task. Current main deliberately aborts rather than killing an outside holder (apps/desktop/electron/main.ts:2469-2480).- The PR targets removed CJS Electron surfaces. Main migrated
main.cjsand the platform tests to TypeScript in39d09453f; this needs a TypeScript port, not a direct merge.
Suggested changes
- Require a Hermes-specific command signature for generic Python/Node/uv processes, with tests for non-Hermes commands under the target venv.
- Port the helpers, wiring, and tests to the current TypeScript Electron layout.
Automated hermes-sweeper review.
| const commandIsHermes = commandLooksHermesOwned(commandLine) | ||
|
|
||
| return executableInVenv || executableInInstall || (commandMentionsInstall && commandIsHermes) | ||
| } |
There was a problem hiding this comment.
This makes any allowlisted Python/Node executable under the install or venv eligible even if it is running a non-Hermes user command. Please require a Hermes-specific command signature for generic runtimes (and add a negative test); current main intentionally aborts rather than killing an outside venv holder.
Summary
Fix the Windows desktop updater preflight so it actually finds and stops Hermes-owned runtime processes before handing off to the staged updater.
The desktop update path already tries to release backend/runtime locks before launching
hermes-setup.exe --update, but the Windows process scan built an invalid PowerShell command ($ErrorActionPreference = 'Stop' Get-CimInstance ...). That parse error was swallowed and returned an empty process list, so stray venv/gateway processes could keep.pyd/venv files locked and makeuv pip installfail withAccess is denied.This PR:
listWindowsProcesses()to include the current Node processnpm run smoke:desktop:update-handoff, that uses a disposableHERMES_HOME, a fake locked venv runtime, a staged fake updater, and the packaged desktop app to prove handoff/relaunch behavior end to endValidation
node --test electron/update-processes.test.cjs-> 8 passnpm run test:desktop:platforms-> 87 passnpx eslint electron/main.cjs electron/update-processes.cjs electron/update-processes.test.cjs electron/update-handoff-smoke.cjs --max-warnings=0git diff --check origin/main..HEAD; git diff --check.\venv\Scripts\python.exe -m hermes_cli.main desktop --build-onlynpm run smoke:desktop:update-handoffSmoke evidence from the latest run:
stopped 1 Hermes runtime process(es) before updatevenv shim unlocked; safe to hand off the update--update --branch mainNotes
The terminal-command fallback remains intentional for source/CLI installs that do not have
HERMES_HOME/hermes-setup.exestaged. Installer-backed desktop installs should take the automatic staged-updater handoff path.