hive-forge: die by SIGPIPE like every other CLI in a pipeline
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 <verb> | head` printed a panic and exited 101 where cat, grep and every other pipeline member exit quietly. 101 is not cosmetic: to a caller running `set -o pipefail` it is a real failure, so a shell script that pipes our output stops on a condition that is not an error. Restore SIG_DFL first thing in main, before any output. Measured against the same command: piped to head, 141 (killed by the signal) with empty stderr; unpiped, 0; the pre-change binary, 101 with a panic. Closes #3972
This commit is contained in:
parent
f293454df9
commit
e0c2f1aeaa
3 changed files with 25 additions and 0 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1740,6 +1740,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"forgejo-api",
|
"forgejo-api",
|
||||||
|
"libc",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ reqwest = { workspace = true, features = [
|
||||||
] }
|
] }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { 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]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,7 @@ enum Verb {
|
||||||
/// (alternate `Display`), which keeps the full `context` chain inline
|
/// (alternate `Display`), which keeps the full `context` chain inline
|
||||||
/// rather than dropping it the way plain `Display` would.
|
/// rather than dropping it the way plain `Display` would.
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
|
restore_sigpipe();
|
||||||
if let Err(e) = run() {
|
if let Err(e) = run() {
|
||||||
eprintln!("hive-forge: FAILED: {e:#}");
|
eprintln!("hive-forge: FAILED: {e:#}");
|
||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
|
|
@ -214,6 +215,26 @@ fn main() -> ExitCode {
|
||||||
ExitCode::SUCCESS
|
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 <verb> | 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<()> {
|
fn run() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let verb = cli.verb;
|
let verb = cli.verb;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue