6

I'm trying to consume output from a process in Rust. If the process doesn't terminate after a certain time, I want to terminate/kill it. Ideally I'd like to wrap everything in a generator so that I can iterate the output line by line, but I'm not experienced enough for that in Rust yet.

Here's my code (src/main.rs), using the subprocess crate:

use subprocess::{Popen, PopenConfig, Redirection};
use std::io::Read;
use std::time::Duration;


fn main() {
    let mut p = Popen::create(&["bash", "-c", "echo \"Hi There!\"; sleep 1000"], PopenConfig {stdout: Redirection::Pipe, ..Default::default()}, ).unwrap();
    let mut output = p.stdout.take().unwrap();

    let thread = std::thread::spawn(move || {
        let three_secs = Duration::from_secs(3);
        let one_sec = Duration::from_secs(1);
        let r = p.wait_timeout(three_secs).unwrap();
        match r {
            None => {
                println!("Wait timed out, terminating process");
                p.terminate().unwrap();
                let r = p.wait_timeout(one_sec).unwrap();
                match r {
                    None => {
                        println!("Termination didn't seem to work, killing");
                        p.kill().unwrap();
                    },
                    Some(_) => {
                        println!("Terminated process successfully");
                    },
                }
                p.wait().unwrap();},
            Some(_) => { println!("Process returned");},
        }
        println!("Everything fine");
    });

    println!("Starting to read output");
    let mut output_str = String::new();
    output.read_to_string(&mut output_str).unwrap();
    println!("Done reading output");
    thread.join().unwrap();

    println!("Output: {}", output_str);
    println!("Hello, world!");
}

I'd expect the following output:

Starting to read output
Wait timed out, terminating process
Terminated process successfully
Everything fine
Done reading output
Output: Hi There!

Hello, world!

and the process finishing after three seconds. But what I get is

Starting to read output
Wait timed out, terminating process
Terminated process successfully
Everything fine

and the process doesn't terminate.

For completeness, here is my Cargo.toml to go along with the src/main.rs from above:

[package]
name = "subproc"
version = "0.1.0"
authors = ["<snip>"]
edition = "2018"

[dependencies]
subprocess = "0.2.4"
maqong
  • 71
  • 1
  • 3
  • Welcome to Stack Overflow! It looks like your question might be answered by the answers of [How to query a child process status regularly](https://stackoverflow.com/q/43705010/155423); [How do I read the output of a child process without blocking in Rust?](https://stackoverflow.com/q/34611742/155423); [How to read subprocess output asynchronously](https://stackoverflow.com/q/49245907/155423). If not, please **[edit]** your question to explain the differences. Otherwise, we can mark this question as already answered. – Shepmaster Jun 01 '20 at 13:39

2 Answers2

4

I'd look for a crate to help you do this.

Perhaps something like this: https://docs.rs/wait-timeout/0.2.0/wait_timeout/

Here's the example code adapted to capture stdout and iterate over it line-by-line:

use std::io::Read;
use std::process::{Command, Stdio};
use std::time::Duration;
use wait_timeout::ChildExt;

fn main() {
    let mut child = Command::new("sh")
        .arg("-c")
        .arg("while true; do date; sleep 1; done")
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();

    let secs = Duration::from_secs(5);
    let _status_code = match child.wait_timeout(secs).unwrap() {
        Some(status) => status.code(),
        None => {
            child.kill().unwrap();
            child.wait().unwrap().code()
        }
    };

    let mut s = String::new();
    child.stdout.unwrap().read_to_string(&mut s).unwrap();

    for (num, line) in s.split("\n").enumerate() {
        println!("{}: {}", num, line);
    }
}

Prints:

0: Mon Jun  1 14:42:06 BST 2020
1: Mon Jun  1 14:42:07 BST 2020
2: Mon Jun  1 14:42:08 BST 2020
3: Mon Jun  1 14:42:09 BST 2020
4: Mon Jun  1 14:42:10 BST 2020
5: 

If you wanted to do other work whilst the child runs, you'd have to use an event loop or a thread.

Edd Barrett
  • 3,425
  • 2
  • 29
  • 48
  • 1
    Thank you, your solution works, but you'll only get the output from the process once the process has terminated or the timeout been reached. If the process fills up the pipe, it will stall and you won't get the rest of the output. I'll post the code that I settled on as another answer. – maqong Jun 02 '20 at 15:48
  • Ah,yes, if you want the data as it comes, you'll need an event loop or a thread. I didn't realise that was a requirement. Anyway, glad you found a solution. – Edd Barrett Jun 02 '20 at 18:59
1

With the help of Jack O'Connor, the author of the os_pipe library, I managed to write a solution that will read the process output, and do the timeout waiting and killing in another thread. Be advised that this solution will only kill the launched process, not its children, you'll need more handling if your child process has children itself.

use std::process::{Command, Stdio};
use std::io::{BufRead, BufReader};
use std::thread;
use std::time;

fn main() {
    const TIMEOUT : i32 = 5;
    let mut cmd = Command::new("bash");
    cmd.arg("-c").arg("for ((i=1000; i > 0; i--)); do echo \"$i bottles of beer on the wall\"; sleep 1; done");
    cmd.stdout(Stdio::piped());
    let mut child = cmd.spawn().unwrap();
    let stdout = child.stdout.take().unwrap();


    let thread = thread::spawn(move || {
        for _ in 0..TIMEOUT {
            if let Ok(Some(_)) = child.try_wait() {
                return;
            }
            thread::sleep(time::Duration::from_secs(1));
        }

        child.kill().unwrap();
    });

    let reader = BufReader::new(stdout);
    for line in reader.lines() {
        println!("line: {}", line.unwrap());
    }
    thread.join().unwrap();

    println!("Hello, world!");
}
maqong
  • 71
  • 1
  • 3