Merge pull request #10156 from ChrisDryden/tail-pipe-fix

tail: fix pipe-f test by detecting broken stdout pipe
This commit is contained in:
Sylvestre Ledru
2026-01-10 16:35:03 +01:00
committed by GitHub
8 changed files with 79 additions and 66 deletions
Generated
-1
View File
@@ -3996,7 +3996,6 @@ version = "0.5.0"
dependencies = [
"clap",
"fluent",
"nix",
"uucore",
]
+7 -18
View File
@@ -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<Box<dyn BufRead>>,
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(
-1
View File
@@ -307,7 +307,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)?;
}
}
}
-1
View File
@@ -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 }
+2 -44
View File
@@ -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<bool> {
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)
}
+1
View File
@@ -93,6 +93,7 @@ nix = { workspace = true, features = [
"signal",
"dir",
"user",
"poll",
] }
xattr = { workspace = true, optional = true }
+43 -1
View File
@@ -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<bool> {
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));
+26
View File
@@ -5039,3 +5039,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();
}