Skip to content

Commit 03b6371

Browse files
authored
Merge pull request #7273 from RenjiSann/tee-fix-p-broken-stdout
tee: fix -p behavior upon broken pipe stdout
2 parents 569afcc + e550e3d commit 03b6371

File tree

4 files changed

+131
-28
lines changed

4 files changed

+131
-28
lines changed

Cargo.lock

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

src/uu/tee/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ path = "src/tee.rs"
1818

1919
[dependencies]
2020
clap = { workspace = true }
21-
libc = { workspace = true }
21+
nix = { workspace = true, features = ["poll", "fs"] }
2222
uucore = { workspace = true, features = ["libc", "signals"] }
2323

2424
[[bin]]

src/uu/tee/src/tee.rs

Lines changed: 85 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
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+
68
use clap::{builder::PossibleValue, crate_version, Arg, ArgAction, Command};
79
use std::fs::OpenOptions;
810
use std::io::{copy, stdin, stdout, Error, ErrorKind, Read, Result, Write};
@@ -33,48 +35,60 @@ mod options {
3335
struct Options {
3436
append: bool,
3537
ignore_interrupts: bool,
38+
ignore_pipe_errors: bool,
3639
files: Vec<String>,
3740
output_error: Option<OutputErrorMode>,
3841
}
3942

4043
#[derive(Clone, Debug)]
4144
enum OutputErrorMode {
45+
/// Diagnose write error on any output
4246
Warn,
47+
/// Diagnose write error on any output that is not a pipe
4348
WarnNoPipe,
49+
/// Exit upon write error on any output
4450
Exit,
51+
/// Exit upon write error on any output that is not a pipe
4552
ExitNoPipe,
4653
}
4754

4855
#[uucore::main]
4956
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
5057
let matches = uu_app().try_get_matches_from(args)?;
5158

59+
let append = matches.get_flag(options::APPEND);
60+
let ignore_interrupts = matches.get_flag(options::IGNORE_INTERRUPTS);
61+
let ignore_pipe_errors = matches.get_flag(options::IGNORE_PIPE_ERRORS);
62+
let output_error = if matches.contains_id(options::OUTPUT_ERROR) {
63+
match matches
64+
.get_one::<String>(options::OUTPUT_ERROR)
65+
.map(String::as_str)
66+
{
67+
Some("warn") => Some(OutputErrorMode::Warn),
68+
// If no argument is specified for --output-error,
69+
// defaults to warn-nopipe
70+
None | Some("warn-nopipe") => Some(OutputErrorMode::WarnNoPipe),
71+
Some("exit") => Some(OutputErrorMode::Exit),
72+
Some("exit-nopipe") => Some(OutputErrorMode::ExitNoPipe),
73+
_ => unreachable!(),
74+
}
75+
} else if ignore_pipe_errors {
76+
Some(OutputErrorMode::WarnNoPipe)
77+
} else {
78+
None
79+
};
80+
81+
let files = matches
82+
.get_many::<String>(options::FILE)
83+
.map(|v| v.map(ToString::to_string).collect())
84+
.unwrap_or_default();
85+
5286
let options = Options {
53-
append: matches.get_flag(options::APPEND),
54-
ignore_interrupts: matches.get_flag(options::IGNORE_INTERRUPTS),
55-
files: matches
56-
.get_many::<String>(options::FILE)
57-
.map(|v| v.map(ToString::to_string).collect())
58-
.unwrap_or_default(),
59-
output_error: {
60-
if matches.get_flag(options::IGNORE_PIPE_ERRORS) {
61-
Some(OutputErrorMode::WarnNoPipe)
62-
} else if matches.contains_id(options::OUTPUT_ERROR) {
63-
if let Some(v) = matches.get_one::<String>(options::OUTPUT_ERROR) {
64-
match v.as_str() {
65-
"warn" => Some(OutputErrorMode::Warn),
66-
"warn-nopipe" => Some(OutputErrorMode::WarnNoPipe),
67-
"exit" => Some(OutputErrorMode::Exit),
68-
"exit-nopipe" => Some(OutputErrorMode::ExitNoPipe),
69-
_ => unreachable!(),
70-
}
71-
} else {
72-
Some(OutputErrorMode::WarnNoPipe)
73-
}
74-
} else {
75-
None
76-
}
77-
},
87+
append,
88+
ignore_interrupts,
89+
ignore_pipe_errors,
90+
files,
91+
output_error,
7892
};
7993

8094
match tee(&options) {
@@ -140,7 +154,6 @@ pub fn uu_app() -> Command {
140154
.help("exit on write errors to any output that are not pipe errors (equivalent to exit on non-unix platforms)"),
141155
]))
142156
.help("set write error behavior")
143-
.conflicts_with(options::IGNORE_PIPE_ERRORS),
144157
)
145158
}
146159

@@ -177,6 +190,11 @@ fn tee(options: &Options) -> Result<()> {
177190
inner: Box::new(stdin()) as Box<dyn Read>,
178191
};
179192

193+
#[cfg(target_os = "linux")]
194+
if options.ignore_pipe_errors && !ensure_stdout_not_broken()? && output.writers.len() == 1 {
195+
return Ok(());
196+
}
197+
180198
let res = match copy(input, &mut output) {
181199
// ErrorKind::Other is raised by MultiWriter when all writers
182200
// have exited, so that copy will abort. It's equivalent to
@@ -367,3 +385,44 @@ impl Read for NamedReader {
367385
}
368386
}
369387
}
388+
389+
/// Check that if stdout is a pipe, it is not broken.
390+
#[cfg(target_os = "linux")]
391+
pub fn ensure_stdout_not_broken() -> Result<bool> {
392+
use nix::{
393+
poll::{PollFd, PollFlags, PollTimeout},
394+
sys::stat::{fstat, SFlag},
395+
};
396+
use std::os::fd::{AsFd, AsRawFd};
397+
398+
let out = stdout();
399+
400+
// First, check that stdout is a fifo and return true if it's not the case
401+
let stat = fstat(out.as_raw_fd())?;
402+
if !SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFIFO) {
403+
return Ok(true);
404+
}
405+
406+
// POLLRDBAND is the flag used by GNU tee.
407+
let mut pfds = [PollFd::new(out.as_fd(), PollFlags::POLLRDBAND)];
408+
409+
// Then, ensure that the pipe is not broken
410+
let res = nix::poll::poll(&mut pfds, PollTimeout::NONE)?;
411+
412+
if res > 0 {
413+
// poll succeeded;
414+
let error = pfds.iter().any(|pfd| {
415+
if let Some(revents) = pfd.revents() {
416+
revents.contains(PollFlags::POLLERR)
417+
} else {
418+
true
419+
}
420+
});
421+
return Ok(!error);
422+
}
423+
424+
// if res == 0, it means that timeout was reached, which is impossible
425+
// because we set infinite timeout.
426+
// And if res < 0, the nix wrapper should have sent back an error.
427+
unreachable!();
428+
}

tests/by-util/test_tee.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ mod linux_only {
165165
use std::fmt::Write;
166166
use std::fs::File;
167167
use std::process::{Output, Stdio};
168+
use std::time::Duration;
168169

169170
fn make_broken_pipe() -> File {
170171
use libc::c_int;
@@ -183,6 +184,22 @@ mod linux_only {
183184
unsafe { File::from_raw_fd(fds[1]) }
184185
}
185186

187+
fn make_hanging_read() -> File {
188+
use libc::c_int;
189+
use std::os::unix::io::FromRawFd;
190+
191+
let mut fds: [c_int; 2] = [0, 0];
192+
assert!(
193+
(unsafe { libc::pipe(std::ptr::from_mut::<c_int>(&mut fds[0])) } == 0),
194+
"Failed to create pipe"
195+
);
196+
197+
// PURPOSELY leak the write end of the pipe, so the read end hangs.
198+
199+
// Return the read end of the pipe
200+
unsafe { File::from_raw_fd(fds[0]) }
201+
}
202+
186203
fn run_tee(proc: &mut UCommand) -> (String, Output) {
187204
let content = (1..=100_000).fold(String::new(), |mut output, x| {
188205
let _ = writeln!(output, "{x}");
@@ -535,4 +552,31 @@ mod linux_only {
535552
expect_failure(&output, "No space left");
536553
expect_short(file_out_a, &at, content.as_str());
537554
}
555+
556+
#[test]
557+
fn test_pipe_mode_broken_pipe_only() {
558+
new_ucmd!()
559+
.timeout(Duration::from_secs(1))
560+
.arg("-p")
561+
.set_stdin(make_hanging_read())
562+
.set_stdout(make_broken_pipe())
563+
.succeeds();
564+
}
565+
566+
#[test]
567+
fn test_pipe_mode_broken_pipe_file() {
568+
let (at, mut ucmd) = at_and_ucmd!();
569+
570+
let file_out_a = "tee_file_out_a";
571+
572+
let proc = ucmd
573+
.arg("-p")
574+
.arg(file_out_a)
575+
.set_stdout(make_broken_pipe());
576+
577+
let (content, output) = run_tee(proc);
578+
579+
expect_success(&output);
580+
expect_correct(file_out_a, &at, content.as_str());
581+
}
538582
}

0 commit comments

Comments
 (0)