fix(desktop-update): drive the Windows hand-off through the venv python, 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>
This commit is contained in:
@@ -672,23 +672,55 @@ try {
|
||||
# is unlocked; the venv-python holder guard (orphan reap included) stays
|
||||
# active. Our marker claim is adopted by the child via update_lock.py's
|
||||
# process-ancestry rule.
|
||||
$hermesExe = Join-Path $InstallRoot "venv\Scripts\hermes.exe"
|
||||
if (-not (Test-Path -LiteralPath $hermesExe)) {
|
||||
#
|
||||
# DRIVE THE UPDATE THROUGH venv\Scripts\python.exe, NOT venv\Scripts\hermes.exe.
|
||||
# `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. On Windows that rename fails whenever ANY child process
|
||||
# spawned from that hermes.exe is still alive: a child inherits a handle on
|
||||
# the parent image, and the resulting sharing violation is indistinguishable
|
||||
# from a user leaving a second Hermes window open. It is the inherited
|
||||
# handle, not the trampoline itself, that pins the file -- killing the child
|
||||
# makes the same rename succeed immediately, and the shim flavour (uv
|
||||
# trampoline vs distlib launcher) makes no difference.
|
||||
#
|
||||
# The updater reliably 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 is a race, not a
|
||||
# deterministic failure: the same hand-off succeeds on one run and dies on
|
||||
# the next. Step 2's preflight cannot catch it, because the shim genuinely
|
||||
# IS unlocked at that moment.
|
||||
#
|
||||
# When the rename loses that race, _schedule_replace_on_reboot is the last
|
||||
# resort -- and it writes to HKLM\...\PendingFileRenameOperations, which
|
||||
# requires elevation. A Desktop-driven update runs non-elevated, so it
|
||||
# returns ERROR_ACCESS_DENIED and `uv pip install -e .` exits 2. The ZIP
|
||||
# fallback repeats the identical sequence, so the desktop build stage is
|
||||
# never reached and apps/desktop/release is left missing -- 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 left alone: unlinking a running executable is
|
||||
# legal there, so the equivalent call is harmless.
|
||||
$pythonExe = Join-Path $InstallRoot "venv\Scripts\python.exe"
|
||||
if (-not (Test-Path -LiteralPath $pythonExe)) {
|
||||
$finalCode = 3
|
||||
$finalMsg = "Update aborted: $hermesExe is missing. The install needs repair (run the Hermes installer or `hermes doctor`)."
|
||||
$finalMsg = "Update aborted: $pythonExe is missing. The install needs repair (run the Hermes installer or `hermes doctor`)."
|
||||
Write-HandoffLog $finalMsg
|
||||
exit $finalCode
|
||||
}
|
||||
$updateArgs = @("update", "--yes", "--gateway", "--force", "--branch", $Branch)
|
||||
Write-HandoffLog ("running: hermes " + ($updateArgs -join " "))
|
||||
$res = Invoke-HermesStep $hermesExe $updateArgs "update"
|
||||
$updateArgs = @("-m", "hermes_cli.main", "update", "--yes", "--gateway", "--force", "--branch", $Branch)
|
||||
Write-HandoffLog ("running: python " + ($updateArgs -join " "))
|
||||
$res = Invoke-HermesStep $pythonExe $updateArgs "update"
|
||||
Write-HandoffLog "hermes update exit code: $($res.Code)"
|
||||
|
||||
if ($res.Code -ne 0 -and $res.Code -ne 2) {
|
||||
# One retry for the update-boundary class (fresh code on disk, stale
|
||||
# code in memory). Exit 2 ("close all Hermes windows") is not retryable.
|
||||
Write-HandoffLog "first attempt failed; retrying once (freshly pulled fix loads on the second run)"
|
||||
$res = Invoke-HermesStep $hermesExe $updateArgs "update"
|
||||
$res = Invoke-HermesStep $pythonExe $updateArgs "update"
|
||||
Write-HandoffLog "retry exit code: $($res.Code)"
|
||||
}
|
||||
|
||||
@@ -700,7 +732,7 @@ try {
|
||||
$desktopBuildFailed = $false
|
||||
if ($res.Code -eq 0 -and $res.Output -match "Desktop build failed") {
|
||||
Write-HandoffLog "hermes update reported a desktop build failure (non-fatal there, fatal here); retrying build"
|
||||
$rebuild = Invoke-HermesStep $hermesExe @("desktop", "--force-build", "--build-only") "rebuild"
|
||||
$rebuild = Invoke-HermesStep $pythonExe @("-m", "hermes_cli.main", "desktop", "--force-build", "--build-only") "rebuild"
|
||||
Write-HandoffLog "desktop rebuild exit code: $($rebuild.Code)"
|
||||
if ($rebuild.Code -ne 0) { $desktopBuildFailed = $true }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user