|
3 | 3 | // For the full copyright and license information, please view the LICENSE |
4 | 4 | // file that was distributed with this source code. |
5 | 5 |
|
| 6 | +// cSpell:ignore POLLERR POLLRDBAND pfds revents |
| 7 | + |
6 | 8 | use clap::{builder::PossibleValue, crate_version, Arg, ArgAction, Command}; |
7 | 9 | use std::fs::OpenOptions; |
8 | 10 | use std::io::{copy, stdin, stdout, Error, ErrorKind, Read, Result, Write}; |
@@ -33,48 +35,60 @@ mod options { |
33 | 35 | struct Options { |
34 | 36 | append: bool, |
35 | 37 | ignore_interrupts: bool, |
| 38 | + ignore_pipe_errors: bool, |
36 | 39 | files: Vec<String>, |
37 | 40 | output_error: Option<OutputErrorMode>, |
38 | 41 | } |
39 | 42 |
|
40 | 43 | #[derive(Clone, Debug)] |
41 | 44 | enum OutputErrorMode { |
| 45 | + /// Diagnose write error on any output |
42 | 46 | Warn, |
| 47 | + /// Diagnose write error on any output that is not a pipe |
43 | 48 | WarnNoPipe, |
| 49 | + /// Exit upon write error on any output |
44 | 50 | Exit, |
| 51 | + /// Exit upon write error on any output that is not a pipe |
45 | 52 | ExitNoPipe, |
46 | 53 | } |
47 | 54 |
|
48 | 55 | #[uucore::main] |
49 | 56 | pub fn uumain(args: impl uucore::Args) -> UResult<()> { |
50 | 57 | let matches = uu_app().try_get_matches_from(args)?; |
51 | 58 |
|
| 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 | + |
52 | 86 | 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, |
78 | 92 | }; |
79 | 93 |
|
80 | 94 | match tee(&options) { |
@@ -140,7 +154,6 @@ pub fn uu_app() -> Command { |
140 | 154 | .help("exit on write errors to any output that are not pipe errors (equivalent to exit on non-unix platforms)"), |
141 | 155 | ])) |
142 | 156 | .help("set write error behavior") |
143 | | - .conflicts_with(options::IGNORE_PIPE_ERRORS), |
144 | 157 | ) |
145 | 158 | } |
146 | 159 |
|
@@ -177,6 +190,11 @@ fn tee(options: &Options) -> Result<()> { |
177 | 190 | inner: Box::new(stdin()) as Box<dyn Read>, |
178 | 191 | }; |
179 | 192 |
|
| 193 | + #[cfg(target_os = "linux")] |
| 194 | + if options.ignore_pipe_errors && !ensure_stdout_not_broken()? && output.writers.len() == 1 { |
| 195 | + return Ok(()); |
| 196 | + } |
| 197 | + |
180 | 198 | let res = match copy(input, &mut output) { |
181 | 199 | // ErrorKind::Other is raised by MultiWriter when all writers |
182 | 200 | // have exited, so that copy will abort. It's equivalent to |
@@ -367,3 +385,44 @@ impl Read for NamedReader { |
367 | 385 | } |
368 | 386 | } |
369 | 387 | } |
| 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 | +} |
0 commit comments