diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index bd29bd246..a46469240 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -133,6 +133,7 @@ setfattr setlocale shortcode shortcodes +sigaction siginfo sigusr strcasecmp diff --git a/Cargo.lock b/Cargo.lock index 37b3362e6..d3845979f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3782,6 +3782,7 @@ dependencies = [ "clap", "codspeed-divan-compat", "fluent", + "nix", "num-bigint", "num-traits", "tempfile", diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 90934a271..bd26e2474 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -1653,6 +1653,7 @@ dependencies = [ "bigdecimal", "clap", "fluent", + "nix", "num-bigint", "num-traits", "thiserror", diff --git a/src/uu/seq/Cargo.toml b/src/uu/seq/Cargo.toml index 6f74ce37a..ad9786eb8 100644 --- a/src/uu/seq/Cargo.toml +++ b/src/uu/seq/Cargo.toml @@ -21,6 +21,7 @@ path = "src/seq.rs" [dependencies] bigdecimal = { workspace = true } clap = { workspace = true } +nix = { workspace = true } num-bigint = { workspace = true } num-traits = { workspace = true } thiserror = { workspace = true } diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index 7b56c26f5..79e64f0ac 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -5,6 +5,8 @@ // spell-checker:ignore (ToDO) bigdecimal extendedbigdecimal numberparse hexadecimalfloat biguint use std::ffi::{OsStr, OsString}; use std::io::{BufWriter, Write, stdout}; +#[cfg(unix)] +use std::sync::atomic::{AtomicBool, Ordering}; use clap::{Arg, ArgAction, Command}; use num_bigint::BigUint; @@ -209,16 +211,18 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { padding, ); - match result { - Ok(()) => Ok(()), - Err(err) if err.kind() == std::io::ErrorKind::BrokenPipe => { + let sigpipe_ignored = sigpipe_is_ignored(); + if let Err(err) = result { + if err.kind() == std::io::ErrorKind::BrokenPipe { // GNU seq prints the Broken pipe message but still exits with status 0 + // unless SIGPIPE was explicitly ignored, in which case it should fail. let err = err.map_err_context(|| "write error".into()); uucore::show_error!("{err}"); - Ok(()) + return if sigpipe_ignored { Err(err) } else { Ok(()) }; } - Err(err) => Err(err.map_err_context(|| "write error".into())), + return Err(err.map_err_context(|| "write error".into())); } + Ok(()) } pub fn uu_app() -> Command { @@ -332,6 +336,44 @@ fn fast_print_seq( Ok(()) } +#[cfg(unix)] +static SIGPIPE_WAS_IGNORED: AtomicBool = AtomicBool::new(false); + +#[cfg(unix)] +/// # Safety +/// This function runs once at process initialization and only observes the +/// current `SIGPIPE` handler, so there are no extra safety requirements for callers. +unsafe extern "C" fn capture_sigpipe_state() { + use nix::libc; + use std::{mem::MaybeUninit, ptr}; + + let mut current = MaybeUninit::::uninit(); + if unsafe { libc::sigaction(libc::SIGPIPE, ptr::null(), current.as_mut_ptr()) } == 0 { + let ignored = unsafe { current.assume_init() }.sa_sigaction == libc::SIG_IGN; + SIGPIPE_WAS_IGNORED.store(ignored, Ordering::Relaxed); + } +} + +#[cfg(all(unix, not(target_os = "macos")))] +#[used] +#[unsafe(link_section = ".init_array")] +static CAPTURE_SIGPIPE_STATE: unsafe extern "C" fn() = capture_sigpipe_state; + +#[cfg(all(unix, target_os = "macos"))] +#[used] +#[unsafe(link_section = "__DATA,__mod_init_func")] +static CAPTURE_SIGPIPE_STATE_APPLE: unsafe extern "C" fn() = capture_sigpipe_state; + +#[cfg(unix)] +fn sigpipe_is_ignored() -> bool { + SIGPIPE_WAS_IGNORED.load(Ordering::Relaxed) +} + +#[cfg(not(unix))] +const fn sigpipe_is_ignored() -> bool { + false +} + fn done_printing(next: &T, increment: &T, last: &T) -> bool { if increment >= &T::zero() { next > last diff --git a/tests/by-util/test_seq.rs b/tests/by-util/test_seq.rs index d5dd526aa..ba185d3d4 100644 --- a/tests/by-util/test_seq.rs +++ b/tests/by-util/test_seq.rs @@ -4,6 +4,10 @@ // file that was distributed with this source code. // spell-checker:ignore lmnop xlmnop use uutests::new_ucmd; +#[cfg(unix)] +use uutests::util::TestScenario; +#[cfg(unix)] +use uutests::util_name; #[test] fn test_invalid_arg() { @@ -203,6 +207,24 @@ fn test_width_invalid_float() { .usage_error("invalid floating point argument: '1e2.3'"); } +#[test] +#[cfg(unix)] +fn test_sigpipe_ignored_reports_write_error() { + let scene = TestScenario::new(util_name!()); + let seq_bin = scene.bin_path.clone().into_os_string(); + let script = "trap '' PIPE; { \"$SEQ_BIN\" seq inf 2>err; echo $? >code; } | head -n1"; + let result = scene.cmd_shell(script).env("SEQ_BIN", &seq_bin).succeeds(); + + assert_eq!(result.stdout_str(), "1\n"); + + let err_contents = scene.fixtures.read("err"); + assert!( + err_contents.contains("seq: write error: Broken pipe"), + "stderr missing write error message: {err_contents:?}" + ); + assert_eq!(scene.fixtures.read("code"), "1\n"); +} + // ---- Tests for the big integer based path ---- #[test]