Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/cspell.dictionaries/jargon.wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ setfacl
setfattr
shortcode
shortcodes
sigaction
siginfo
sigusr
strcasecmp
Expand Down
1 change: 1 addition & 0 deletions src/uu/seq/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ uucore = { workspace = true, features = [
"format",
"parser",
"quoting-style",
"signals",
] }
fluent = { workspace = true }

Expand Down
29 changes: 24 additions & 5 deletions src/uu/seq/src/seq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ mod numberparse;
use crate::error::SeqError;
use crate::number::PreciseNumber;

#[cfg(unix)]
use uucore::signals;
use uucore::translate;

const OPT_SEPARATOR: &str = "separator";
Expand Down Expand Up @@ -90,8 +92,20 @@ fn select_precision(
}
}

// Initialize SIGPIPE state capture at process startup (Unix only)
#[cfg(unix)]
uucore::init_sigpipe_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() {
let _ = signals::enable_pipe_errors();
}

let matches =
uucore::clap_localization::handle_clap_result(uu_app(), split_short_args_with_value(args))?;

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

match result {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::BrokenPipe => {
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(())
#[cfg(unix)]
if signals::sigpipe_was_ignored() {
uucore::error::set_exit_code(1);
}
return 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 {
Expand Down
62 changes: 62 additions & 0 deletions src/uucore/src/lib/features/signals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,68 @@ pub fn ignore_interrupts() -> Result<(), Errno> {
unsafe { signal(SIGINT, SigIgn) }.map(|_| ())
}

// SIGPIPE state capture - captures whether SIGPIPE was ignored at process startup
#[cfg(unix)]
use std::sync::atomic::{AtomicBool, Ordering};

#[cfg(unix)]
static SIGPIPE_WAS_IGNORED: AtomicBool = AtomicBool::new(false);

/// Captures SIGPIPE state at process initialization, before main() runs.
///
/// # Safety
/// Called from `.init_array` before main(). Only reads current SIGPIPE handler state.
#[cfg(unix)]
pub unsafe extern "C" fn capture_sigpipe_state() {
use nix::libc;
use std::mem::MaybeUninit;
use std::ptr;

let mut current = MaybeUninit::<libc::sigaction>::uninit();
// SAFETY: sigaction with null new-action just queries current state
if unsafe { libc::sigaction(libc::SIGPIPE, ptr::null(), current.as_mut_ptr()) } == 0 {
// SAFETY: sigaction succeeded, so current is initialized
let ignored = unsafe { current.assume_init() }.sa_sigaction == libc::SIG_IGN;
SIGPIPE_WAS_IGNORED.store(ignored, Ordering::Relaxed);
}
}

/// Initializes SIGPIPE state capture. Call once at crate root level.
#[macro_export]
#[cfg(unix)]
macro_rules! init_sigpipe_capture {
() => {
#[cfg(all(unix, not(target_os = "macos")))]
#[used]
#[unsafe(link_section = ".init_array")]
static CAPTURE_SIGPIPE_STATE: unsafe extern "C" fn() =
$crate::signals::capture_sigpipe_state;

#[cfg(all(unix, target_os = "macos"))]
#[used]
#[unsafe(link_section = "__DATA,__mod_init_func")]
static CAPTURE_SIGPIPE_STATE: unsafe extern "C" fn() =
$crate::signals::capture_sigpipe_state;
};
}

#[macro_export]
#[cfg(not(unix))]
macro_rules! init_sigpipe_capture {
() => {};
}

/// Returns whether SIGPIPE was ignored at process startup.
#[cfg(unix)]
pub fn sigpipe_was_ignored() -> bool {
SIGPIPE_WAS_IGNORED.load(Ordering::Relaxed)
}

#[cfg(not(unix))]
pub const fn sigpipe_was_ignored() -> bool {
false
}

#[test]
fn signal_by_value() {
assert_eq!(signal_by_name_or_value("0"), Some(0));
Expand Down
146 changes: 66 additions & 80 deletions tests/by-util/test_seq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,18 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore lmnop xlmnop
use rstest::rstest;
use uutests::new_ucmd;
#[cfg(unix)]
use uutests::util::TestScenario;
#[cfg(unix)]
use uutests::util_name;

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

#[test]
#[cfg(unix)]
fn test_broken_pipe_still_exits_success() {
use std::process::Stdio;

let mut child = new_ucmd!()
// Use an infinite sequence so a burst of output happens immediately after spawn.
// With small output the process can finish before stdout is closed and the Broken pipe never occurs.
.args(&["inf"])
.set_stdout(Stdio::piped())
.run_no_wait();

// Trigger a Broken pipe by writing to a pipe whose reader closed first.
child.close_stdout();
let result = child.wait().unwrap();

result
.code_is(0)
.stderr_contains("write error: Broken pipe");
}

#[test]
fn test_no_args() {
new_ucmd!()
Expand Down Expand Up @@ -203,6 +187,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]
Expand Down Expand Up @@ -648,52 +650,49 @@ fn test_width_floats() {
.stdout_only("09.0\n10.0\n");
}

#[test]
fn test_neg_inf() {
new_ucmd!()
.args(&["--", "-inf", "0"])
.run_stdout_starts_with(b"-inf\n-inf\n-inf\n")
.success();
}

#[test]
fn test_neg_infinity() {
new_ucmd!()
.args(&["--", "-infinity", "0"])
.run_stdout_starts_with(b"-inf\n-inf\n-inf\n")
.success();
}

#[test]
fn test_inf() {
new_ucmd!()
.args(&["inf"])
.run_stdout_starts_with(b"1\n2\n3\n")
.success();
}

#[test]
fn test_infinity() {
new_ucmd!()
.args(&["infinity"])
.run_stdout_starts_with(b"1\n2\n3\n")
.success();
}

#[test]
fn test_inf_width() {
new_ucmd!()
.args(&["-w", "1.000", "inf", "inf"])
.run_stdout_starts_with(b"1.000\n inf\n inf\n inf\n")
.success();
}

#[test]
fn test_neg_inf_width() {
new_ucmd!()
.args(&["-w", "1.000", "-inf", "-inf"])
.run_stdout_starts_with(b"1.000\n -inf\n -inf\n -inf\n")
.success();
/// Test infinite sequences - these produce endless output, so we check they start correctly
/// and terminate with SIGPIPE on Unix (or succeed on non-Unix where pipe behavior differs).
#[rstest]
#[case::neg_inf(
&["--", "-inf", "0"],
b"-inf\n-inf\n-inf\n"
)]
#[case::neg_infinity(
&["--", "-infinity", "0"],
b"-inf\n-inf\n-inf\n"
)]
#[case::inf(
&["inf"],
b"1\n2\n3\n"
)]
#[case::infinity(
&["infinity"],
b"1\n2\n3\n"
)]
#[case::inf_width(
&["-w", "1.000", "inf", "inf"],
b"1.000\n inf\n inf\n inf\n"
)]
#[case::neg_inf_width(
&["-w", "1.000", "-inf", "-inf"],
b"1.000\n -inf\n -inf\n -inf\n"
)]
#[case::precision_inf(
&["1", "1.2", "inf"],
b"1.0\n2.2\n3.4\n"
)]
#[case::equalize_width_inf(
&["-w", "1", "1.2", "inf"],
b"1.0\n2.2\n3.4\n"
)]
fn test_infinite_sequence(#[case] args: &[&str], #[case] expected_start: &[u8]) {
let result = new_ucmd!()
.args(args)
.run_stdout_starts_with(expected_start);
#[cfg(unix)]
result.signal_name_is("PIPE");
#[cfg(not(unix))]
result.success();
}

#[test]
Expand Down Expand Up @@ -1073,12 +1072,6 @@ fn test_precision_corner_cases() {
.args(&["1", "1.20", "3.000000"])
.succeeds()
.stdout_is("1.00\n2.20\n");

// Infinity is ignored
new_ucmd!()
.args(&["1", "1.2", "inf"])
.run_stdout_starts_with(b"1.0\n2.2\n3.4\n")
.success();
}

// GNU `seq` manual only makes guarantees about `-w` working if the
Expand Down Expand Up @@ -1135,11 +1128,4 @@ fn test_equalize_widths_corner_cases() {
.args(&["-w", "0x1.1", "1.00002", "3"])
.succeeds()
.stdout_is("1.0625\n2.06252\n");

// We can't really pad with infinite number of zeros, so `-w` is ignored.
// (there is another test with infinity as an increment above)
new_ucmd!()
.args(&["-w", "1", "1.2", "inf"])
.run_stdout_starts_with(b"1.0\n2.2\n3.4\n")
.success();
}
Loading