Skip to content

Commit e96a170

Browse files
committed
tail: fix pipe-f test by detecting broken stdout pipe
1 parent d737450 commit e96a170

8 files changed

Lines changed: 79 additions & 66 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/uu/tail/src/follow/watch.rs

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use std::path::{Path, PathBuf};
1515
use std::sync::mpsc::{self, Receiver, channel};
1616
use uucore::display::Quotable;
1717
use uucore::error::{UResult, USimpleError, set_exit_code};
18+
#[cfg(target_os = "linux")]
19+
use uucore::signals::ensure_stdout_not_broken;
1820
use uucore::translate;
1921

2022
use uucore::show_error;
@@ -160,24 +162,6 @@ impl Observer {
160162
Ok(())
161163
}
162164

163-
pub fn add_stdin(
164-
&mut self,
165-
display_name: &str,
166-
reader: Option<Box<dyn BufRead>>,
167-
update_last: bool,
168-
) -> UResult<()> {
169-
if self.follow == Some(FollowMode::Descriptor) {
170-
return self.add_path(
171-
&PathBuf::from(text::DEV_STDIN),
172-
display_name,
173-
reader,
174-
update_last,
175-
);
176-
}
177-
178-
Ok(())
179-
}
180-
181165
pub fn add_bad_path(
182166
&mut self,
183167
path: &Path,
@@ -619,6 +603,11 @@ pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> {
619603
}
620604
Err(mpsc::RecvTimeoutError::Timeout) => {
621605
timeout_counter += 1;
606+
// Check if stdout pipe is still open
607+
#[cfg(target_os = "linux")]
608+
if let Ok(false) = ensure_stdout_not_broken() {
609+
return Ok(());
610+
}
622611
}
623612
Err(e) => {
624613
return Err(USimpleError::new(

src/uu/tail/src/tail.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,6 @@ fn tail_stdin(
265265
} else {
266266
let mut reader = BufReader::new(stdin());
267267
unbounded_tail(&mut reader, settings)?;
268-
observer.add_stdin(input.display_name.as_str(), Some(Box::new(reader)), true)?;
269268
}
270269
}
271270
}

src/uu/tee/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ path = "src/tee.rs"
1919

2020
[dependencies]
2121
clap = { workspace = true }
22-
nix = { workspace = true, features = ["poll", "fs"] }
2322
uucore = { workspace = true, features = ["libc", "parser", "signals"] }
2423
fluent = { workspace = true }
2524

src/uu/tee/src/tee.rs

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6-
// cSpell:ignore POLLERR POLLRDBAND pfds revents
7-
86
use clap::{Arg, ArgAction, Command, builder::PossibleValue};
97
use std::ffi::OsString;
108
use std::fs::OpenOptions;
@@ -18,6 +16,8 @@ use uucore::{format_usage, show_error};
1816

1917
// spell-checker:ignore nopipe
2018

19+
#[cfg(target_os = "linux")]
20+
use uucore::signals::ensure_stdout_not_broken;
2121
#[cfg(unix)]
2222
use uucore::signals::{enable_pipe_errors, ignore_interrupts};
2323

@@ -422,45 +422,3 @@ impl Read for NamedReader {
422422
}
423423
}
424424
}
425-
426-
/// Check that if stdout is a pipe, it is not broken.
427-
#[cfg(target_os = "linux")]
428-
pub fn ensure_stdout_not_broken() -> Result<bool> {
429-
use nix::{
430-
poll::{PollFd, PollFlags, PollTimeout},
431-
sys::stat::{SFlag, fstat},
432-
};
433-
use std::os::fd::AsFd;
434-
435-
let out = stdout();
436-
437-
// First, check that stdout is a fifo and return true if it's not the case
438-
let stat = fstat(out.as_fd())?;
439-
if !SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFIFO) {
440-
return Ok(true);
441-
}
442-
443-
// POLLRDBAND is the flag used by GNU tee.
444-
let mut pfds = [PollFd::new(out.as_fd(), PollFlags::POLLRDBAND)];
445-
446-
// Then, ensure that the pipe is not broken.
447-
// Use ZERO timeout to return immediately - we just want to check the current state.
448-
let res = nix::poll::poll(&mut pfds, PollTimeout::ZERO)?;
449-
450-
if res > 0 {
451-
// poll returned with events ready - check if POLLERR is set (pipe broken)
452-
let error = pfds.iter().any(|pfd| {
453-
if let Some(revents) = pfd.revents() {
454-
revents.contains(PollFlags::POLLERR)
455-
} else {
456-
true
457-
}
458-
});
459-
return Ok(!error);
460-
}
461-
462-
// res == 0 means no events ready (timeout reached immediately with ZERO timeout).
463-
// This means the pipe is healthy (not broken).
464-
// res < 0 would be an error, but nix returns Err in that case.
465-
Ok(true)
466-
}

src/uucore/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ nix = { workspace = true, features = [
9393
"signal",
9494
"dir",
9595
"user",
96+
"poll",
9697
] }
9798
xattr = { workspace = true, optional = true }
9899

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

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6-
// spell-checker:ignore (vars/api) fcntl setrlimit setitimer rubout pollable sysconf pgrp
6+
// spell-checker:ignore (vars/api) fcntl setrlimit setitimer rubout pollable sysconf pgrp pfds revents POLLRDBAND POLLERR
77
// 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
88

99
//! This module provides a way to handle signals in a platform-independent way.
@@ -488,6 +488,48 @@ pub const fn sigpipe_was_ignored() -> bool {
488488
false
489489
}
490490

491+
#[cfg(target_os = "linux")]
492+
pub fn ensure_stdout_not_broken() -> std::io::Result<bool> {
493+
use nix::{
494+
poll::{PollFd, PollFlags, PollTimeout, poll},
495+
sys::stat::{SFlag, fstat},
496+
};
497+
use std::io::stdout;
498+
use std::os::fd::AsFd;
499+
500+
let out = stdout();
501+
502+
// First, check that stdout is a fifo and return true if it's not the case
503+
let stat = fstat(out.as_fd())?;
504+
if !SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFIFO) {
505+
return Ok(true);
506+
}
507+
508+
// POLLRDBAND is the flag used by GNU tee.
509+
let mut pfds = [PollFd::new(out.as_fd(), PollFlags::POLLRDBAND)];
510+
511+
// Then, ensure that the pipe is not broken.
512+
// Use ZERO timeout to return immediately - we just want to check the current state.
513+
let res = poll(&mut pfds, PollTimeout::ZERO)?;
514+
515+
if res > 0 {
516+
// poll returned with events ready - check if POLLERR is set (pipe broken)
517+
let error = pfds.iter().any(|pfd| {
518+
if let Some(revents) = pfd.revents() {
519+
revents.contains(PollFlags::POLLERR)
520+
} else {
521+
true
522+
}
523+
});
524+
return Ok(!error);
525+
}
526+
527+
// res == 0 means no events ready (timeout reached immediately with ZERO timeout).
528+
// This means the pipe is healthy (not broken).
529+
// res < 0 would be an error, but nix returns Err in that case.
530+
Ok(true)
531+
}
532+
491533
#[test]
492534
fn signal_by_value() {
493535
assert_eq!(signal_by_name_or_value("0"), Some(0));

tests/by-util/test_tail.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4961,3 +4961,29 @@ fn tail_n_lines_with_emoji() {
49614961
.succeeds()
49624962
.stdout_only("💐\n");
49634963
}
4964+
4965+
#[test]
4966+
#[cfg(target_os = "linux")]
4967+
fn test_follow_pipe_f() {
4968+
new_ucmd!()
4969+
.args(&["-f", "-c3", "-s.1", "--max-unchanged-stats=1"])
4970+
.pipe_in("foo\n")
4971+
.succeeds()
4972+
.stdout_only("oo\n");
4973+
}
4974+
4975+
#[test]
4976+
#[cfg(target_os = "linux")]
4977+
fn test_follow_stdout_pipe_close() {
4978+
let (at, mut ucmd) = at_and_ucmd!();
4979+
at.write("f", "line1\nline2\n");
4980+
4981+
let mut child = ucmd
4982+
.args(&["-f", "-s.1", "--max-unchanged-stats=1", "f"])
4983+
.set_stdout(Stdio::piped())
4984+
.run_no_wait();
4985+
4986+
child.stdout_exact_bytes(6); // read "line1\n"
4987+
child.close_stdout();
4988+
child.delay(2000).make_assertion().is_not_alive();
4989+
}

0 commit comments

Comments
 (0)