mirror of
https://github.com/uutils/coreutils.git
synced 2025-07-07 21:45:01 +00:00
tee: remove output buffering
To comply with POSIX standard `tee` implementation must not buffer its output, so we replace std::io::copy implementation that does buffering with the custom one.
This commit is contained in:
parent
2ba6c32687
commit
e354ddea02
2 changed files with 90 additions and 1 deletions
|
@ -7,7 +7,7 @@
|
|||
|
||||
use clap::{Arg, ArgAction, Command, builder::PossibleValue};
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{Error, ErrorKind, Read, Result, Write, copy, stdin, stdout};
|
||||
use std::io::{Error, ErrorKind, Read, Result, Write, stdin, stdout};
|
||||
use std::path::PathBuf;
|
||||
use uucore::display::Quotable;
|
||||
use uucore::error::UResult;
|
||||
|
@ -190,6 +190,7 @@ fn tee(options: &Options) -> Result<()> {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
// We cannot use std::io::copy here as it doesn't flush the output buffer
|
||||
let res = match copy(input, &mut output) {
|
||||
// ErrorKind::Other is raised by MultiWriter when all writers
|
||||
// have exited, so that copy will abort. It's equivalent to
|
||||
|
@ -207,6 +208,46 @@ fn tee(options: &Options) -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Copies all bytes from the input buffer to the output buffer.
|
||||
///
|
||||
/// Returns the number of written bytes.
|
||||
fn copy(mut input: impl Read, mut output: impl Write) -> Result<usize> {
|
||||
// The implementation for this function is adopted from the generic buffer copy implementation from
|
||||
// the standard library:
|
||||
// https://github.com/rust-lang/rust/blob/2feb91181882e525e698c4543063f4d0296fcf91/library/std/src/io/copy.rs#L271-L297
|
||||
|
||||
// Use buffer size from std implementation:
|
||||
// https://github.com/rust-lang/rust/blob/2feb91181882e525e698c4543063f4d0296fcf91/library/std/src/sys/io/mod.rs#L44
|
||||
// spell-checker:ignore espidf
|
||||
const DEFAULT_BUF_SIZE: usize = if cfg!(target_os = "espidf") {
|
||||
512
|
||||
} else {
|
||||
8 * 1024
|
||||
};
|
||||
|
||||
let mut buffer = [0u8; DEFAULT_BUF_SIZE];
|
||||
let mut len = 0;
|
||||
|
||||
loop {
|
||||
let received = match input.read(&mut buffer) {
|
||||
Ok(bytes_count) => bytes_count,
|
||||
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
if received == 0 {
|
||||
return Ok(len);
|
||||
}
|
||||
|
||||
output.write_all(&buffer[0..received])?;
|
||||
|
||||
// We need to flush the buffer here to comply with POSIX requirement that
|
||||
// `tee` does not buffer the input.
|
||||
output.flush()?;
|
||||
len += received;
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to open the indicated file and return it. Reports an error if that's not possible.
|
||||
/// If that error should lead to program termination, this function returns Some(Err()),
|
||||
/// otherwise it returns None.
|
||||
|
|
|
@ -10,6 +10,8 @@ use uutests::{at_and_ucmd, new_ucmd, util_name};
|
|||
use regex::Regex;
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::fmt::Write;
|
||||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
|
||||
// tests for basic tee functionality.
|
||||
// inspired by:
|
||||
|
@ -134,6 +136,52 @@ fn test_readonly() {
|
|||
assert_eq!(at.read(writable_file), content_tee);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tee_output_not_buffered() {
|
||||
// POSIX says: The tee utility shall not buffer output
|
||||
|
||||
// If the output is buffered, the test will hang, so we run it in
|
||||
// a separate thread to stop execution by timeout.
|
||||
let handle = std::thread::spawn(move || {
|
||||
let content = "a";
|
||||
let file_out = "tee_file_out";
|
||||
|
||||
let (at, mut ucmd) = at_and_ucmd!();
|
||||
let mut child = ucmd
|
||||
.arg(file_out)
|
||||
.set_stdin(Stdio::piped())
|
||||
.set_stdout(Stdio::piped())
|
||||
.run_no_wait();
|
||||
|
||||
// We write to the input pipe, but do not close it. If the output is
|
||||
// buffered, reading from output pipe will hang indefinitely, as we
|
||||
// will never write anything else to it.
|
||||
child.write_in(content.as_bytes());
|
||||
|
||||
let out = String::from_utf8(child.stdout_exact_bytes(1)).unwrap();
|
||||
assert_eq!(&out, content);
|
||||
|
||||
// Writing to a file may take a couple hundreds nanoseconds
|
||||
child.delay(1);
|
||||
assert_eq!(at.read(file_out), content);
|
||||
});
|
||||
|
||||
// Give some time for the `tee` to create an output file. Some platforms
|
||||
// take a lot of time to spin up the process and create the output file
|
||||
for _ in 0..100 {
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
if handle.is_finished() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
handle.is_finished(),
|
||||
"Nothing was received through output pipe"
|
||||
);
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux_only {
|
||||
use uutests::util::{AtPath, CmdResult, TestScenario, UCommand};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue