From e1ccd674c0950912c86cc1f794eb6ccecc72d126 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 1 Aug 2026 20:43:08 -0500 Subject: [PATCH] fix(desktop): apply the stale-installer marker guard to both hand-offs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both hand-off sites pre-write the update marker: the in-app Update button (applyUpdates) and the Windows bootstrap-recovery path (handOffWindowsBootstrapRecovery). Either one can strand a user on a pre-#74782 staged installer, and the recovery path is worse — it fires when the install is already unhealthy, so a refused claim there wedges the very repair meant to heal it. Route both through stagedUpdaterSupportsPrewrittenMarker and log the skip so the reason is visible in desktop.log instead of looking like a missing write. Also document on copy_self_to_hermes_home that its --update no-op is what lets an installer-protocol change strand the entire installed base on a binary that predates it — the root enabler of this class of bug. --- .../src-tauri/src/paths.rs | 6 ++++ apps/desktop/electron/main.ts | 30 ++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/apps/bootstrap-installer/src-tauri/src/paths.rs b/apps/bootstrap-installer/src-tauri/src/paths.rs index 0eec8ccd31..3a7b1b0dbf 100644 --- a/apps/bootstrap-installer/src-tauri/src/paths.rs +++ b/apps/bootstrap-installer/src-tauri/src/paths.rs @@ -98,6 +98,12 @@ pub fn update_in_progress_marker() -> PathBuf { /// that path), where copying onto ourselves would be a Windows sharing /// violation. Best-effort: a failure here must not fail the install, so the /// caller logs and continues. +/// +/// NOTE: because of that no-op, a user's staged installer is only ever written +/// by a full install/repair. Every later `--update` runs the ORIGINAL binary, +/// so an installer-protocol change can strand the whole installed base on a +/// binary that predates it (see `restage_from_checkout`, which repairs this +/// from the freshly-updated checkout). pub fn copy_self_to_hermes_home() -> std::io::Result<()> { let src = std::env::current_exe()?; let dest = installer_dest(); diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 785a1dc96e..e0d43be1f6 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -201,7 +201,11 @@ import { sandboxPreflight } from './update-relaunch' import { isOfficialSshRemote, OFFICIAL_REPO_HTTPS_URL } from './update-remote' -import { resolveStagedUpdaterBinary, spawnUpdaterProcess } from './updater-process' +import { + resolveStagedUpdaterBinary, + spawnUpdaterProcess, + stagedUpdaterSupportsPrewrittenMarker +} from './updater-process' import { formatBlockerMessage, formatProbeFailedMessage, scanVenvBlockers } from './venv-blocker-scan' import { fetchMarketplaceThemes, searchMarketplaceThemes } from './vscode-marketplace' import { createWakeIndicatorWindowController } from './wake-indicator-window' @@ -3012,8 +3016,20 @@ async function applyUpdates(opts = {}) { // the venv. By writing the marker ourselves the renderer's // waitForUpdateToFinish() gate sees a live update and parks instead. // The updater overwrites this with its own PID later; same format. - if (Number.isInteger(child.pid)) { + // + // SKIPPED for pre-#74782 staged updaters: those have no self-PID + // exclusion, so they read this very marker as a foreign live owner and + // abort with "Another Hermes update is already running (PID )" — + // an unbreakable loop, because the update that would replace the stale + // binary is the one being refused. Losing the anti-respawn hardening is + // strictly better than never updating again, and the updater still writes + // its own marker moments later. + if (Number.isInteger(child.pid) && stagedUpdaterSupportsPrewrittenMarker(updater)) { writeUpdateMarker(HERMES_HOME, child.pid) + } else if (Number.isInteger(child.pid)) { + rememberLog( + `[updates] skipping marker pre-write: staged updater predates self-adopt (${updater}); it would refuse its own claim` + ) } rememberLog(`[updates] launched updater: ${updater} ${updaterArgs.join(' ')}; exiting desktop to release venv shim`) @@ -3100,9 +3116,15 @@ async function handOffWindowsBootstrapRecovery(reason) { // Same marker pre-write as applyUpdates — see comment there. The recovery // hand-off has the same window where the renderer can respawn a backend - // before the updater writes its own marker. - if (Number.isInteger(child.pid)) { + // before the updater writes its own marker, and the same stale-updater + // exclusion: a pre-#74782 binary would refuse its own pre-written claim and + // strand the very recovery meant to heal the install. + if (Number.isInteger(child.pid) && stagedUpdaterSupportsPrewrittenMarker(updater)) { writeUpdateMarker(HERMES_HOME, child.pid) + } else if (Number.isInteger(child.pid)) { + rememberLog( + `[bootstrap] skipping marker pre-write: staged updater predates self-adopt (${updater}); it would refuse its own claim` + ) } rememberLog(