Skip to content

fix(desktop-update): complete Windows Desktop updates instead of self-locking the shim - #86326

Merged
OutThisLife merged 3 commits into
mainfrom
bb/win-update-shim-lock
Aug 14, 2026
Merged

fix(desktop-update): complete Windows Desktop updates instead of self-locking the shim#86326
OutThisLife merged 3 commits into
mainfrom
bb/win-update-shim-lock

Conversation

@OutThisLife

Copy link
Copy Markdown
Collaborator

On Windows, the in-app Desktop updater's hand-off (scripts/desktop-update/windows.ps1) drives hermes update through the venv's hermes.exe console-script launcher. That launcher keeps hermes.exe mapped as its running image and spawns python.exe as a child, so when the update finishes with uv pip install -e . -- which rewrites the console-script shims, including that same hermes.exe -- Windows refuses to replace a file mapped as a running image (os error 32). The rename fallback then defers to next reboot via MOVEFILE_DELAY_UNTIL_REBOOT, which needs elevation a Desktop-driven (non-elevated) update doesn't have, so uv pip install -e . exits non-zero, the ZIP fallback repeats the identical sequence, the desktop build stage is never reached, and the pre-build clean has already removed apps/desktop/release -- leaving an install whose Start Menu shortcut points at a Hermes.exe that no longer exists (the "my Desktop app disappeared" reports).

This runs the update as python.exe -m hermes_cli.main update instead, at all three Invoke-HermesStep call sites (the update, its retry, and the desktop rebuild). The inherited image handle then lands on python.exe, which uv never has to replace, so the shim is an ordinary unlocked file when uv rewrites it. scripts/desktop-update/posix.sh is intentionally untouched: unlinking a running executable is legal there.

Detection table

Symptom on Windows Cause After this PR
hermes update from Desktop fails every run with os error 32 on Scripts/hermes.exe update runs from the hermes.exe launcher, which holds the shim mapped update runs from python.exe; shim is unlocked when uv rewrites it
Update "succeeds" then Desktop app is gone after reboot reboot-deferred shim rename removes hermes.exe with no replacement written no reboot-deferred rename path taken; shim replaced in place
Looks flaky (succeeds once, fails next) race with updater-spawned children (npx warm, hindsight-api daemon) inheriting the shim handle children inherit the python.exe handle, not the shim

Worked example

Jango's thread: the update "doesn't finish," produces no completion, and the Desktop app disappears. logs/desktop-update-handoff.log shows hermes update exit code: 0 then 1 on runs 18 min apart on one box -- the only variable being whether a long-lived child was alive during the quarantine. Driving the same code as python.exe -m hermes_cli.main update completed with exit 0 on the first attempt after three consecutive shim-driven failures on that install, and the subsequent python.exe -m hermes_cli.main desktop --force-build --build-only rebuilt release/win-unpacked/Hermes.exe cleanly.

Tests

Adds tests/test_desktop_update_windows_python_handoff.py -- a source-level guard (Linux CI can't execute the PowerShell hand-off) asserting every Invoke-HermesStep call drives $pythonExe, the update/rebuild use -m hermes_cli.main, and Invoke-HermesStep $hermesExe never reappears. This fails on current main (three shim call sites) and passes here.

python -m pytest tests/test_desktop_update_windows_python_handoff.py -q
# 3 passed

Supersedes #85679 and #86101. Both authors landed on the same core insight (run the update off the shim, on python.exe); this takes #85679's surgical hand-off change as the base and keeps the footprint on the update path minimal. #86101's separate detached-grandchild trampoline (which also covers CLI-typed hermes update) is deliberately not included here -- it's a larger, independent change to the core update path and can land on its own merits if wanted.

Co-authored-by: Sascha Haase sascha.haase@textiletsg.com
Co-authored-by: adamcap926 adamcap926@users.noreply.github.com

Sascha and others added 2 commits August 14, 2026 15:27
…on, not the hermes.exe shim

`uv pip install -e .` has to replace the console-script shims, so
_quarantine_running_hermes_exe must first rename the running hermes.exe out
of the way. That rename fails whenever any child process spawned from that
hermes.exe is still alive: on Windows a child inherits a handle on the parent
image. It is the inherited handle, not the trampoline, that pins the file --
killing the child makes the identical rename succeed, and the shim flavour
(uv trampoline vs distlib launcher) makes no difference.

The updater spawns such children itself (npx cache warm, memory-provider
refresh -- hindsight-api runs as a daemon with --idle-timeout 300 and outlives
the step that started it), so this presents as a race rather than a hard
failure: the same hand-off succeeds on one run and dies on the next. Step 2's
shim-unlock preflight cannot catch it, because the shim genuinely is unlocked
at that moment; the pinning child appears later, during the update.

When the rename loses that race, _schedule_replace_on_reboot is the last
resort -- and MOVEFILE_DELAY_UNTIL_REBOOT writes to HKLM, so it needs
elevation. A Desktop-driven update is not elevated, so it returns
ERROR_ACCESS_DENIED, `uv pip install -e .` exits 2, and the ZIP fallback
repeats the identical sequence. The desktop build stage is then never reached
while the pre-build clean has already removed apps/desktop/release, leaving an
install whose Start Menu shortcut points at a Hermes.exe that no longer exists.

Running the same code as `python.exe -m hermes_cli.main update` puts the
inherited handles on python.exe, which uv never has to replace.

posix.sh is deliberately untouched: unlinking a running executable is legal
there, so the equivalent call is harmless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Source-level regression for the Windows Desktop update self-lock: assert every
Invoke-HermesStep call in scripts/desktop-update/windows.ps1 drives $pythonExe
via `python.exe -m hermes_cli.main`, never the hermes.exe shim. Driving the
update through the shim keeps hermes.exe mapped as a running image, so uv's
final `pip install -e .` shim rewrite fails with os error 32 and the update can
never complete. Runs on Linux CI (no PowerShell execution needed).

Co-authored-by: Sascha Haase <sascha.haase@textiletsg.com>
Co-authored-by: adamcap926 <adamcap926@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@OutThisLife, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3ac53156-85c0-4ccc-919e-3b66cd0f335f

📥 Commits

Reviewing files that changed from the base of the PR and between c59e30f and 944616e.

📒 Files selected for processing (3)
  • contributors/emails/sascha.haase@textiletsg.com
  • scripts/desktop-update/windows.ps1
  • tests/test_desktop_update_windows_python_handoff.py

Comment @coderabbitai help to get the list of available commands.

@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/desktop Electron desktop app (apps/desktop/*) platform/windows Native Windows-specific behavior or breakage area/install-update Installer, updater, packaging, wheels, doctor 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 14, 2026
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

running on 944616e — chore: map contributor email for the salvaged Windows update


waiting for more jobs to start…

⚠️ Warnings

OSV vulnerability scan · View job

5 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.

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/desktop Electron desktop app (apps/desktop/*) P1 High — major feature broken, no workaround 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.

2 participants