From 26c987c38dc0dc1252a9aded251bebdc35bccffa Mon Sep 17 00:00:00 2001 From: 3x3xX3N0N <300752012+3x3xX3N0N@users.noreply.github.com> Date: Mon, 3 Aug 2026 18:32:25 -0400 Subject: [PATCH] fix(install): heal the updater's self-owned-marker refusal loop on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An updater binary spawns 'hermes update' while holding the update marker with its own PID. A checkout that predates the HERMES_UPDATE_HANDOFF_PID env fix (8c76fe19f) and the ancestor-pid fallback runs its pre-pull update_lock.py, reads that marker as a live foreign update, and exits 2 — and the updater deliberately skips its retry for exit 2, so the refusal loops forever: the update being refused is the one that ships the fix, and the failure screen's Retry re-enters the same state. Detect the case with a raw marker read (live_marker_owner deliberately maps self-ownership to None, so it cannot answer this), drop our own claim, and retry the child once with the marker absent. The guard re-removes on Drop (idempotent) and the desktop is already gone at this point, so nothing races the brief marker-free window. Fixes #75788 Co-Authored-By: Claude Fable 5 --- .../src-tauri/src/update.rs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/apps/bootstrap-installer/src-tauri/src/update.rs b/apps/bootstrap-installer/src-tauri/src/update.rs index df2f0a1bc8..ee11cca232 100644 --- a/apps/bootstrap-installer/src-tauri/src/update.rs +++ b/apps/bootstrap-installer/src-tauri/src/update.rs @@ -156,6 +156,24 @@ fn live_marker_owner(path: &Path) -> Option { Some(MarkerOwner { pid, age_secs }) } +/// True when the on-disk marker names THIS process as its owner. +/// +/// `live_marker_owner` cannot answer this: it deliberately maps +/// self-ownership to `None` (the #74761 pre-write adoption). The exit-2 +/// self-heal below needs the raw fact, because a `hermes update` child that +/// refuses over OUR marker is a handoff-recognition failure, not a real +/// concurrent update. +fn marker_owned_by_self(path: &Path) -> bool { + std::fs::read_to_string(path) + .ok() + .and_then(|raw| { + raw.lines() + .next() + .and_then(|line| line.trim().parse::().ok()) + }) + == Some(std::process::id()) +} + /// True when a process with `pid` currently exists. #[cfg(windows)] fn pid_is_alive(pid: u32) -> bool { @@ -425,6 +443,40 @@ async fn run_update(app: AppHandle) -> Result<()> { ) .await?; } + + // Self-owned-marker heal (#75788). Exit 2 means the child refused over a + // live update marker with a foreign owner. When that "foreign" owner is + // THIS process, the child simply failed to recognize the handoff — a + // checkout predating the HERMES_UPDATE_HANDOFF_PID env fix (8c76fe19f) + // and the ancestor-pid fallback runs its pre-pull update_lock.py, reads + // our marker, and exits 2 every time. The refusal loop is unbreakable + // from the user's side because the update being refused is the one that + // ships the fix. The marker exists to serialize updates and this process + // IS the update: drop our claim and retry once with the marker absent. + // The guard re-removes on Drop (idempotent), and the desktop is already + // gone at this point, so nothing races the brief marker-free window. + if update.exit_code == Some(UPDATE_EXIT_CONCURRENT) + && marker_owned_by_self(&crate::paths::update_in_progress_marker()) + { + emit_log( + &app, + Some("update"), + LogStream::Stdout, + "[update] child refused over this updater's own marker (stale \ + checkout without handoff recognition); clearing the claim and \ + retrying once…", + ); + _update_marker.complete(); + update = run_streamed( + &app, + &hermes, + &update_args, + &install_root, + &child_env, + Some("update"), + ) + .await?; + } let update_ms = started.elapsed().as_millis() as u64; match update.exit_code {