mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37:44 +00:00
Merge pull request #7273 from RenjiSann/tee-fix-p-broken-stdout
tee: fix -p behavior upon broken pipe stdout
This commit is contained in:
commit
03b6371422
4 changed files with 131 additions and 28 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -3320,7 +3320,7 @@ name = "uu_tee"
|
||||||
version = "0.0.29"
|
version = "0.0.29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"libc",
|
"nix",
|
||||||
"uucore",
|
"uucore",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ path = "src/tee.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
libc = { workspace = true }
|
nix = { workspace = true, features = ["poll", "fs"] }
|
||||||
uucore = { workspace = true, features = ["libc", "signals"] }
|
uucore = { workspace = true, features = ["libc", "signals"] }
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
// For the full copyright and license information, please view the LICENSE
|
// For the full copyright and license information, please view the LICENSE
|
||||||
// file that was distributed with this source code.
|
// file that was distributed with this source code.
|
||||||
|
|
||||||
|
// cSpell:ignore POLLERR POLLRDBAND pfds revents
|
||||||
|
|
||||||
use clap::{builder::PossibleValue, crate_version, Arg, ArgAction, Command};
|
use clap::{builder::PossibleValue, crate_version, Arg, ArgAction, Command};
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io::{copy, stdin, stdout, Error, ErrorKind, Read, Result, Write};
|
use std::io::{copy, stdin, stdout, Error, ErrorKind, Read, Result, Write};
|
||||||
|
@ -33,15 +35,20 @@ mod options {
|
||||||
struct Options {
|
struct Options {
|
||||||
append: bool,
|
append: bool,
|
||||||
ignore_interrupts: bool,
|
ignore_interrupts: bool,
|
||||||
|
ignore_pipe_errors: bool,
|
||||||
files: Vec<String>,
|
files: Vec<String>,
|
||||||
output_error: Option<OutputErrorMode>,
|
output_error: Option<OutputErrorMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum OutputErrorMode {
|
enum OutputErrorMode {
|
||||||
|
/// Diagnose write error on any output
|
||||||
Warn,
|
Warn,
|
||||||
|
/// Diagnose write error on any output that is not a pipe
|
||||||
WarnNoPipe,
|
WarnNoPipe,
|
||||||
|
/// Exit upon write error on any output
|
||||||
Exit,
|
Exit,
|
||||||
|
/// Exit upon write error on any output that is not a pipe
|
||||||
ExitNoPipe,
|
ExitNoPipe,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,32 +56,39 @@ enum OutputErrorMode {
|
||||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
let matches = uu_app().try_get_matches_from(args)?;
|
let matches = uu_app().try_get_matches_from(args)?;
|
||||||
|
|
||||||
|
let append = matches.get_flag(options::APPEND);
|
||||||
|
let ignore_interrupts = matches.get_flag(options::IGNORE_INTERRUPTS);
|
||||||
|
let ignore_pipe_errors = matches.get_flag(options::IGNORE_PIPE_ERRORS);
|
||||||
|
let output_error = if matches.contains_id(options::OUTPUT_ERROR) {
|
||||||
|
match matches
|
||||||
|
.get_one::<String>(options::OUTPUT_ERROR)
|
||||||
|
.map(String::as_str)
|
||||||
|
{
|
||||||
|
Some("warn") => Some(OutputErrorMode::Warn),
|
||||||
|
// If no argument is specified for --output-error,
|
||||||
|
// defaults to warn-nopipe
|
||||||
|
None | Some("warn-nopipe") => Some(OutputErrorMode::WarnNoPipe),
|
||||||
|
Some("exit") => Some(OutputErrorMode::Exit),
|
||||||
|
Some("exit-nopipe") => Some(OutputErrorMode::ExitNoPipe),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
} else if ignore_pipe_errors {
|
||||||
|
Some(OutputErrorMode::WarnNoPipe)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let files = matches
|
||||||
|
.get_many::<String>(options::FILE)
|
||||||
|
.map(|v| v.map(ToString::to_string).collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let options = Options {
|
let options = Options {
|
||||||
append: matches.get_flag(options::APPEND),
|
append,
|
||||||
ignore_interrupts: matches.get_flag(options::IGNORE_INTERRUPTS),
|
ignore_interrupts,
|
||||||
files: matches
|
ignore_pipe_errors,
|
||||||
.get_many::<String>(options::FILE)
|
files,
|
||||||
.map(|v| v.map(ToString::to_string).collect())
|
output_error,
|
||||||
.unwrap_or_default(),
|
|
||||||
output_error: {
|
|
||||||
if matches.get_flag(options::IGNORE_PIPE_ERRORS) {
|
|
||||||
Some(OutputErrorMode::WarnNoPipe)
|
|
||||||
} else if matches.contains_id(options::OUTPUT_ERROR) {
|
|
||||||
if let Some(v) = matches.get_one::<String>(options::OUTPUT_ERROR) {
|
|
||||||
match v.as_str() {
|
|
||||||
"warn" => Some(OutputErrorMode::Warn),
|
|
||||||
"warn-nopipe" => Some(OutputErrorMode::WarnNoPipe),
|
|
||||||
"exit" => Some(OutputErrorMode::Exit),
|
|
||||||
"exit-nopipe" => Some(OutputErrorMode::ExitNoPipe),
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Some(OutputErrorMode::WarnNoPipe)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match tee(&options) {
|
match tee(&options) {
|
||||||
|
@ -140,7 +154,6 @@ pub fn uu_app() -> Command {
|
||||||
.help("exit on write errors to any output that are not pipe errors (equivalent to exit on non-unix platforms)"),
|
.help("exit on write errors to any output that are not pipe errors (equivalent to exit on non-unix platforms)"),
|
||||||
]))
|
]))
|
||||||
.help("set write error behavior")
|
.help("set write error behavior")
|
||||||
.conflicts_with(options::IGNORE_PIPE_ERRORS),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,6 +190,11 @@ fn tee(options: &Options) -> Result<()> {
|
||||||
inner: Box::new(stdin()) as Box<dyn Read>,
|
inner: Box::new(stdin()) as Box<dyn Read>,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if options.ignore_pipe_errors && !ensure_stdout_not_broken()? && output.writers.len() == 1 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let res = match copy(input, &mut output) {
|
let res = match copy(input, &mut output) {
|
||||||
// ErrorKind::Other is raised by MultiWriter when all writers
|
// ErrorKind::Other is raised by MultiWriter when all writers
|
||||||
// have exited, so that copy will abort. It's equivalent to
|
// have exited, so that copy will abort. It's equivalent to
|
||||||
|
@ -367,3 +385,44 @@ 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::{fstat, SFlag},
|
||||||
|
};
|
||||||
|
use std::os::fd::{AsFd, AsRawFd};
|
||||||
|
|
||||||
|
let out = stdout();
|
||||||
|
|
||||||
|
// First, check that stdout is a fifo and return true if it's not the case
|
||||||
|
let stat = fstat(out.as_raw_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
|
||||||
|
let res = nix::poll::poll(&mut pfds, PollTimeout::NONE)?;
|
||||||
|
|
||||||
|
if res > 0 {
|
||||||
|
// poll succeeded;
|
||||||
|
let error = pfds.iter().any(|pfd| {
|
||||||
|
if let Some(revents) = pfd.revents() {
|
||||||
|
revents.contains(PollFlags::POLLERR)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Ok(!error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if res == 0, it means that timeout was reached, which is impossible
|
||||||
|
// because we set infinite timeout.
|
||||||
|
// And if res < 0, the nix wrapper should have sent back an error.
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
|
|
@ -165,6 +165,7 @@ mod linux_only {
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::process::{Output, Stdio};
|
use std::process::{Output, Stdio};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
fn make_broken_pipe() -> File {
|
fn make_broken_pipe() -> File {
|
||||||
use libc::c_int;
|
use libc::c_int;
|
||||||
|
@ -183,6 +184,22 @@ mod linux_only {
|
||||||
unsafe { File::from_raw_fd(fds[1]) }
|
unsafe { File::from_raw_fd(fds[1]) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn make_hanging_read() -> File {
|
||||||
|
use libc::c_int;
|
||||||
|
use std::os::unix::io::FromRawFd;
|
||||||
|
|
||||||
|
let mut fds: [c_int; 2] = [0, 0];
|
||||||
|
assert!(
|
||||||
|
(unsafe { libc::pipe(std::ptr::from_mut::<c_int>(&mut fds[0])) } == 0),
|
||||||
|
"Failed to create pipe"
|
||||||
|
);
|
||||||
|
|
||||||
|
// PURPOSELY leak the write end of the pipe, so the read end hangs.
|
||||||
|
|
||||||
|
// Return the read end of the pipe
|
||||||
|
unsafe { File::from_raw_fd(fds[0]) }
|
||||||
|
}
|
||||||
|
|
||||||
fn run_tee(proc: &mut UCommand) -> (String, Output) {
|
fn run_tee(proc: &mut UCommand) -> (String, Output) {
|
||||||
let content = (1..=100_000).fold(String::new(), |mut output, x| {
|
let content = (1..=100_000).fold(String::new(), |mut output, x| {
|
||||||
let _ = writeln!(output, "{x}");
|
let _ = writeln!(output, "{x}");
|
||||||
|
@ -535,4 +552,31 @@ mod linux_only {
|
||||||
expect_failure(&output, "No space left");
|
expect_failure(&output, "No space left");
|
||||||
expect_short(file_out_a, &at, content.as_str());
|
expect_short(file_out_a, &at, content.as_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pipe_mode_broken_pipe_only() {
|
||||||
|
new_ucmd!()
|
||||||
|
.timeout(Duration::from_secs(1))
|
||||||
|
.arg("-p")
|
||||||
|
.set_stdin(make_hanging_read())
|
||||||
|
.set_stdout(make_broken_pipe())
|
||||||
|
.succeeds();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pipe_mode_broken_pipe_file() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
let file_out_a = "tee_file_out_a";
|
||||||
|
|
||||||
|
let proc = ucmd
|
||||||
|
.arg("-p")
|
||||||
|
.arg(file_out_a)
|
||||||
|
.set_stdout(make_broken_pipe());
|
||||||
|
|
||||||
|
let (content, output) = run_tee(proc);
|
||||||
|
|
||||||
|
expect_success(&output);
|
||||||
|
expect_correct(file_out_a, &at, content.as_str());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue