Skip to content
Open
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
96 changes: 61 additions & 35 deletions src/uu/tee/src/tee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore nopipe
// spell-checker:ignore memfd nopipe

use std::ffi::OsString;
use std::fs::OpenOptions;
Expand Down Expand Up @@ -149,7 +149,7 @@
const BUF_SIZE: usize = 32 * 1024;
#[cfg(all(unix, not(target_os = "fuchsia")))]
if uucore::signals::stdin_was_closed() {
let e = Error::from(rustix::io::Errno::BADF);

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

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

WARNING: `cspell`: Unknown word 'BADF' (file:'src/uu/tee/src/tee.rs', line:152)
show_error!(
"{}",
translate!("tee-error-stdin", "error" => strip_errno(&e))
Expand All @@ -160,55 +160,81 @@
let input = rustix::stdio::stdin();
#[cfg(not(any(unix, target_os = "wasi")))]
let mut input = io::stdin();
// needs 2 pipes to duplicate input multiple times
// freeze by max size since system allocates pipe smaller than default size when user has pages more than fs.pipe-user-pages-soft
#[cfg(any(target_os = "linux", target_os = "android"))]
if let Ok((mut pipe_read, mut pipe_write)) = uucore::pipes::pipe::<true>()
&& let Ok((mut pipe2_read, mut pipe2_write)) = uucore::pipes::pipe::<true>()
if let [writer] = &mut self.writers[..] {
match uucore::pipes::splice_unbounded_auto(&input, writer) {
Ok(Ok(())) => return Ok(()),
Ok(Err(())) => {} // no splice support
Err(e) => process_error(
self.output_error_mode,
e.into(),
writer,
&mut self.ignored_errors,
)?,
}
// For 2+ outputs, we need to own page caches of input for the case input was changed after writing to 1st writer
// todo: should we allocate MAX_ROOTLESS_PIPE_SIZE memfd at here?
} else if let Ok(memfd) = rustix::fs::memfd_create("", rustix::fs::MemfdFlags::empty())
&& let Ok((mut pipe_reader, mut pipe_writer)) = uucore::pipes::pipe::<false>()
{
use uucore::pipes::MAX_ROOTLESS_PIPE_SIZE;
// improve throughput when stdout is pipe, but no needed to be same size with broker.
use uucore::pipes::{MAX_ROOTLESS_PIPE_SIZE, splice};
// improve throughput when stdout is pipe, but no needed to guarantee same size with broker.
let _ = rustix::pipe::fcntl_setpipe_size(&self.writers[0], MAX_ROOTLESS_PIPE_SIZE);
macro_rules! splice_or_detach {
($pipe_read:ident, $pipe_write:ident, $writer:expr, $len:expr) => {
if let Err(e) = uucore::pipes::drain_pipe(&$pipe_read, &$writer, $len) {
while let Ok(s_in) = splice(&input, &pipe_writer, MAX_ROOTLESS_PIPE_SIZE) {
use rustix::pipe::{SpliceFlags, splice as splice_off};
if s_in == 0 {
return Ok(());
}
// Own page caches. This is not a zero-copy, but it still copies in kernel.
let mut remaining = s_in;
let mut off = 0;
while remaining > 0 {
remaining -= splice_off(
&pipe_reader,
None,
&memfd,
Some(&mut off),
remaining,
SpliceFlags::empty(),
)
.expect("copy between private fds failed");
}
for writer in &mut self.writers {
let mut remaining = s_in;
let mut off = 0;
while remaining > 0 {
remaining -= splice_off(
&memfd,
Some(&mut off),
&pipe_writer,
None,
remaining,
SpliceFlags::empty(),
)
.expect("splice between private fds failed");
}
if let Err(e) = uucore::pipes::drain_pipe(&pipe_reader, &writer, s_in) {
self.aborted |= process_error(
self.output_error_mode,
e,
&$writer,
writer,
&mut self.ignored_errors,
).is_err();
$writer.name.clear(); //mark as exited
// the failed write can leave bytes in the pipe: replace it with an empty one.
)
.is_err();
writer.name.clear(); //mark as exited
// pipes can be dirty. Regenerate them.
// Free it first, so that the new one does not need more file descriptors.
drop($pipe_read);
drop($pipe_write);
match uucore::pipes::pipe::<true>() {
Ok(pipe) => ($pipe_read, $pipe_write) = pipe,
drop(pipe_reader);
drop(pipe_writer);
match uucore::pipes::pipe::<false>() {
Ok(pipe) => (pipe_reader, pipe_writer) = pipe,
Err(e) => {
show_error!("{}", strip_errno(&e));
return Err(());
}
}
}
};
}
while let Ok(s) = uucore::pipes::splice(&input, &pipe_write, MAX_ROOTLESS_PIPE_SIZE) {
if s == 0 {
return Ok(());
}
let Some((last, others)) = self.writers.split_last_mut() else {
// all writers exited
return Ok(());
};
for other in others {
// do not consume input
let tee_res = uucore::pipes::tee(&pipe_read, &pipe2_write, s);
assert_eq!(tee_res, Ok(s), "2nd pipe should have enough spare");
splice_or_detach!(pipe2_read, pipe2_write, *other, s);
}
// last one consumes input
splice_or_detach!(pipe_read, pipe_write, *last, s);
self.writers.retain(|w| !w.name.is_empty());
if self.aborted {
return Err(());
Expand Down
2 changes: 2 additions & 0 deletions tests/by-util/test_tee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -752,13 +752,15 @@ mod linux_only {
}

#[test]
#[ignore = "this test was made when we were not using memfd which accidentally gets EFBIG. Needed to rewrite to exclude memfd."]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abendrothj Is there any way to exclude memfd from rlimit?

fn test_tee_failed_output_does_not_spill_into_others() {
check_failed_outputs_do_not_spill(None);
}

// Just enough file descriptors for stdio, the 3 outputs and tee's 2 pipes: a
// failed output's pipe has to be freed before it is replaced.
#[test]
#[ignore = "this test was made when we were not using memfd which accidentally gets EFBIG. Needed to rewrite to exclude memfd."]
fn test_tee_failed_output_at_open_file_limit() {
check_failed_outputs_do_not_spill(Some(10));
}
Expand Down
Loading