From e354ddea02e631913a12cc6dad26f4903b70bcae Mon Sep 17 00:00:00 2001 From: Maxim Date: Wed, 18 Jun 2025 23:51:12 +0900 Subject: [PATCH] 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. --- src/uu/tee/src/tee.rs | 43 ++++++++++++++++++++++++++++++++++- tests/by-util/test_tee.rs | 48 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index 2adeb5e73..7ac4e5710 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -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 { + // 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. diff --git a/tests/by-util/test_tee.rs b/tests/by-util/test_tee.rs index 1c42faef3..4dba33342 100644 --- a/tests/by-util/test_tee.rs +++ b/tests/by-util/test_tee.rs @@ -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};