10000 tee: remove output buffering · uutils/coreutils@01b6d24 · GitHub
[go: up one dir, main page]

Skip to content

Commit 01b6d24

Browse files
committed
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.
1 parent 2ba6c32 commit 01b6d24

File tree

2 files changed

+84
-1
lines changed

2 files changed

+84
-1
lines changed

src/uu/tee/src/tee.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
use clap::{Arg, ArgAction, Command, builder::PossibleValue};
99
use std::fs::OpenOptions;
10-
use std::io::{Error, ErrorKind, Read, Result, Write, copy, stdin, stdout};
10+
use std::io::{Error, ErrorKind, Read, Result, Write, stdin, stdout};
1111
use std::path::PathBuf;
1212
use uucore::display::Quotable;
1313
use uucore::error::UResult;
@@ -190,6 +190,7 @@ fn tee(options: &Options) -> Result<()> {
190190
return Ok(());
191191
}
192192

193+
// We cannot use std::io::copy here as it doesn't flush the output buffer
193194
let res = match copy(input, &mut output) {
194195
// ErrorKind::Other is raised by MultiWriter when all writers
195196
// have exited, so that copy will abort. It's equivalent to
@@ -207,6 +208,46 @@ fn tee(options: &Options) -> Result<()> {
207208
}
208209
}
209210

211+
/// Copies all bytes from the input buffer to the output buffer.
212+
///
213+
/// Returns the number of written bytes.
214+
fn copy(mut input: impl Read, mut output: impl Write) -> Result<usize> {
215+
// The implementation for this function is adopted from the generic buffer copy implementation from
216+
// the standard library:
217+
// https://github.com/rust-lang/rust/blob/2feb91181882e525e698c4543063f4d0296fcf91/library/std/src/io/copy.rs#L271-L297
218+
219+
// Use buffer size from std implementation:
220+
// https://github.com/rust-lang/rust/blob/2feb91181882e525e698c4543063f4d0296fcf91/library/std/src/sys/io/mod.rs#L44
221+
// spell-checker:ignore espidf
222+
const DEFAULT_BUF_SIZE: usize = if cfg!(target_os = "espidf") {
223+
512
224+
} else {
225+
8 * 1024
226+
};
227+
228+
let mut buffer = [0u8; DEFAULT_BUF_SIZE];
229+
let mut len = 0;
230+
231+
loop {
232+
let received = match input.read(&mut buffer) {
233+
Ok(bytes_count) => bytes_count,
234+
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
235+
Err(e) => return Err(e),
236+
};
237+
238+
if received == 0 {
239+
return Ok(len);
240+
}
241+
242+
output.write_all(&buffer[0..received])?;
243+
244+
// We need to flush the buffer here to comply with POSIX requirement that
245+
// `tee` does not buffer the input.
246+
output.flush()?;
247+
len += received;
248+
}
249+
}
250+
210251
/// Tries to open the indicated file and return it. Reports an error if that's not possible.
211252
/// If that error should lead to program termination, this function returns Some(Err()),
212253
/// otherwise it returns None.

tests/by-util/test_tee.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use uutests::{at_and_ucmd, new_ucmd, util_name};
1010
use regex::Regex;
1111
#[cfg(target_os = "linux")]
1212
use std::fmt::Write;
13+
use std::process::Stdio;
14+
use std::time::Duration;
1315

1416
// tests for basic tee functionality.
1517
// inspired by:
@@ -134,6 +136,46 @@ fn test_readonly() {
134136
assert_eq!(at.read(writable_file), content_tee);
135137
}
136138

139+
#[test]
140+
fn test_tee_output_not_buffered() {
141+
// POSIX says: The tee utility shall not buffer output
142+
143+
// If the output is buffered, the test will hang, so we run it in
144+
// a separate thread to stop execution by timeout.
145+
let handle = std::thread::spawn(move || {
146+
let content = "a";
147+
let file_out = "tee_file_out";
148+
149+
let (at, mut ucmd) = at_and_ucmd!();
150+
let mut child = ucmd
151+
.arg(file_out)
152+
.set_stdin(Stdio::piped())
153+
.set_stdout(Stdio::piped())
154+
.run_no_wait();
155+
156+
// We write to the input pipe, but do not close it. If the output is
157+
// buffered, reading from output pipe will hang indefinitely, as we
158+
// will never write anything else to it.
159+
child.write_in(content.as_bytes());
160+
161+
let out = String::from_utf8(child.stdout_exact_bytes(1)).unwrap();
162+
assert_eq!(&out, content);
163+
164+
// Writing to a file may take a couple hundreds nanoseconds
165+
child.delay(1);
166+
assert_eq!(at.read(file_out), content);
167+
});
168+
169+
// Give some time for the `tee` to create an output file
170+
std::thread::sleep(Duration::from_millis(10));
171+
172+
assert!(
173+
handle.is_finished(),
174+
"Nothing was received through output pipe"
175+
);
176+
handle.join().unwrap();
177+
}
178+
137179
#[cfg(target_os = "linux")]
138180
mod linux_only {
139181
use uutests::util::{AtPath, CmdResult, TestScenario, UCommand};

0 commit comments

Comments
 (0)
0