From e96a1706759836ce9c6458285675c87223cd68ca Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Sat, 10 Jan 2026 06:01:50 +0000 Subject: [PATCH] tail: fix pipe-f test by detecting broken stdout pipe --- Cargo.lock | 1 - src/uu/tail/src/follow/watch.rs | 25 ++++---------- src/uu/tail/src/tail.rs | 1 - src/uu/tee/Cargo.toml | 1 - src/uu/tee/src/tee.rs | 46 ++------------------------ src/uucore/Cargo.toml | 1 + src/uucore/src/lib/features/signals.rs | 44 +++++++++++++++++++++++- tests/by-util/test_tail.rs | 26 +++++++++++++++ 8 files changed, 79 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30192f4cb..ec4b6825a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3995,7 +3995,6 @@ version = "0.5.0" dependencies = [ "clap", "fluent", - "nix", "uucore", ] diff --git a/src/uu/tail/src/follow/watch.rs b/src/uu/tail/src/follow/watch.rs index b4b4d00ac..a5dd53897 100644 --- a/src/uu/tail/src/follow/watch.rs +++ b/src/uu/tail/src/follow/watch.rs @@ -15,6 +15,8 @@ use std::path::{Path, PathBuf}; use std::sync::mpsc::{self, Receiver, channel}; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, set_exit_code}; +#[cfg(target_os = "linux")] +use uucore::signals::ensure_stdout_not_broken; use uucore::translate; use uucore::show_error; @@ -160,24 +162,6 @@ impl Observer { Ok(()) } - pub fn add_stdin( - &mut self, - display_name: &str, - reader: Option>, - update_last: bool, - ) -> UResult<()> { - if self.follow == Some(FollowMode::Descriptor) { - return self.add_path( - &PathBuf::from(text::DEV_STDIN), - display_name, - reader, - update_last, - ); - } - - Ok(()) - } - pub fn add_bad_path( &mut self, path: &Path, @@ -619,6 +603,11 @@ pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> { } Err(mpsc::RecvTimeoutError::Timeout) => { timeout_counter += 1; + // Check if stdout pipe is still open + #[cfg(target_os = "linux")] + if let Ok(false) = ensure_stdout_not_broken() { + return Ok(()); + } } Err(e) => { return Err(USimpleError::new( diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 56bf15504..ec094ae0a 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -265,7 +265,6 @@ fn tail_stdin( } else { let mut reader = BufReader::new(stdin()); unbounded_tail(&mut reader, settings)?; - observer.add_stdin(input.display_name.as_str(), Some(Box::new(reader)), true)?; } } } diff --git a/src/uu/tee/Cargo.toml b/src/uu/tee/Cargo.toml index 397f6efbb..38a946edf 100644 --- a/src/uu/tee/Cargo.toml +++ b/src/uu/tee/Cargo.toml @@ -19,7 +19,6 @@ path = "src/tee.rs" [dependencies] clap = { workspace = true } -nix = { workspace = true, features = ["poll", "fs"] } uucore = { workspace = true, features = ["libc", "parser", "signals"] } fluent = { workspace = true } diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index 77859fa8f..1325b0465 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -3,8 +3,6 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// cSpell:ignore POLLERR POLLRDBAND pfds revents - use clap::{Arg, ArgAction, Command, builder::PossibleValue}; use std::ffi::OsString; use std::fs::OpenOptions; @@ -18,6 +16,8 @@ use uucore::{format_usage, show_error}; // spell-checker:ignore nopipe +#[cfg(target_os = "linux")] +use uucore::signals::ensure_stdout_not_broken; #[cfg(unix)] use uucore::signals::{enable_pipe_errors, ignore_interrupts}; @@ -422,45 +422,3 @@ impl Read for NamedReader { } } } - -/// Check that if stdout is a pipe, it is not broken. -#[cfg(target_os = "linux")] -pub fn ensure_stdout_not_broken() -> Result { - use nix::{ - poll::{PollFd, PollFlags, PollTimeout}, - sys::stat::{SFlag, fstat}, - }; - use std::os::fd::AsFd; - - let out = stdout(); - - // First, check that stdout is a fifo and return true if it's not the case - let stat = fstat(out.as_fd())?; - if !SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFIFO) { - return Ok(true); - } - - // POLLRDBAND is the flag used by GNU tee. - let mut pfds = [PollFd::new(out.as_fd(), PollFlags::POLLRDBAND)]; - - // Then, ensure that the pipe is not broken. - // Use ZERO timeout to return immediately - we just want to check the current state. - let res = nix::poll::poll(&mut pfds, PollTimeout::ZERO)?; - - if res > 0 { - // poll returned with events ready - check if POLLERR is set (pipe broken) - let error = pfds.iter().any(|pfd| { - if let Some(revents) = pfd.revents() { - revents.contains(PollFlags::POLLERR) - } else { - true - } - }); - return Ok(!error); - } - - // res == 0 means no events ready (timeout reached immediately with ZERO timeout). - // This means the pipe is healthy (not broken). - // res < 0 would be an error, but nix returns Err in that case. - Ok(true) -} diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index d58d2ccca..80742d2d5 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -93,6 +93,7 @@ nix = { workspace = true, features = [ "signal", "dir", "user", + "poll", ] } xattr = { workspace = true, optional = true } diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index 81b414a86..1c4d684a7 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (vars/api) fcntl setrlimit setitimer rubout pollable sysconf pgrp +// spell-checker:ignore (vars/api) fcntl setrlimit setitimer rubout pollable sysconf pgrp pfds revents POLLRDBAND POLLERR // spell-checker:ignore (vars/signals) ABRT ALRM CHLD SEGV SIGABRT SIGALRM SIGBUS SIGCHLD SIGCONT SIGDANGER SIGEMT SIGFPE SIGHUP SIGILL SIGINFO SIGINT SIGIO SIGIOT SIGKILL SIGMIGRATE SIGMSG SIGPIPE SIGPRE SIGPROF SIGPWR SIGQUIT SIGSEGV SIGSTOP SIGSYS SIGTALRM SIGTERM SIGTRAP SIGTSTP SIGTHR SIGTTIN SIGTTOU SIGURG SIGUSR SIGVIRT SIGVTALRM SIGWINCH SIGXCPU SIGXFSZ STKFLT PWR THR TSTP TTIN TTOU VIRT VTALRM XCPU XFSZ SIGCLD SIGPOLL SIGWAITING SIGAIOCANCEL SIGLWP SIGFREEZE SIGTHAW SIGCANCEL SIGLOST SIGXRES SIGJVM SIGRTMIN SIGRT SIGRTMAX TALRM AIOCANCEL XRES RTMIN RTMAX LTOSTOP //! This module provides a way to handle signals in a platform-independent way. @@ -488,6 +488,48 @@ pub const fn sigpipe_was_ignored() -> bool { false } +#[cfg(target_os = "linux")] +pub fn ensure_stdout_not_broken() -> std::io::Result { + use nix::{ + poll::{PollFd, PollFlags, PollTimeout, poll}, + sys::stat::{SFlag, fstat}, + }; + use std::io::stdout; + use std::os::fd::AsFd; + + let out = stdout(); + + // First, check that stdout is a fifo and return true if it's not the case + let stat = fstat(out.as_fd())?; + if !SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFIFO) { + return Ok(true); + } + + // POLLRDBAND is the flag used by GNU tee. + let mut pfds = [PollFd::new(out.as_fd(), PollFlags::POLLRDBAND)]; + + // Then, ensure that the pipe is not broken. + // Use ZERO timeout to return immediately - we just want to check the current state. + let res = poll(&mut pfds, PollTimeout::ZERO)?; + + if res > 0 { + // poll returned with events ready - check if POLLERR is set (pipe broken) + let error = pfds.iter().any(|pfd| { + if let Some(revents) = pfd.revents() { + revents.contains(PollFlags::POLLERR) + } else { + true + } + }); + return Ok(!error); + } + + // res == 0 means no events ready (timeout reached immediately with ZERO timeout). + // This means the pipe is healthy (not broken). + // res < 0 would be an error, but nix returns Err in that case. + Ok(true) +} + #[test] fn signal_by_value() { assert_eq!(signal_by_name_or_value("0"), Some(0)); diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 50b404c91..36a0a9a53 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -4961,3 +4961,29 @@ fn tail_n_lines_with_emoji() { .succeeds() .stdout_only("💐\n"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_follow_pipe_f() { + new_ucmd!() + .args(&["-f", "-c3", "-s.1", "--max-unchanged-stats=1"]) + .pipe_in("foo\n") + .succeeds() + .stdout_only("oo\n"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_follow_stdout_pipe_close() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("f", "line1\nline2\n"); + + let mut child = ucmd + .args(&["-f", "-s.1", "--max-unchanged-stats=1", "f"]) + .set_stdout(Stdio::piped()) + .run_no_wait(); + + child.stdout_exact_bytes(6); // read "line1\n" + child.close_stdout(); + child.delay(2000).make_assertion().is_not_alive(); +}