How do I read the output of a child process without blocking in Rust?

后端 未结 2 2027
生来不讨喜
生来不讨喜 2020-11-27 19:39

I\'m making a small ncurses application in Rust that needs to communicate with a child process. I already have a prototype written in Common Lisp. I\'m trying to rewrite it

2条回答
  •  野性不改
    2020-11-27 20:18

    Tokio's Command

    Here is an example of using tokio 0.2:

    use std::process::Stdio;
    use futures::StreamExt; // 0.3.1
    use tokio::{io::BufReader, prelude::*, process::Command}; // 0.2.4, features = ["full"]
    
    #[tokio::main]
    async fn main() {
        let mut cmd = Command::new("/tmp/slow.bash")
            .stdout(Stdio::piped()) // Can do the same for stderr
            .spawn()
            .expect("cannot spawn");
    
        let stdout = cmd.stdout().take().expect("no stdout");
        // Can do the same for stderr
    
        // To print out each line
        // BufReader::new(stdout)
        //     .lines()
        //     .for_each(|s| async move { println!("> {:?}", s) })
        //     .await;
    
        // To print out each line *and* collect it all into a Vec
        let result: Vec<_> = BufReader::new(stdout)
            .lines()
            .inspect(|s| println!("> {:?}", s))
            .collect()
            .await;
    
        println!("All the lines: {:?}", result);
    }
    

    Tokio-Threadpool

    Here is an example of using tokio 0.1 and tokio-threadpool. We start the process in a thread using the blocking function. We convert that to a stream with stream::poll_fn

    use std::process::{Command, Stdio};
    use tokio::{prelude::*, runtime::Runtime}; // 0.1.18
    use tokio_threadpool; // 0.1.13
    
    fn stream_command_output(
        mut command: Command,
    ) -> impl Stream, Error = tokio_threadpool::BlockingError> {
        // Ensure that the output is available to read from and start the process
        let mut child = command
            .stdout(Stdio::piped())
            .spawn()
            .expect("cannot spawn");
        let mut stdout = child.stdout.take().expect("no stdout");
    
        // Create a stream of data
        stream::poll_fn(move || {
            // Perform blocking IO
            tokio_threadpool::blocking(|| {
                // Allocate some space to store anything read
                let mut data = vec![0; 128];
                // Read 1-128 bytes of data
                let n_bytes_read = stdout.read(&mut data).expect("cannot read");
    
                if n_bytes_read == 0 {
                    // Stdout is done
                    None
                } else {
                    // Only return as many bytes as we read
                    data.truncate(n_bytes_read);
                    Some(data)
                }
            })
        })
    }
    
    fn main() {
        let output_stream = stream_command_output(Command::new("/tmp/slow.bash"));
    
        let mut runtime = Runtime::new().expect("Unable to start the runtime");
    
        let result = runtime.block_on({
            output_stream
                .map(|d| String::from_utf8(d).expect("Not UTF-8"))
                .fold(Vec::new(), |mut v, s| {
                    print!("> {}", s);
                    v.push(s);
                    Ok(v)
                })
        });
    
        println!("All the lines: {:?}", result);
    }
    

    There's numerous possible tradeoffs that can be made here. For example, always allocating 128 bytes isn't ideal, but it's simple to implement.

    Support

    For reference, here's slow.bash:

    #!/usr/bin/env bash
    
    set -eu
    
    val=0
    
    while [[ $val -lt 10 ]]; do
        echo $val
        val=$(($val + 1))
        sleep 1
    done
    

    See also:

    • How do I synchronously return a value calculated in an asynchronous Future in stable Rust?

提交回复
热议问题