8000 tee: remove output buffering by Maximkaaa · Pull Request #8218 · uutils/coreutils · GitHub
[go: up one dir, main page]

Skip to content

tee: remove output buffering #8218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
  • Loading branch information
Maximkaaa committed Jun 19, 2025
commit e354ddea02e631913a12cc6dad26f4903b70bcae
43 changes: 42 additions & 1 deletion src/uu/tee/src/tee.rs
< 8000 tr data-hunk="6db3375e00d490a35a2af36cb2a9e004cb87465e9060bec9ade716293741333e" class="show-top-border">
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -190,6 +190,7 @@
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
Expand All @@ -207,6 +208,46 @@
}
}

/// 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),

Check warning on line 235 in src/uu/tee/src/tee.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/tee/src/tee.rs#L234-L235

Added lines #L234 - L235 were not covered by tests
};

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.
Expand Down
48 changes: 48 additions & 0 deletions tests/by-util/test_tee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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};
Expand Down
Loading
0