fix(update): clean orphaned hermes shim entries from PendingFileRenameOperations - #85942
fix(update): clean orphaned hermes shim entries from PendingFileRenameOperations#85942Halldrix wants to merge 2 commits into
Conversation
On Windows `hermes update` can never complete. `hermes` is launched
through a distlib console-script launcher, which is not a thin redirect:
it spawns `venv\Scripts\python.exe` as a child, hands it the launcher's
own path as the script, and blocks. Every invocation is two processes:
hermes.exe (pid A, holds Scripts\hermes.exe as its running image)
\_ python.exe (pid B, runs hermes_cli.main)
The update ends in `uv pip install -e .`, which rewrites the console-
script shims -- including the one pid A has mapped. Windows refuses to
replace a running image, so uv fails with "The process cannot access the
file because it is being used by another process. (os error 32)". The
git path fails, the ZIP fallback fails identically, and no retry can
succeed: nothing pid B does releases the lock, because the holder is its
parent.
Remove the hazard rather than race it. Before the update touches
anything, re-launch it as a detached `python -m hermes_cli.main`
grandchild and let pids B and A exit. The grandchild waits for the
launcher to disappear, then updates against an ordinary unlocked file:
hermes.exe (A) --> python.exe (B) --> python.exe (C, detached)
exit exit waits for A, then updates
Hooked into cmd_update after --check (which installs nothing) and before
the update lock is acquired: a trampolining parent that had already
written the lock marker would release it moments later while the
grandchild ran unlocked, since the grandchild's acquire() sees its
still-live parent as an ancestor and would run under a claim about to
vanish.
stdin is DEVNULL so the detached updater cannot race the reclaimed shell
for keystrokes. That also selects the existing non-interactive path, so
local changes follow updates.non_interactive_local_changes (default
stash) rather than prompting where nobody can answer.
Off Windows maybe_trampoline() returns False immediately and behavior is
unchanged; POSIX replaces a running executable's inode atomically.
Verified on Windows 11 / Python 3.11.15 / uv, non-elevated: before, four
consecutive failed updates; after, the launcher exits, the grandchild
completes the update, and no os error 32 occurs on either path.
Scoped deliberately to the trampoline. The residual case where another
process holds a shim, and cleanup of the reboot-deferred rename entries
that case queues, are addressed by NousResearch#68821 and NousResearch#85942 respectively and
are not duplicated here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGLv7LyNrpaPm4ciGqhyxD
fix(update): clean orphaned hermes shim entries from PendingFileRenameOperations Targeted, well-documented fix for a nasty Windows footgun. Observations:
|
…eOperations `_schedule_replace_on_reboot` queues `MoveFileExW(MOVEFILE_DELAY_UNTIL_REBOOT)` pairs into the Session Manager registry so a locked `hermes.exe` can be renamed aside on next boot. Neither success nor failure of the subsequent recovery install removes the queued entries, so across repeated failed boot-recoveries they accumulate (one per failed boot — the NousResearch#85839 report counted 12). On the next reboot the Session Manager applies entry #1, renaming the current, healthy `hermes.exe` to a `.old.` backup — the shim vanishes after a reboot that was supposed to fix things. Add `_cleanup_pending_file_rename_operations()` (called from `_cleanup_quarantined_exes` on every launch) that scans the `PendingFileRenameOperations` registry value and removes hermes-shim pairs whose source file no longer exists (the shim was rewritten by a later install — the pending rename is a booby trap) or whose `.old.` target backup no longer exists (stale pair from a failed cycle). Non-hermes entries are left untouched. Fixes NousResearch#85839 🛠️ Dev: Halldrix 🤖 Sidekick: Hermes Agent v0.20.0
0f3879c to
5fdd264
Compare
- Read-first pattern: open registry with KEY_READ, only re-open with KEY_WRITE when changes exist (avoids Access Denied on non-elevated users) - Disarm 'armed' pairs: healthy hermes*.exe (no .old.) + .old. target is the NousResearch#85839 booby trap — remove it once install is confirmed good - Test-safety: mock winreg in integration test so it never touches real registry - Add test for armed-pair category (both source and target exist)
|
What does this PR do?
_schedule_replace_on_rebootqueuesMoveFileExW(MOVEFILE_DELAY_UNTIL_REBOOT)pairs into the Session Manager registry so a lockedhermes.execan be renamed aside on next boot. Neither success nor failure of the subsequent recovery install removes the queued entries, so across repeated failed boot-recoveries they accumulate (one per failed boot — the #85839 report counted 12). On the next reboot the Session Manager applies entry #1, renaming the current, healthyhermes.exeto a.old.backup — the shim vanishes after a reboot that was supposed to fix things.Add
_cleanup_pending_file_rename_operations()(called from_cleanup_quarantined_exeson every launch) that scans thePendingFileRenameOperationsregistry value and removes hermes-shim pairs whose source file no longer exists (the shim was rewritten by a later install — the pending rename is a booby trap) or whose.old.target backup no longer exists (stale pair from a failed cycle). Also disarms "armed" pairs where the source is the current healthyhermes*.exe(no.old.) and the target is a.old.backup — this is the booby trap that would destroy the healthy shim on next boot. Once we're running, the install is good; disarm it.Improvements over original:
KEY_READ, only re-opens withKEY_WRITEwhen changes exist — avoids Access Denied on every launch for non-elevated Windows users.winregso it never touches the real registry on Windows dev machines.Related Issue
Fixes #85839
Type of Change
Changes Made
hermes_cli/main.py— new_cleanup_pending_file_rename_operations()function with read-first pattern + armed-pair disarm logic + call from_cleanup_quarantined_exestests/hermes_cli/test_update_pending_rename_cleanup.py— 11 new tests (armed pair, test-safety mock, read-first verified)How to Test
Sabotage-run: with the fix stashed, all 11 tests fail (11/11 red); with the fix restored they all pass. Verified locally before opening.
Evidence
The #85839 report counted 12 accumulated
PendingFileRenameOperationsentries from repeated failed boot-recoveries. On the next reboot the Session Manager applied the first entry, renaming the healthyhermes.exe→hermes.exe.old.<ts>, making the shim vanish after a reboot that was supposed to fix things.Checklist
🛠️ Dev: Halldrix
🤖 Sidekick: Hermes Agent v0.20.0