fix(update): stop Windows updates from locking their own launcher - #90192
Merged
Conversation
`uv venv` writes `.venv` while our installers write `venv`, and every venv lookup in the update/repair paths hardcoded `venv`. On a `.venv` install `_venv_scripts_dir()` returned None, so the Windows shim-lock preflight, the quarantine, and the console-script verification all silently skipped themselves — the update walked straight into the failure they exist to catch. Adds `hermes_constants.project_venv_dir()` as the single resolver and routes both `_venv_scripts_dir()` implementations plus the two VIRTUAL_ENV call sites through it. Refs #79542
`hermes update` launched as venv\Scripts\hermes.exe can never finish on Windows. The launcher runs the interpreter with the shim as its script and holds it open without FILE_SHARE_DELETE for the whole command, so the quarantine rename is refused and uv fails to replace hermes.exe with os error 32 — every time, with no Desktop, gateway or AV involved. The concurrent-instance preflight cannot catch it because it excludes this process and its ancestors by design. Detect the shim from both the process ancestry and this process's own launch paths (argv[0], __main__.__file__, the spec origin — the runpy/zipapp launch puts <shim>\__main__.py there), intersected with the project venv's shims so an unrelated hermes.exe never matches. When it matches, re-run the same argv as `venv\Scripts\python.exe -m hermes_cli.main ...` and return, which releases the shim before the child installs anything. The hand-off sits ahead of the update lock so the child claims the marker itself rather than adopting one the parent immediately releases, and any failure falls through to the previous in-process behaviour with the manual command printed. Refs #88838, #89599, #86093
MOVEFILE_DELAY_UNTIL_REBOOT was the quarantine's last resort, and it is worse than doing nothing. It writes to HKLM, so a non-elevated update — every Desktop-driven one, and most terminal ones — gets ERROR_ACCESS_DENIED and reports nothing. When it does succeed it frees nothing for the install running right now, and the queued operation outlives that update: at the next boot it moves aside whatever sits at the shim path, including a shim a later repair just wrote. Drops the fallback and sweeps entries older versions queued, matching only our own <shim> -> <shim>.old.<stamp> pairs so unrelated installers keep theirs. Salvaged from #88121 by @fangliquanflq.
The Windows branch spawned the updater as `hermes.exe update --gateway`, so the update held the very shim it had to replace and failed with os error 32. Invoke it as `python -m hermes_cli.main update --gateway` under the same interpreter the gateway already runs, which maps no shim. Salvaged from #89970 by @Akloenx123.
Detection across every launch variant (argv[0], the zipapp __main__.py, the main-module spec origin, the ancestor chain) plus the venv scoping that keeps an unrelated hermes.exe from triggering a hand-off; the re-exec's argv, env marker, loop guard and both fall-through paths; the pending-rename filter; and the venv/.venv layout split. Retires the reboot-deferred quarantine assertion along with the fallback. Launch-variant cases from #89970 by @Akloenx123, pending-rename cases from #88121 by @fangliquanflq.
Contributor
૮ >ﻌ< ა ci reviewran on 268615c — test(update): cover the Windows shim self-lock class
|
13 tasks
This was referenced Aug 19, 2026
3 tasks
6 tasks
bobaba76
pushed a commit
to bobaba76/hermes-agent
that referenced
this pull request
Aug 27, 2026
…date-shim-self-lock fix(update): stop Windows updates from locking their own launcher
melon-xf
added a commit
to melon-xf/hermes-agent
that referenced
this pull request
Sep 3, 2026
…date-shim-self-lock fix(update): stop Windows updates from locking their own launcher
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
On Windows,
hermes updatestarted fromvenv\Scripts\hermes.execan never succeed. The launcher runs the interpreter with the shim as its script and holds it open withoutFILE_SHARE_DELETEfor the whole command, so the dependency sync has to replace a file it is itself holding: the quarantine rename is refused and uv fails withos error 32. No Desktop, gateway or antivirus involvement is required, and the concurrent-instance preflight cannot catch it because it excludes this process and its ancestors by design. The ZIP fallback repeats the same sequence, so the update reports progress while the editable install stays stale.This consolidates the two open attempts at the bug into one fix and closes the gaps neither covered.
What changed
The update re-runs itself off the shim. When
cmd_updatedetects that it was launched through one of this venv's console shims, it re-runs the same argv asvenv\Scripts\python.exe -m hermes_cli.main ...and returns, releasing the shim before the child installs anything. Detection reads both the process ancestry and this process's own launch paths —sys.argv[0],__main__.__file__, the module spec origin — because the runpy/zipapp launch puts<shim>\__main__.pythere and an argv[0] check alone misses it. Candidates are intersected with the venv's own shims, so ahermes.exefrom another install never triggers a hand-off.The hand-off runs ahead of the update lock. Placing it later means the child adopts the parent's marker by ancestry and the parent then releases it, leaving the update running unlocked.
The reboot-deferred rename is gone.
MOVEFILE_DELAY_UNTIL_REBOOTwas the quarantine's last resort and it is worse than doing nothing: it writes to HKLM, so every non-elevated update getsERROR_ACCESS_DENIEDsilently, and when it does land it frees nothing for the install in flight while queueing an operation that moves aside whatever sits at the shim path at next boot — including a shim a later repair just wrote. Entries queued by older versions are now swept, matching only our own<shim>→<shim>.old.<stamp>pairs so other installers keep theirs.Gateway
/updateno longer goes through the shim on Windows; it runs as a module under the interpreter the gateway already uses.The venv is resolved as
venvor.venv.uv venvwrites.venvand our installers writevenv, but every lookup hardcodedvenv. On a.venvinstall_venv_scripts_dir()returnedNone, which silently disabled the shim preflight, the quarantine and the console-script verification — so any fix gated on it would have been a no-op there.Verification
47 tests across the update/quarantine/recovery suites pass, including 21 new ones covering every launch variant, the venv scoping, the re-exec's argv and env marker, its loop guard and both fall-through paths, the pending-rename filter, and the
venv/.venvsplit.ruffand the blocking Windows-footgun lint are clean on the diff.Trade-off
The process the shell waited on exits as soon as the child is spawned, so
hermes update's exit code reflects the hand-off rather than the update. A synchronous wait is not available here — this process exiting is the fix. The update prints its own result, and--gatewaywrites the true exit code to.update_exit_codebefore the gateway restart, which the watcher still reads. Anything that blocks the hand-off (no venv python, spawn refused) falls through to the previous in-process behaviour with the manual command printed, so a broken venv still gets whatever the update can do.Credit
Supersedes #89970 and #88121, whose authors each had a piece of this. The automatic re-exec, the launch-variant detection and the gateway spawn fix come from @Akloenx123's #89970. Removing the reboot-deferred rename and sweeping the stale pending-rename entries come from @fangliquanflq's #88121. @jrleal10 reproduced the failure on native Windows 11 on both
mainand #88121, which is what made the shared root cause legible.Supersedes #89970, #88121
Fixes #88838, #89599, #86093, #88078
Refs #79542, #89295, #79561