fix(install): heal the updater's self-owned-marker refusal loop on Windows
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 <noreply@anthropic.com>
This commit is contained in:
@@ -156,6 +156,24 @@ fn live_marker_owner(path: &Path) -> Option<MarkerOwner> {
|
||||
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::<u32>().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 {
|
||||
|
||||
Reference in New Issue
Block a user