uucore: centralize SIGPIPE handling in main macro (#10354)

Instead of adding this code to every utility, hoping to make it centralized and then addressing all of the special cases in the individual utilities. There's definitely a chance that a special case is missed and could be a regression, but this should solve a much larger number of issues for all utilities.

Fixes https://github.com/uutils/coreutils/issues/10325
Fixes https://github.com/uutils/coreutils/issues/10260
Fixes https://github.com/uutils/coreutils/issues/10230
Fixes https://github.com/uutils/coreutils/issues/10214
Fixes https://github.com/uutils/coreutils/issues/9936
Fixes https://github.com/uutils/coreutils/issues/8919
Fixes https://github.com/uutils/coreutils/issues/7252
Fixes https://github.com/uutils/coreutils/issues/4965
This commit is contained in:
Chris Dryden
2026-01-21 17:54:38 +01:00
committed by GitHub
parent 69f9dc2a34
commit 376d5b26c7
15 changed files with 49 additions and 64 deletions
-7
View File
@@ -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) {
-3
View File
@@ -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;
-4
View File
@@ -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)
}
-14
View File
@@ -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))?;
+10 -1
View File
@@ -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}"))),
}
-2
View File
@@ -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;
-10
View File
@@ -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();
+3 -3
View File
@@ -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<NamedWriter> = options
-7
View File
@@ -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
-7
View File
@@ -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);
+5
View File
@@ -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);
-4
View File
@@ -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<u8>) {
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)?;
+1 -1
View File
@@ -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 = []
+10 -1
View File
@@ -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> {
+20
View File
@@ -16,14 +16,34 @@ use quote::quote;
//* ref: [path construction from LitStr](https://oschwald.github.io/maxminddb-rust/syn/struct.LitStr.html) @@ <http://archive.is/8YDua>
/// 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");