fix(bootstrap): show the update child's exit-2 refusal instead of "still running"

Every exit 2 from `hermes update` was reported as "Hermes is still
running. Close all Hermes windows", including when the holder was another
live update. The child prints the specific refusal as a ✗ block right
before exiting; keep a bounded stdout tail and show that block, falling
back to the generic text only when none was captured.

Co-authored-by: RelaxJonh <92573950+RelaxJonh@users.noreply.github.com>
This commit is contained in:
brooklyn!
2026-09-24 03:25:12 -05:00
parent db114c2504
commit edecebf69d

View File

@@ -22,6 +22,7 @@
//! the no-window creation flag — both already cfg-gated. Keep new logic
//! OS-agnostic so the mac/linux port stays "fill in the paths".
use std::collections::VecDeque;
use std::env;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
@@ -499,9 +500,7 @@ async fn run_update(app: AppHandle) -> Result<()> {
emit_stage(&app, "update", StageState::Succeeded, Some(update_ms), None);
}
Some(code) if code == UPDATE_EXIT_CONCURRENT => {
let msg = "Hermes is still running. Close all Hermes windows and try \
the update again."
.to_string();
let msg = concurrent_update_message(&update.stdout_tail);
emit_stage(
&app,
"update",
@@ -931,6 +930,27 @@ fn rebuild_needs_retry(exit_code: Option<i32>) -> bool {
exit_code != Some(0)
}
/// Lines of child stdout kept for the exit-2 diagnosis. Every refusal block
/// is a handful of lines printed right before `sys.exit(2)`.
const STDOUT_TAIL_LINES: usize = 40;
/// User-facing message for an exit-2 refusal from `hermes update`.
///
/// Exit 2 has several causes (another update holds the marker, a live
/// hermes.exe or venv holder, a self-mapped `.pyd` after the code swap), and
/// the child always prints the specific one as a block starting with `✗`
/// just before exiting. Show that block — it names the real holder/PID — and
/// fall back to the generic "still running" text only when none was captured
/// (#78135).
fn concurrent_update_message(stdout_tail: &[String]) -> String {
match stdout_tail.iter().rposition(|l| l.trim_start().starts_with('✗')) {
Some(start) => stdout_tail[start..].join("\n").trim().to_string(),
None => "Hermes is still running. Close all Hermes windows and try \
the update again."
.to_string(),
}
}
/// Spawn `hermes <args>` from `cwd`, stream stdout/stderr as Log events on the
/// bootstrap channel, and return the exit code. Mirrors powershell::run_script
/// but for an arbitrary command (no install.ps1 -File wrapping).
@@ -942,6 +962,8 @@ async fn run_streamed(
envs: &[(String, OsString)],
stage: Option<&str>,
) -> Result<CmdResult> {
let mut stdout_tail: VecDeque<String> = VecDeque::with_capacity(STDOUT_TAIL_LINES);
let mut cmd = Command::new(program);
cmd.args(args)
.current_dir(cwd)
@@ -970,7 +992,13 @@ async fn run_streamed(
let stage_owned = stage.map(|s| s.to_string());
let outcome = pump_child(
&mut child,
|l| emit_log(app, stage_owned.as_deref(), LogStream::Stdout, l),
|l| {
emit_log(app, stage_owned.as_deref(), LogStream::Stdout, l);
if stdout_tail.len() == STDOUT_TAIL_LINES {
stdout_tail.pop_front();
}
stdout_tail.push_back(l.to_string());
},
|l| emit_log(app, stage_owned.as_deref(), LogStream::Stderr, l),
&mut None,
DRAIN_GRACE,
@@ -991,11 +1019,14 @@ async fn run_streamed(
Ok(CmdResult {
exit_code: outcome.exit_code,
stdout_tail: stdout_tail.into(),
})
}
struct CmdResult {
exit_code: Option<i32>,
/// Last [`STDOUT_TAIL_LINES`] stdout lines; see [`concurrent_update_message`].
stdout_tail: Vec<String>,
}
/// Path to the venv hermes shim under an install root, regardless of existence.
@@ -1433,6 +1464,34 @@ mod tests {
));
}
fn lines(text: &str) -> Vec<String> {
text.lines().map(str::to_string).collect()
}
#[test]
fn concurrent_update_message_shows_the_childs_refusal_block() {
// Exit 2 after the code swap: fetch/pull noise precedes the refusal,
// and only the refusal (which names the real holder) is the message.
let tail = lines(
"→ Fetching updates...\n\
✓ Updated to 6b2c23ae42\n\
✗ Another Hermes update is already running (started 3m 42s ago, process 65285).\n\
\n Wait for it to finish, then run `hermes update` again.\n",
);
assert_eq!(
concurrent_update_message(&tail),
"✗ Another Hermes update is already running (started 3m 42s ago, process 65285).\n\
\n Wait for it to finish, then run `hermes update` again."
);
}
#[test]
fn concurrent_update_message_falls_back_without_a_refusal_block() {
let generic = "Hermes is still running. Close all Hermes windows and try the update again.";
assert_eq!(concurrent_update_message(&[]), generic);
assert_eq!(concurrent_update_message(&lines("→ Fetching updates...\n")), generic);
}
#[test]
fn update_marker_guard_writes_then_removes_on_drop() {
let dir = unique_tmp_dir("marker-guard");