Skip to content

fix(update): stop Windows updates from locking their own launcher - #90192

Merged
OutThisLife merged 5 commits into
mainfrom
bb/windows-update-shim-self-lock
Aug 19, 2026
Merged

fix(update): stop Windows updates from locking their own launcher#90192
OutThisLife merged 5 commits into
mainfrom
bb/windows-update-shim-self-lock

Conversation

@OutThisLife

Copy link
Copy Markdown
Collaborator

On Windows, hermes update started from venv\Scripts\hermes.exe can never succeed. 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 dependency sync has to replace a file it is itself holding: the quarantine rename is refused and uv fails with os 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_update detects that it was launched through one of this venv's console shims, it re-runs the same argv as venv\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__.py there and an argv[0] check alone misses it. Candidates are intersected with the venv's own shims, so a hermes.exe from 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_REBOOT was the quarantine's last resort and it is worse than doing nothing: it writes to HKLM, so every non-elevated update gets ERROR_ACCESS_DENIED silently, 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 /update no longer goes through the shim on Windows; it runs as a module under the interpreter the gateway already uses.

The venv is resolved as venv or .venv. uv venv writes .venv and our installers write venv, but every lookup hardcoded venv. On a .venv install _venv_scripts_dir() returned None, 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/.venv split. ruff and 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 --gateway writes the true exit code to .update_exit_code before 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 main and #88121, which is what made the shared root cause legible.

Supersedes #89970, #88121
Fixes #88838, #89599, #86093, #88078
Refs #79542, #89295, #79561

`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.
@OutThisLife OutThisLife added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery platform/windows Native Windows-specific behavior or breakage area/install-update Installer, updater, packaging, wheels, doctor P2 Medium — degraded but workaround exists labels Aug 19, 2026
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 268615c — test(update): cover the Windows shim self-lock class

⚠️ Warnings

OSV vulnerability scan · View job

7 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.


debug info

CI timings

CI timings · View report · View job

Wall time 5m2s vs 5m34s (-9.6%). 8 job(s) slower, 15 faster, 1 unchanged.

  • Python tests / Run tests slice 2/12: -106.0s
  • Python tests / Run tests slice 12/12: -63.0s
  • Python tests / Run tests slice 3/12: -43.0s
  • Python tests / Run tests slice 1/12: -43.0s
  • Python tests / Run tests slice 7/12: -31.0s

@OutThisLife
OutThisLife merged commit 73a1d58 into main Aug 19, 2026
50 checks passed
@OutThisLife
OutThisLife deleted the bb/windows-update-shim-self-lock branch August 19, 2026 18:25
@alt-glitch alt-glitch added sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 19, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/install-update Installer, updater, packaging, wheels, doctor comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/windows Native Windows-specific behavior or breakage sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows: hermes update self-locks its own console-script launcher; ZIP fallback masks it and leaves the venv editable install stale

2 participants