Run command, stream stdout/stderr and capture results - rust

I'm trying to use std::process::Command to run a command and stream its stdout and stderr while also capturing a copy of stdout/stderr. I found I can use spawn.
This code will capture the output, but won't stream it to stdout/stderr while it's happening:
let mut child = command
.envs(env)
.stdout(Stdio::piped()) // <=== Difference here
.spawn()
.unwrap();
let output = child
.wait_with_output().unwrap();
println!("Done {}", std::str::from_utf8(&output.stdout).unwrap());
This code will stream the output but not capture it:
let mut child = command
.envs(env)
.spawn()
.unwrap();
let output = child
.wait_with_output().unwrap();
println!("Done {}", std::str::from_utf8(&output.stdout).unwrap());
Is there a way to capture a command's output while also streaming it to the parent stdout/stderr?

There might be a less verbose way to do this, but this is the solution I came up with.
Spawn the process with a piped io for stdout and stderr. Spawn a thread for stdout and stderr. In each thread read from the pipe and output directly to stdout or stderr then write the contents to a channel.
In the main thread wait for the process to finish, then join the threads and finally read each channel to get the contents of stdout and stderr.
use std::io::BufRead;
let mut child = command
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let child_stdout = child
.stdout
.take()
.expect("Internal error, could not take stdout");
let child_stderr = child
.stderr
.take()
.expect("Internal error, could not take stderr");
let (stdout_tx, stdout_rx) = std::sync::mpsc::channel();
let (stderr_tx, stderr_rx) = std::sync::mpsc::channel();
let stdout_thread = thread::spawn(move || {
let stdout_lines = BufReader::new(child_stdout).lines();
for line in stdout_lines {
let line = line.unwrap();
println!("{}", line);
stdout_tx.send(line).unwrap();
}
});
let stderr_thread = thread::spawn(move || {
let stderr_lines = BufReader::new(child_stderr).lines();
for line in stderr_lines {
let line = line.unwrap();
eprintln!("{}", line);
stderr_tx.send(line).unwrap();
}
});
let status = child
.wait()
.expect("Internal error, failed to wait on child");
stdout_thread.join().unwrap();
stderr_thread.join().unwrap();
let stdout = stdout_rx.into_iter().collect::<Vec<String>>().join("");
let stderr = stderr_rx.into_iter().collect::<Vec<String>>().join("");
The channel isn't strictly needed. I originally wanted to mutate a string, but I'm new in Rust with threads and couldn't find any examples showing how to mutate a string in a thread and then read it back into main.
I'm accepting the other solution as it really answered my main question. I just wanted to post back to give everyone a fully-featured answer that does exactly what I originally asked

This is similar to how I stream the compilation and execution output on Rust Explorer.
To stream the output you can pipe the stdout and read it line by line using BufReader.
Playground
use std::io::BufRead;
use std::io::BufReader;
use std::process::Command;
use std::process::Stdio;
fn main() {
// Compile code.
let mut child = Command::new("bash")
.args([
"-c",
"echo 'Hello'; sleep 3s; echo 'World'"
])
.stdout(Stdio::piped())
.spawn()
.unwrap();
let stdout = child.stdout.take().unwrap();
// Stream output.
let lines = BufReader::new(stdout).lines();
for line in lines {
println!("{}", line.unwrap());
}
}

Related

Command child process output stream

I have an Actix Web application and what I want to achieve is that making an interactive command in one of my routes.
Run the command and stream
Get the first data that is streamed
Return that to the user without killing the child process, let it keep running until it exits.
Rust child process not correctly streaming the data. For example I have the below code:
let mut command_output = command
.arg("arg1")
.arg("arg2")
.arg("arg3")
.spawn()
.unwrap();
match &mut command_output.stdout {
_ => {
// ...
}
}
println!("the data {:?}", command_output.stdout);
let status = command_output.wait();
I want that child process to stream the data. Right now, firstly there is one text printed to terminal that says "the data None" then one other line is printed with the output that I was expecting but it's not my println! I don't know where does that comes from, probably that is internal but I need to access that data, it is not printed within my println!.
I tried to read the stdout with BufReader and changed command.stdout() to different values like inherit and piped but still streaming is not working as expected. I can't println that value, it's printed by the process internally I think.
In order to capture output from a child process, you need to call Command::stdout supplying the argument Stdio::piped().
If you do not do that, the child process will inherit the same stdout as the parent process, and the output is displayed on the terminal instead of being captured by the parent process. In that scenario, the child's stdout will be None, which is what you are getting when you print it.
If you do call command.stdout(Stdio::piped()) then the child's stdout will be Some(stdout) where stdout is an instance of std::process::ChildStdout. This struct implements the Read trait, so you can read from it. For example, to read line by line:
let mut command = Command::new("ls")
.arg("-ltr")
.stdout(Stdio::piped())
.spawn()
.unwrap();
let stdout = command.stdout.take().unwrap();
let mut bufread = BufReader::new(stdout);
let mut buf = String::new();
while let Ok(n) = bufread.read_line(&mut buf) {
if n > 0 {
println!("Line: {}", buf.trim());
buf.clear();
} else {
break;
}
}
Playground

How to save command stdout to a file?

I'm writing a function that has a particular logging requirement. I want to capture the output from a Command::new() call and save it into a file. I'm just using echo here for simplicity's sake.
fn sys_command(id: &str) -> u64 {
let mut cmd = Command::new("echo")
.args(&[id])
.stdout(Stdio::piped())
.spawn()
.expect("failed to echo");
let stdout = cmd.stdout.as_mut().unwrap();
let stdout_reader = BufReader::new(stdout);
// let log_name = format!("./tmp/log/{}.log", id);
// fs::write(log_name, &stdout_reader);
println!("{:?}", stdout_reader.buffer());
cmd.wait().expect("failed to call");
id.parse::<u64>().unwrap()
}
How can I capture the output and save it to a file? I've made a playground here. My println! call returns [].
Even reading to another buffer prints the wrong value. Below, sys_command("10") prints 3. Here's an updated playground.
fn sys_command(id: &str) -> u64 {
let mut buffer = String::new();
let mut cmd = Command::new("echo")
.args(&[id])
.stdout(Stdio::piped())
.spawn()
.expect("failed to echo");
let stdout = cmd.stdout.as_mut().unwrap();
let mut stdout_reader = BufReader::new(stdout);
let result = stdout_reader.read_to_string(&mut buffer);
println!("{:?}", result.unwrap());
cmd.wait().expect("failed to draw");
id.parse::<u64>().unwrap()
}
What am I missing?
Instead of capturing the output in memory and then writing it to a file, you should just redirect the process's output to a file. This is simpler and more efficient.
let log_name = format!("./tmp/log/{}.log", id);
let log = File::create(log_name).expect("failed to open log");
let mut cmd = Command::new("echo")
.args(&[id])
.stdout(log)
.spawn()
.expect("failed to start echo");
cmd.wait().expect("failed to finish echo");
Even reading to another buffer prints the wrong value. Below, sys_command("10") prints 3.
This is unrelated — read_to_string() returns the number of bytes read, not the contents of them, and there are 3 characters: '1' '0' '\n'.

Writing to stdio & reading from stdout in Rust Command process

I'll try to simplify as much as possible what I'm trying to do accomplish but in a nutshell here is my problem:
I am trying to spawn the node shell as a process in Rust. I would like to pass to the process' stdin javascript code and read the nodejs output from stdout of the process. This would be an interactive usage where the node shell is spawned and keeps receiving JS instructions and executing them.
I do not wish to launch the nodejs app using a file argument.
I have read quite a bit about std::process::Command, tokio and why we can't write and read to a piped input using standard library. One of the solutions that I kept seeing online (in order to not block the main thread while reading/writing) is to use a thread for reading the output. Most solutions did not involve a continuous write/read flow.
What I have done is to spawn 2 threads, one that keeps writing to stdin and one that keeps reading from stdout. That way, I thought, I won't be blocking the main thread. However my issue is that only 1 thread can actively be used. When I have a thread for stdin, stdout does not even receive data.
Here is the code, comments should provide more details
pub struct Runner {
handle: Child,
pub input: Arc<Mutex<String>>,
pub output: Arc<Mutex<String>>,
input_thread: JoinHandle<()>,
output_thread: JoinHandle<()>,
}
impl Runner {
pub fn new() -> Runner {
let mut handle = Command::new("node")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to spawn node process!");
// begin stdout thread part
let mut stdout = handle.stdout.take().unwrap();
let output = Arc::new(Mutex::new(String::new()));
let out_clone = Arc::clone(&output);
let output_thread = spawn(move || loop {
// code here never executes...why ?
let mut buf: [u8; 1] = [0];
let mut output = out_clone.lock().unwrap();
let what_i_read = stdout.read(&mut buf);
println!("reading: {:?}", what_i_read);
match what_i_read {
Err(err) => {
println!("{}] Error reading from stream: {}", line!(), err);
break;
}
Ok(bytes_read) => {
if bytes_read != 0 {
let char = String::from_utf8(buf.to_vec()).unwrap();
output.push_str(char.as_str());
} else if output.len() != 0 {
println!("result: {}", output);
out_clone.lock().unwrap().clear();
}
}
}
});
// begin stdin thread block
let mut stdin = handle.stdin.take().unwrap();
let input = Arc::new(Mutex::new(String::new()));
let input_clone = Arc::clone(&input);
let input_thread = spawn(move || loop {
let mut in_text = input_clone.lock().unwrap();
if in_text.len() != 0 {
println!("writing: {}", in_text);
stdin.write_all(in_text.as_bytes()).expect("!write");
stdin.write_all("\n".as_bytes()).expect("!write");
in_text.clear();
}
});
Runner {
handle,
input,
output,
input_thread,
output_thread,
}
}
// this function should receive commands
pub fn execute(&mut self, str: &str) {
let input = Arc::clone(&self.input);
let mut input = input.lock().unwrap();
input.push_str(str);
}
}
In the main thread I'd like use this as
let mut runner = Runner::new();
runner.execute("console.log('foo'");
println!("{:?}", runner.output);
I am still new to Rust but at least I passed the point where the borrow checker makes me bang my head against the wall, I am starting to find it more pleasing now :)

How to tee stdout/stderr from a subprocess in Rust

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.
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.

How do I prefix Command stdout with [stdout] and [sterr]?

Using the Command struct, how can I add a prefix to the stdout and stderr buffers?
I would like the output to look something like this:
[stdout] things are good.
[sterr] fatal: repository does not exist.
This would also be nice to apply to the program's main stdout so anything the program prints is prefixed like that.
Here is the code I currently have:
let output = Command::new("git").arg("clone").output().unwrap_or_else(|e| {
panic!("Failed to run git clone: {}", e)
});
I don't believe you can do what you truly want to do right now. Ideally, you'd be able to provide an implementor of Write to the Process::stdout method. Unfortunately, the set of choices for Stdio is sparse. Perhaps you can campaign to have this be a feature request for Rust 1.1, or create a crate to start fleshing out some of the details (like cross-platform compatibility)
If it is acceptable to remove the interleaving of stdout / stderr, then this solution could help:
use std::io::{BufRead,BufReader};
use std::process::{Command,Stdio};
fn main() {
let mut child =
Command::new("/tmp/output")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn().unwrap();
if let Some(ref mut stdout) = child.stdout {
for line in BufReader::new(stdout).lines() {
let line = line.unwrap();
println!("[stdout] {}", line);
}
}
if let Some(ref mut stderr) = child.stderr {
for line in BufReader::new(stderr).lines() {
let line = line.unwrap();
println!("[stderr] {}", line);
}
}
let status = child.wait().unwrap();
println!("Finished with status {:?}", status);
}

Resources