fix(exit): diagnostics after the terminal may be gone are best-effort writes
With fd 2 dead (a closed pane, an exited pipe reader) every write fails, eprintln! panics on that failure, and under panic = "abort" the process dies by SIGABRT with a crash report on the next launch instead of its exit code. Exit reports, headless plain-format diagnostics, update-exit, setup-result and leader-management messages now write through best_effort_stderr::eprint_line: one attempt, failure reported, never a panic. Ported from upstream 1.0.19 (grok-build best_effort_stderr.rs). Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
This commit is contained in:
1 parent
901203c2b2
commit
17d4ba08f9
4 files changed
+95
-35
No files matched your search
@@ -0,0 +1,33 @@
|
||||
//! Stderr lines for paths where fd 2 may be dead: a closed pane fails every write, `eprintln!` panics on it, and under `panic = "abort"` that is a SIGABRT plus a crash report.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
/// Write `line` and a newline to `w`; `false` when the write failed.
|
||||
pub fn write_line(w: &mut impl Write, line: &str) -> bool {
|
||||
writeln!(w, "{line}").is_ok()
|
||||
}
|
||||
|
||||
/// [`write_line`] to process stderr, outcome discarded.
|
||||
pub fn eprint_line(line: &str) {
|
||||
write_line(&mut std::io::stderr(), line);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn write_line_appends_a_newline_and_reports_success() {
|
||||
let mut buf = Vec::new();
|
||||
assert!(write_line(&mut buf, "Finishing session…"));
|
||||
assert_eq!(buf, "Finishing session…\n".as_bytes());
|
||||
}
|
||||
|
||||
/// A closed read end fails every write with EPIPE, like the dead tty a closed pane leaves.
|
||||
#[test]
|
||||
fn write_line_reports_failure_on_a_dead_pipe() {
|
||||
let (reader, mut writer) = std::io::pipe().expect("pipe");
|
||||
drop(reader);
|
||||
assert!(!write_line(&mut writer, "unreachable"));
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
//! `spawn_kigi_shell`, sends the ACP lifecycle (init → auth → session → prompt),
|
||||
//! streams text to stdout, and exits cleanly via `CancellationToken`.
|
||||
|
||||
use crate::best_effort_stderr::eprint_line;
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -459,7 +460,7 @@ impl HeadlessEmitter {
|
||||
|
||||
fn on_error(&self, message: &str) {
|
||||
match self.format {
|
||||
OutputFormat::Plain => eprintln!("{message}"),
|
||||
OutputFormat::Plain => eprint_line(message),
|
||||
OutputFormat::StreamingJson | OutputFormat::Json => {
|
||||
let mut err = serde_json::json!({"type":"error","message": message});
|
||||
if let Some(usage) = &self.usage {
|
||||
@@ -1276,7 +1277,7 @@ pub async fn run_single_turn(
|
||||
== Some("max_turns_reached");
|
||||
if is_max_turns {
|
||||
match emitter.format {
|
||||
OutputFormat::Plain => eprintln!("Max turns reached"),
|
||||
OutputFormat::Plain => eprint_line("Max turns reached"),
|
||||
OutputFormat::StreamingJson => {
|
||||
println!("{}", serde_json::json!({"type": "max_turns_reached"}))
|
||||
}
|
||||
@@ -1644,7 +1645,9 @@ fn handle_ext_notification(
|
||||
);
|
||||
}
|
||||
OutputFormat::Plain => {
|
||||
eprintln!("Auto-compacting conversation ({percentage}% full)...");
|
||||
eprint_line(&format!(
|
||||
"Auto-compacting conversation ({percentage}% full)..."
|
||||
));
|
||||
}
|
||||
OutputFormat::Json => {}
|
||||
},
|
||||
@@ -1652,7 +1655,7 @@ fn handle_ext_notification(
|
||||
OutputFormat::StreamingJson => {
|
||||
println!("{}", serde_json::json!({"type": "auto_compact_completed"}));
|
||||
}
|
||||
OutputFormat::Plain => eprintln!("Conversation compacted."),
|
||||
OutputFormat::Plain => eprint_line("Conversation compacted."),
|
||||
OutputFormat::Json => {}
|
||||
},
|
||||
XaiUpdate::AutoCompactFailed { error } => match format {
|
||||
@@ -1664,9 +1667,9 @@ fn handle_ext_notification(
|
||||
}
|
||||
OutputFormat::Plain => {
|
||||
if error.trim().is_empty() {
|
||||
eprintln!("Auto-compact failed.");
|
||||
eprint_line("Auto-compact failed.");
|
||||
} else {
|
||||
eprintln!("Auto-compact failed: {error}");
|
||||
eprint_line(&format!("Auto-compact failed: {error}"));
|
||||
}
|
||||
}
|
||||
OutputFormat::Json => {}
|
||||
@@ -1675,7 +1678,7 @@ fn handle_ext_notification(
|
||||
OutputFormat::StreamingJson => {
|
||||
println!("{}", serde_json::json!({"type": "auto_compact_cancelled"}));
|
||||
}
|
||||
OutputFormat::Plain => eprintln!("Auto-compact cancelled."),
|
||||
OutputFormat::Plain => eprint_line("Auto-compact cancelled."),
|
||||
OutputFormat::Json => {}
|
||||
},
|
||||
XaiUpdate::AutoContinueCompleted { total_tokens } => match format {
|
||||
@@ -1685,7 +1688,7 @@ fn handle_ext_notification(
|
||||
serde_json::json!({"type": "auto_continue_completed", "total_tokens": total_tokens})
|
||||
);
|
||||
}
|
||||
OutputFormat::Plain => eprintln!("Resumed after compaction."),
|
||||
OutputFormat::Plain => eprint_line("Resumed after compaction."),
|
||||
OutputFormat::Json => {}
|
||||
},
|
||||
XaiUpdate::ImageCompressed { message } => match format {
|
||||
@@ -1695,7 +1698,7 @@ fn handle_ext_notification(
|
||||
serde_json::json!({"type": "image_compressed", "message": message})
|
||||
);
|
||||
}
|
||||
OutputFormat::Plain => eprintln!("{message}"),
|
||||
OutputFormat::Plain => eprint_line(&message),
|
||||
OutputFormat::Json => {}
|
||||
},
|
||||
XaiUpdate::SubagentSpawned { subagent_id } => {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
pub mod acp;
|
||||
pub mod actions;
|
||||
pub mod app;
|
||||
pub mod best_effort_stderr;
|
||||
pub mod client_identity;
|
||||
pub mod completions_cmd;
|
||||
mod config_toml_edit;
|
||||
|
||||
Reference in new issue
Block a user