How to tee stdout/stderr from a subprocess in Rust

Viewed 597

I'd like to capture stdout/stderr of a subprocess but also stream the outputs to my process' stdout/stderr as well. In bash I'd do something like:

my_command > >(tee /tmp/capture.out) 2> >(tee /tmp/capture.err >&2)

Is there any reasonably concise or typical way to do this in Rust? Looking at std::process::Child.wait_with_output() and sys::pipe::read2 handling stdout/stderr directly seems somewhat involved (and involves an unsafe call to poll).

Another way of framing the question might be "what's the expected way to stream over a subprocess' stdout and stderr?" which would at least allow me to manually capture and print the streams' contents.

I'm currently using std::process but if it's easier with the subprocess crate I'd be willing to swap to that.

2 Answers

Something along those lines?

use std::{
    fs::File,
    io::{stderr, stdout, Read, Write},
    process::{Command, Stdio},
    thread,
};

fn main() {
    let mut child = Command::new("/usr/bin/date")
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("failed to execute child");

    fn communicate(
        mut stream: impl Read,
        filename: &'static str,
        mut output: impl Write,
    ) -> std::io::Result<()> {
        let mut file = File::create(filename)?;

        let mut buf = [0u8; 1024];
        loop {
            let num_read = stream.read(&mut buf)?;
            if num_read == 0 {
                break;
            }

            let buf = &buf[..num_read];
            file.write_all(buf)?;
            output.write_all(buf)?;
        }

        Ok(())
    }

    let child_out = std::mem::take(&mut child.stdout).expect("cannot attach to child stdout");
    let child_err = std::mem::take(&mut child.stderr).expect("cannot attach to child stderr");

    let thread_out = thread::spawn(move || {
        communicate(child_out, "stdout.txt", stdout())
            .expect("error communicating with child stdout")
    });
    let thread_err = thread::spawn(move || {
        communicate(child_err, "stderr.txt", stderr())
            .expect("error communicating with child stderr")
    });

    thread_out.join().unwrap();
    thread_err.join().unwrap();

    let ecode = child.wait().expect("failed to wait on child");

    assert!(ecode.success());
}

Output:

Tue Jul  5 01:07:01 CEST 2022

stdout.txt:

Tue Jul  5 01:07:01 CEST 2022

handling stdout/stderr directly seems somewhat involved (and involves an unsafe call to poll

It's probably going to be involved either way as Rust doesn't really have abstractions over complicated pipe & fd operations and this requires a fair number of them: creating two pipes and doing a lot of dup'ing around to properly set all the input and output streams to match.

The "easy" way to do this (it's not actually easy but it avoids having to deal with libc and fds) would be to spawn threads to explicitly copy data around between the various bits.

Related