diff --git a/Cargo.lock b/Cargo.lock index 88c8eb6c..066ec58f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1740,6 +1740,7 @@ dependencies = [ "anyhow", "clap", "forgejo-api", + "libc", "reqwest", "serde", "serde_json", diff --git a/hive-forge/Cargo.toml b/hive-forge/Cargo.toml index e8bae060..6fd9de5b 100644 --- a/hive-forge/Cargo.toml +++ b/hive-forge/Cargo.toml @@ -38,6 +38,9 @@ reqwest = { workspace = true, features = [ ] } serde = { workspace = true } serde_json = { workspace = true } +# Only for restoring the default SIGPIPE disposition at startup — see +# `restore_sigpipe` in src/main.rs. std has no safe API for it. +libc = { workspace = true } [lints] workspace = true diff --git a/hive-forge/src/main.rs b/hive-forge/src/main.rs index d423a665..5a04be79 100644 --- a/hive-forge/src/main.rs +++ b/hive-forge/src/main.rs @@ -207,6 +207,7 @@ enum Verb { /// (alternate `Display`), which keeps the full `context` chain inline /// rather than dropping it the way plain `Display` would. fn main() -> ExitCode { + restore_sigpipe(); if let Err(e) = run() { eprintln!("hive-forge: FAILED: {e:#}"); return ExitCode::FAILURE; @@ -214,6 +215,26 @@ fn main() -> ExitCode { ExitCode::SUCCESS } +/// Give `SIGPIPE` back its default disposition before any output happens. +/// +/// Rust's runtime sets `SIGPIPE` to `SIG_IGN` at startup, so writing to a +/// pipe whose reader has gone away returns `EPIPE` and `println!` panics. +/// `hive-forge | head` therefore dies with a panic message and exit +/// **101** where every other CLI in a pipeline exits quietly — and 101 is +/// a real failure to a caller running `set -o pipefail`, so a shell script +/// that pipes our output stops on a condition that is not an error. +/// +/// Restoring `SIG_DFL` makes the process die by signal instead, which is +/// what the surrounding shell already knows how to interpret. +fn restore_sigpipe() { + // SAFETY: called first thing in `main`, before any thread exists and + // before any output — so no concurrent handler mutation is possible, + // and no write can race the change. + unsafe { + libc::signal(libc::SIGPIPE, libc::SIG_DFL); + } +} + fn run() -> Result<()> { let cli = Cli::parse(); let verb = cli.verb;