Skip to content

Commit ba48f71

Browse files
committed
seq: add SIGPIPE handling for GNU compatibility
1 parent c085cd1 commit ba48f71

File tree

4 files changed

+113
-34
lines changed

4 files changed

+113
-34
lines changed

src/uu/seq/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ uucore = { workspace = true, features = [
3030
"format",
3131
"parser",
3232
"quoting-style",
33+
"signals",
3334
] }
3435
fluent = { workspace = true }
3536

src/uu/seq/src/seq.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,19 @@ fn select_precision(
9090
}
9191
}
9292

93+
// Initialize SIGPIPE state capture at process startup
94+
uucore::init_sigpipe_capture!();
95+
9396
#[uucore::main]
9497
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
98+
// Restore SIGPIPE to default if it wasn't explicitly ignored by parent.
99+
// The Rust runtime ignores SIGPIPE, but we need to respect the parent's
100+
// signal disposition for proper pipeline behavior (GNU compatibility).
101+
#[cfg(unix)]
102+
if !uucore::signals::sigpipe_was_ignored() {
103+
let _ = uucore::signals::enable_pipe_errors();
104+
}
105+
95106
let matches =
96107
uucore::clap_localization::handle_clap_result(uu_app(), split_short_args_with_value(args))?;
97108

@@ -209,16 +220,20 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
209220
padding,
210221
);
211222

212-
match result {
213-
Ok(()) => Ok(()),
214-
Err(err) if err.kind() == std::io::ErrorKind::BrokenPipe => {
223+
if let Err(err) = result {
224+
if err.kind() == std::io::ErrorKind::BrokenPipe {
215225
// GNU seq prints the Broken pipe message but still exits with status 0
226+
// unless SIGPIPE was explicitly ignored, in which case it should fail.
216227
let err = err.map_err_context(|| "write error".into());
217228
uucore::show_error!("{err}");
218-
Ok(())
229+
if uucore::signals::sigpipe_was_ignored() {
230+
uucore::error::set_exit_code(1);
231+
}
232+
return Ok(());
219233
}
220-
Err(err) => Err(err.map_err_context(|| "write error".into())),
234+
return Err(err.map_err_context(|| "write error".into()));
221235
}
236+
Ok(())
222237
}
223238

224239
pub fn uu_app() -> Command {

src/uucore/src/lib/features/signals.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,68 @@ pub fn ignore_interrupts() -> Result<(), Errno> {
426426
unsafe { signal(SIGINT, SigIgn) }.map(|_| ())
427427
}
428428

429+
// SIGPIPE state capture - captures whether SIGPIPE was ignored at process startup
430+
#[cfg(unix)]
431+
use std::sync::atomic::{AtomicBool, Ordering};
432+
433+
#[cfg(unix)]
434+
static SIGPIPE_WAS_IGNORED: AtomicBool = AtomicBool::new(false);
435+
436+
/// Captures SIGPIPE state at process initialization, before main() runs.
437+
///
438+
/// # Safety
439+
/// Called from `.init_array` before main(). Only reads current SIGPIPE handler state.
440+
#[cfg(unix)]
441+
pub unsafe extern "C" fn capture_sigpipe_state() {
442+
use nix::libc;
443+
use std::mem::MaybeUninit;
444+
use std::ptr;
445+
446+
let mut current = MaybeUninit::<libc::sigaction>::uninit();
447+
// SAFETY: sigaction with null new-action just queries current state
448+
if unsafe { libc::sigaction(libc::SIGPIPE, ptr::null(), current.as_mut_ptr()) } == 0 {
449+
// SAFETY: sigaction succeeded, so current is initialized
450+
let ignored = unsafe { current.assume_init() }.sa_sigaction == libc::SIG_IGN;
451+
SIGPIPE_WAS_IGNORED.store(ignored, Ordering::Relaxed);
452+
}
453+
}
454+
455+
/// Initializes SIGPIPE state capture. Call once at crate root level.
456+
#[macro_export]
457+
#[cfg(unix)]
458+
macro_rules! init_sigpipe_capture {
459+
() => {
460+
#[cfg(all(unix, not(target_os = "macos")))]
461+
#[used]
462+
#[unsafe(link_section = ".init_array")]
463+
static CAPTURE_SIGPIPE_STATE: unsafe extern "C" fn() =
464+
$crate::signals::capture_sigpipe_state;
465+
466+
#[cfg(all(unix, target_os = "macos"))]
467+
#[used]
468+
#[unsafe(link_section = "__DATA,__mod_init_func")]
469+
static CAPTURE_SIGPIPE_STATE: unsafe extern "C" fn() =
470+
$crate::signals::capture_sigpipe_state;
471+
};
472+
}
473+
474+
#[macro_export]
475+
#[cfg(not(unix))]
476+
macro_rules! init_sigpipe_capture {
477+
() => {};
478+
}
479+
480+
/// Returns whether SIGPIPE was ignored at process startup.
481+
#[cfg(unix)]
482+
pub fn sigpipe_was_ignored() -> bool {
483+
SIGPIPE_WAS_IGNORED.load(Ordering::Relaxed)
484+
}
485+
486+
#[cfg(not(unix))]
487+
pub const fn sigpipe_was_ignored() -> bool {
488+
false
489+
}
490+
429491
#[test]
430492
fn signal_by_value() {
431493
assert_eq!(signal_by_name_or_value("0"), Some(0));

tests/by-util/test_seq.rs

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,16 @@
44
// file that was distributed with this source code.
55
// spell-checker:ignore lmnop xlmnop
66
use uutests::new_ucmd;
7+
#[cfg(unix)]
8+
use uutests::util::TestScenario;
9+
#[cfg(unix)]
10+
use uutests::util_name;
711

812
#[test]
913
fn test_invalid_arg() {
1014
new_ucmd!().arg("--definitely-invalid").fails_with_code(1);
1115
}
1216

13-
#[test]
14-
#[cfg(unix)]
15-
fn test_broken_pipe_still_exits_success() {
16-
use std::process::Stdio;
17-
18-
let mut child = new_ucmd!()
19-
// Use an infinite sequence so a burst of output happens immediately after spawn.
20-
// With small output the process can finish before stdout is closed and the Broken pipe never occurs.
21-
.args(&["inf"])
22-
.set_stdout(Stdio::piped())
23-
.run_no_wait();
24-
25-
// Trigger a Broken pipe by writing to a pipe whose reader closed first.
26-
child.close_stdout();
27-
let result = child.wait().unwrap();
28-
29-
result
30-
.code_is(0)
31-
.stderr_contains("write error: Broken pipe");
32-
}
33-
3417
#[test]
3518
fn test_no_args() {
3619
new_ucmd!()
@@ -203,6 +186,24 @@ fn test_width_invalid_float() {
203186
.usage_error("invalid floating point argument: '1e2.3'");
204187
}
205188

189+
#[test]
190+
#[cfg(unix)]
191+
fn test_sigpipe_ignored_reports_write_error() {
192+
let scene = TestScenario::new(util_name!());
193+
let seq_bin = scene.bin_path.clone().into_os_string();
194+
let script = "trap '' PIPE; { \"$SEQ_BIN\" seq inf 2>err; echo $? >code; } | head -n1";
195+
let result = scene.cmd_shell(script).env("SEQ_BIN", &seq_bin).succeeds();
196+
197+
assert_eq!(result.stdout_str(), "1\n");
198+
199+
let err_contents = scene.fixtures.read("err");
200+
assert!(
201+
err_contents.contains("seq: write error: Broken pipe"),
202+
"stderr missing write error message: {err_contents:?}"
203+
);
204+
assert_eq!(scene.fixtures.read("code"), "1\n");
205+
}
206+
206207
// ---- Tests for the big integer based path ----
207208

208209
#[test]
@@ -653,47 +654,47 @@ fn test_neg_inf() {
653654
new_ucmd!()
654655
.args(&["--", "-inf", "0"])
655656
.run_stdout_starts_with(b"-inf\n-inf\n-inf\n")
656-
.success();
657+
.signal_name_is("PIPE");
657658
}
658659

659660
#[test]
660661
fn test_neg_infinity() {
661662
new_ucmd!()
662663
.args(&["--", "-infinity", "0"])
663664
.run_stdout_starts_with(b"-inf\n-inf\n-inf\n")
664-
.success();
665+
.signal_name_is("PIPE");
665666
}
666667

667668
#[test]
668669
fn test_inf() {
669670
new_ucmd!()
670671
.args(&["inf"])
671672
.run_stdout_starts_with(b"1\n2\n3\n")
672-
.success();
673+
.signal_name_is("PIPE");
673674
}
674675

675676
#[test]
676677
fn test_infinity() {
677678
new_ucmd!()
678679
.args(&["infinity"])
679680
.run_stdout_starts_with(b"1\n2\n3\n")
680-
.success();
681+
.signal_name_is("PIPE");
681682
}
682683

683684
#[test]
684685
fn test_inf_width() {
685686
new_ucmd!()
686687
.args(&["-w", "1.000", "inf", "inf"])
687688
.run_stdout_starts_with(b"1.000\n inf\n inf\n inf\n")
688-
.success();
689+
.signal_name_is("PIPE");
689690
}
690691

691692
#[test]
692693
fn test_neg_inf_width() {
693694
new_ucmd!()
694695
.args(&["-w", "1.000", "-inf", "-inf"])
695696
.run_stdout_starts_with(b"1.000\n -inf\n -inf\n -inf\n")
696-
.success();
697+
.signal_name_is("PIPE");
697698
}
698699

699700
#[test]
@@ -1078,7 +1079,7 @@ fn test_precision_corner_cases() {
10781079
new_ucmd!()
10791080
.args(&["1", "1.2", "inf"])
10801081
.run_stdout_starts_with(b"1.0\n2.2\n3.4\n")
1081-
.success();
1082+
.signal_name_is("PIPE");
10821083
}
10831084

10841085
// GNU `seq` manual only makes guarantees about `-w` working if the
@@ -1141,5 +1142,5 @@ fn test_equalize_widths_corner_cases() {
11411142
new_ucmd!()
11421143
.args(&["-w", "1", "1.2", "inf"])
11431144
.run_stdout_starts_with(b"1.0\n2.2\n3.4\n")
1144-
.success();
1145+
.signal_name_is("PIPE");
11451146
}

0 commit comments

Comments
 (0)