diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index ff53df767..eeec89025 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -218,13 +218,6 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - // When we receive a SIGPIPE signal, we want to terminate the process so - // that we don't print any error messages to stderr. Rust ignores SIGPIPE - // (see https://github.com/rust-lang/rust/issues/62569), so we restore it's - // default action here. - #[cfg(not(target_os = "windows"))] - let _ = uucore::signals::enable_pipe_errors(); - let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; let number_mode = if matches.get_flag(options::NUMBER_NONBLANK) { diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 45fdf6f3d..fc1adc537 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -5,9 +5,6 @@ // spell-checker:ignore fname, ftype, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rremain, rsofar, rstat, sigusr, wlen, wstat seekable oconv canonicalized fadvise Fadvise FADV DONTNEED ESPIPE bufferedoutput, SETFL -#[cfg(unix)] -uucore::init_startup_state_capture!(); - mod blocks; mod bufferedoutput; mod conversion_tables; diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 40f32b765..a5dd8a8d7 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -1095,10 +1095,6 @@ fn list_signal_handling(log: &SignalActionLog) { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - // Rust ignores SIGPIPE (see https://github.com/rust-lang/rust/issues/62569). - // We restore its default action here. - #[cfg(unix)] - let _ = uucore::signals::enable_pipe_errors(); EnvAppData::default().run_env(args) } diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index b931cc8b1..29373a511 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -92,22 +92,8 @@ fn select_precision( } } -// Initialize SIGPIPE state capture at process startup (Unix only) -#[cfg(unix)] -uucore::init_startup_state_capture!(); - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - // Restore SIGPIPE to default if it wasn't explicitly ignored by parent. - // The Rust runtime ignores SIGPIPE, but we need to respect the parent's - // signal disposition for proper pipeline behavior (GNU compatibility). - #[cfg(unix)] - if !signals::sigpipe_was_ignored() { - // Ignore the return value: if setting signal handler fails, we continue anyway. - // The worst case is we don't get proper SIGPIPE behavior, but seq will still work. - let _ = signals::enable_pipe_errors(); - } - let matches = uucore::clap_localization::handle_clap_result(uu_app(), split_short_args_with_value(args))?; diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 6f290a7d5..c40a6a07e 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -54,7 +54,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; match Settings::from(&matches, obs_lines.as_deref()) { - Ok(settings) => split(&settings), + Ok(settings) => { + // When using --filter, we write to a child process's stdin which may + // close early. Disable SIGPIPE so we get EPIPE errors instead of + // being terminated, allowing graceful handling of broken pipes. + #[cfg(unix)] + if settings.filter.is_some() { + let _ = uucore::signals::disable_pipe_errors(); + } + split(&settings) + } Err(e) if e.requires_usage() => Err(UUsageError::new(1, format!("{e}"))), Err(e) => Err(USimpleError::new(1, format!("{e}"))), } diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index e1686f459..cba911a7e 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -4,8 +4,6 @@ // file that was distributed with this source code. // spell-checker:ignore (ToDO) sbytes slen dlen memmem memmap Mmap mmap SIGBUS -#[cfg(unix)] -uucore::init_startup_state_capture!(); mod error; diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index c1cfb333a..7b82e9566 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -38,18 +38,8 @@ use uucore::translate; use uucore::{show, show_error}; -#[cfg(unix)] -uucore::init_startup_state_capture!(); - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - // When we receive a SIGPIPE signal, we want to terminate the process so - // that we don't print any error messages to stderr. Rust ignores SIGPIPE - // (see https://github.com/rust-lang/rust/issues/62569), so we restore it's - // default action here. - #[cfg(not(target_os = "windows"))] - let _ = uucore::signals::enable_pipe_errors(); - let settings = parse_args(args)?; settings.check_warnings(); diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index cf3d89c0a..026f7fd95 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -19,7 +19,7 @@ use uucore::{format_usage, show_error}; #[cfg(target_os = "linux")] use uucore::signals::ensure_stdout_not_broken; #[cfg(unix)] -use uucore::signals::{enable_pipe_errors, ignore_interrupts}; +use uucore::signals::{disable_pipe_errors, ignore_interrupts}; mod options { pub const APPEND: &str = "append"; @@ -163,8 +163,8 @@ fn tee(options: &Options) -> Result<()> { if options.ignore_interrupts { ignore_interrupts().map_err(|_| Error::from(ErrorKind::Other))?; } - if options.output_error.is_none() { - enable_pipe_errors().map_err(|_| Error::from(ErrorKind::Other))?; + if options.output_error.is_some() { + disable_pipe_errors().map_err(|_| Error::from(ErrorKind::Other))?; } } let mut writers: Vec = options diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 22c839c42..2a917ae79 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -4,8 +4,6 @@ // file that was distributed with this source code. // spell-checker:ignore (ToDO) tstr sigstr cmdname setpgid sigchld getpid -#[cfg(unix)] -uucore::init_startup_state_capture!(); mod status; @@ -22,9 +20,6 @@ use uucore::parser::parse_time; use uucore::process::ChildExt; use uucore::translate; -#[cfg(unix)] -use uucore::signals::enable_pipe_errors; - use uucore::{ format_usage, show_error, signals::{signal_by_name_or_value, signal_name_by_value}, @@ -334,8 +329,6 @@ fn timeout( if !foreground { let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0)); } - #[cfg(unix)] - enable_pipe_errors()?; let mut command = process::Command::new(&cmd[0]); command diff --git a/src/uu/tr/src/tr.rs b/src/uu/tr/src/tr.rs index 2b20d29ce..3a0ee6253 100644 --- a/src/uu/tr/src/tr.rs +++ b/src/uu/tr/src/tr.rs @@ -31,13 +31,6 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - // When we receive a SIGPIPE signal, we want to terminate the process so - // that we don't print any error messages to stderr. Rust ignores SIGPIPE - // (see https://github.com/rust-lang/rust/issues/62569), so we restore it's - // default action here. - #[cfg(not(target_os = "windows"))] - let _ = uucore::signals::enable_pipe_errors(); - let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; let delete_flag = matches.get_flag(options::DELETE); diff --git a/src/uu/tty/src/tty.rs b/src/uu/tty/src/tty.rs index 1469948b8..5bf5199a0 100644 --- a/src/uu/tty/src/tty.rs +++ b/src/uu/tty/src/tty.rs @@ -19,6 +19,11 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { + // Disable SIGPIPE so we can handle broken pipe errors gracefully + // and exit with code 3 instead of being killed by the signal. + #[cfg(unix)] + let _ = uucore::signals::disable_pipe_errors(); + let matches = uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), args, 2)?; let silent = matches.get_flag(options::SILENT); diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index a5aaa18a8..98ee5550c 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -11,8 +11,6 @@ use std::ffi::OsString; use std::io::{self, Write}; use uucore::error::{UResult, USimpleError}; use uucore::format_usage; -#[cfg(unix)] -use uucore::signals::enable_pipe_errors; use uucore::translate; // it's possible that using a smaller or larger buffer might provide better performance on some @@ -113,8 +111,6 @@ fn prepare_buffer(buf: &mut Vec) { pub fn exec(bytes: &[u8]) -> io::Result<()> { let stdout = io::stdout(); let mut stdout = stdout.lock(); - #[cfg(unix)] - enable_pipe_errors()?; loop { stdout.write_all(bytes)?; diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 1cbc276e4..06b192dfe 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -120,7 +120,7 @@ windows-sys = { workspace = true, optional = true, default-features = false, fea utmp-classic = { workspace = true, optional = true } [features] -default = [] +default = ["signals"] # * non-default features backup-control = [] colors = [] diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index 25b91d585..6d0956b39 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.rs @@ -410,7 +410,7 @@ pub fn signal_name_by_value(signal_value: usize) -> Option<&'static str> { ALL_SIGNALS.get(signal_value).copied() } -/// Returns the default signal value. +/// Restores SIGPIPE to default behavior (process terminates on broken pipe). #[cfg(unix)] pub fn enable_pipe_errors() -> Result<(), Errno> { // We pass the error as is, the return value would just be Ok(SigDfl), so we can safely ignore it. @@ -418,6 +418,15 @@ pub fn enable_pipe_errors() -> Result<(), Errno> { unsafe { signal(SIGPIPE, SigDfl) }.map(|_| ()) } +/// Ignores SIGPIPE signal (broken pipe errors are returned instead of terminating). +/// Use this to override the default SIGPIPE handling when you need to handle +/// broken pipe errors gracefully (e.g., tee with --output-error). +#[cfg(unix)] +pub fn disable_pipe_errors() -> Result<(), Errno> { + // SAFETY: this function is safe as long as we do not use a custom SigHandler -- we use the default one. + unsafe { signal(SIGPIPE, SigIgn) }.map(|_| ()) +} + /// Ignores the SIGINT signal. #[cfg(unix)] pub fn ignore_interrupts() -> Result<(), Errno> { diff --git a/src/uucore_procs/src/lib.rs b/src/uucore_procs/src/lib.rs index e60e2b822..c73f542a9 100644 --- a/src/uucore_procs/src/lib.rs +++ b/src/uucore_procs/src/lib.rs @@ -16,14 +16,34 @@ use quote::quote; //* ref: [path construction from LitStr](https://oschwald.github.io/maxminddb-rust/syn/struct.LitStr.html) @@ /// A procedural macro to define the main function of a uutils binary. +/// +/// This macro handles: +/// - SIGPIPE state capture at process startup (before Rust runtime overrides it) +/// - SIGPIPE restoration to default if parent didn't explicitly ignore it +/// - Disabling Rust signal handlers for proper core dumps +/// - Error handling and exit code management #[proc_macro_attribute] pub fn main(_args: TokenStream, stream: TokenStream) -> TokenStream { let stream = proc_macro2::TokenStream::from(stream); let new = quote!( + // Initialize SIGPIPE state capture at process startup (Unix only). + // This must be at module level to set up the .init_array static that runs + // before main() to capture whether SIGPIPE was ignored by the parent process. + #[cfg(unix)] + uucore::init_startup_state_capture!(); + pub fn uumain(args: impl uucore::Args) -> i32 { #stream + // Restore SIGPIPE to default if it wasn't explicitly ignored by parent. + // The Rust runtime ignores SIGPIPE, but we need to respect the parent's + // signal disposition for proper pipeline behavior (GNU compatibility). + #[cfg(unix)] + if !uucore::signals::sigpipe_was_ignored() { + let _ = uucore::signals::enable_pipe_errors(); + } + // disable rust signal handlers (otherwise processes don't dump core after e.g. one SIGSEGV) #[cfg(unix)] uucore::disable_rust_signal_handlers().expect("Disabling rust signal handlers failed");