Skip to content

Commit 832ee2f

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

File tree

5 files changed

+154
-85
lines changed

5 files changed

+154
-85
lines changed

.vscode/cspell.dictionaries/jargon.wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ setfacl
131131
setfattr
132132
shortcode
133133
shortcodes
134+
sigaction
134135
siginfo
135136
sigusr
136137
strcasecmp

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: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ mod numberparse;
2828
use crate::error::SeqError;
2929
use crate::number::PreciseNumber;
3030

31+
#[cfg(unix)]
32+
use uucore::signals;
3133
use uucore::translate;
3234

3335
const OPT_SEPARATOR: &str = "separator";
@@ -90,8 +92,20 @@ fn select_precision(
9092
}
9193
}
9294

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

@@ -209,16 +223,21 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
209223
padding,
210224
);
211225

212-
match result {
213-
Ok(()) => Ok(()),
214-
Err(err) if err.kind() == std::io::ErrorKind::BrokenPipe => {
226+
if let Err(err) = result {
227+
if err.kind() == std::io::ErrorKind::BrokenPipe {
215228
// GNU seq prints the Broken pipe message but still exits with status 0
229+
// unless SIGPIPE was explicitly ignored, in which case it should fail.
216230
let err = err.map_err_context(|| "write error".into());
217231
uucore::show_error!("{err}");
218-
Ok(())
232+
#[cfg(unix)]
233+
if signals::sigpipe_was_ignored() {
234+
uucore::error::set_exit_code(1);
235+
}
236+
return Ok(());
219237
}
220-
Err(err) => Err(err.map_err_context(|| "write error".into())),
238+
return Err(err.map_err_context(|| "write error".into()));
221239
}
240+
Ok(())
222241
}
223242

224243
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: 66 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,18 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55
// spell-checker:ignore lmnop xlmnop
6+
use rstest::rstest;
67
use uutests::new_ucmd;
8+
#[cfg(unix)]
9+
use uutests::util::TestScenario;
10+
#[cfg(unix)]
11+
use uutests::util_name;
712

813
#[test]
914
fn test_invalid_arg() {
1015
new_ucmd!().arg("--definitely-invalid").fails_with_code(1);
1116
}
1217

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-
3418
#[test]
3519
fn test_no_args() {
3620
new_ucmd!()
@@ -203,6 +187,24 @@ fn test_width_invalid_float() {
203187
.usage_error("invalid floating point argument: '1e2.3'");
204188
}
205189

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

208210
#[test]
@@ -648,52 +650,49 @@ fn test_width_floats() {
648650
.stdout_only("09.0\n10.0\n");
649651
}
650652

651-
#[test]
652-
fn test_neg_inf() {
653-
new_ucmd!()
654-
.args(&["--", "-inf", "0"])
655-
.run_stdout_starts_with(b"-inf\n-inf\n-inf\n")
656-
.success();
657-
}
658-
659-
#[test]
660-
fn test_neg_infinity() {
661-
new_ucmd!()
662-
.args(&["--", "-infinity", "0"])
663-
.run_stdout_starts_with(b"-inf\n-inf\n-inf\n")
664-
.success();
665-
}
666-
667-
#[test]
668-
fn test_inf() {
669-
new_ucmd!()
670-
.args(&["inf"])
671-
.run_stdout_starts_with(b"1\n2\n3\n")
672-
.success();
673-
}
674-
675-
#[test]
676-
fn test_infinity() {
677-
new_ucmd!()
678-
.args(&["infinity"])
679-
.run_stdout_starts_with(b"1\n2\n3\n")
680-
.success();
681-
}
682-
683-
#[test]
684-
fn test_inf_width() {
685-
new_ucmd!()
686-
.args(&["-w", "1.000", "inf", "inf"])
687-
.run_stdout_starts_with(b"1.000\n inf\n inf\n inf\n")
688-
.success();
689-
}
690-
691-
#[test]
692-
fn test_neg_inf_width() {
693-
new_ucmd!()
694-
.args(&["-w", "1.000", "-inf", "-inf"])
695-
.run_stdout_starts_with(b"1.000\n -inf\n -inf\n -inf\n")
696-
.success();
653+
/// Test infinite sequences - these produce endless output, so we check they start correctly
654+
/// and terminate with SIGPIPE on Unix (or succeed on non-Unix where pipe behavior differs).
655+
#[rstest]
656+
#[case::neg_inf(
657+
&["--", "-inf", "0"],
658+
b"-inf\n-inf\n-inf\n"
659+
)]
660+
#[case::neg_infinity(
661+
&["--", "-infinity", "0"],
662+
b"-inf\n-inf\n-inf\n"
663+
)]
664+
#[case::inf(
665+
&["inf"],
666+
b"1\n2\n3\n"
667+
)]
668+
#[case::infinity(
669+
&["infinity"],
670+
b"1\n2\n3\n"
671+
)]
672+
#[case::inf_width(
673+
&["-w", "1.000", "inf", "inf"],
674+
b"1.000\n inf\n inf\n inf\n"
675+
)]
676+
#[case::neg_inf_width(
677+
&["-w", "1.000", "-inf", "-inf"],
678+
b"1.000\n -inf\n -inf\n -inf\n"
679+
)]
680+
#[case::precision_inf(
681+
&["1", "1.2", "inf"],
682+
b"1.0\n2.2\n3.4\n"
683+
)]
684+
#[case::equalize_width_inf(
685+
&["-w", "1", "1.2", "inf"],
686+
b"1.0\n2.2\n3.4\n"
687+
)]
688+
fn test_infinite_sequence(#[case] args: &[&str], #[case] expected_start: &[u8]) {
689+
let result = new_ucmd!()
690+
.args(args)
691+
.run_stdout_starts_with(expected_start);
692+
#[cfg(unix)]
693+
result.signal_name_is("PIPE");
694+
#[cfg(not(unix))]
695+
result.success();
697696
}
698697

699698
#[test]
@@ -1073,12 +1072,6 @@ fn test_precision_corner_cases() {
10731072
.args(&["1", "1.20", "3.000000"])
10741073
.succeeds()
10751074
.stdout_is("1.00\n2.20\n");
1076-
1077-
// Infinity is ignored
1078-
new_ucmd!()
1079-
.args(&["1", "1.2", "inf"])
1080-
.run_stdout_starts_with(b"1.0\n2.2\n3.4\n")
1081-
.success();
10821075
}
10831076

10841077
// GNU `seq` manual only makes guarantees about `-w` working if the
@@ -1135,11 +1128,4 @@ fn test_equalize_widths_corner_cases() {
11351128
.args(&["-w", "0x1.1", "1.00002", "3"])
11361129
.succeeds()
11371130
.stdout_is("1.0625\n2.06252\n");
1138-
1139-
// We can't really pad with infinite number of zeros, so `-w` is ignored.
1140-
// (there is another test with infinity as an increment above)
1141-
new_ucmd!()
1142-
.args(&["-w", "1", "1.2", "inf"])
1143-
.run_stdout_starts_with(b"1.0\n2.2\n3.4\n")
1144-
.success();
11451131
}

0 commit comments

Comments
 (0)