捕获stdout和amp; stderr via pipe

时间:2018-03-02 04:12:29

标签: rust pipe stdout stderr

我想从子进程中读取stderr和stdout,但它不起作用。

main.rs

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

fn main() {
    let mut child = Command::new("./1.sh")
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();

    let out = BufReader::new(child.stdout.take().unwrap());
    let err = BufReader::new(child.stderr.take().unwrap());

    out.lines().for_each(|line|
        println!("out: {}", line.unwrap())
    );
    err.lines().for_each(|line|
        println!("err: {}", line.unwrap())
    );

    let status = child.wait().unwrap();
    println!("{}", status);
}

1.sh

#!/bin/bash
counter=100
while [ $counter -gt 0 ]
do
   sleep 0.1
   echo "on stdout"
   echo "on stderr" >&2
   counter=$(( $counter - 1 ))
done
exit 0

此代码仅读取stdout:

out: on stdout

如果我在此代码中删除与stdout相关的所有内容并且只保留stderr,它将只读取stderr:

let mut child = Command::new("./1.sh")
    .stdout(Stdio::null())
    .stderr(Stdio::piped())
    .spawn()
    .unwrap();

let err = BufReader::new(child.stderr.take().unwrap());

err.lines().for_each(|line|
    println!("err: {}", line.unwrap())
);

可生产

err: on stderr

它似乎可以一次读取stdout或stderr,但不能同时读取。我做错了什么?

我每晚使用Rust 1.26.0(322d7f7b9 2018-02-25)

1 个答案:

答案 0 :(得分:5)

当我在Linux下的计算机上运行这个程序时,会发生的事情是它每隔0.1秒从stdout打印一行,直到读完所有100行,然后立即打印出来自stderr的100行,然后是程序打印被调用程序的退出代码并终止。

当您从管道中读取时,如果没有传入数据,默认情况下,您的程序将阻止,直到某些数据可用。当另一个程序终止或决定关闭它的管道末尾时,如果你在读完其他程序发送的所有内容后从管道中读取,则读取将返回零字节长度,表示“文件结束” “(即它与常规文件的机制相同)。

当程序写入管道时,操作系统会将数据存储在缓冲区中,直到管道的另一端读取它为止。该缓冲区的大小有限,因此如果它已满, write 将被阻止。例如,可能发生的情况是,一端在从stdout读取时阻塞,而另一端在写入stderr时阻塞。您发布的shell脚本没有输出足够的数据来阻止,但如果我将计数器更改为10000,则会在我的系统上阻止5632,因为stderr已满,因为Rust程序还没有开始读取它。 / p>

我知道解决这个问题的两种解决方案:

  1. 将管道设置为非阻塞模式。非阻塞模式意味着如果读取或写入将阻塞,则它会立即返回一个不同的错误代码,表明这种情况。发生这种情况时,您可以切换到下一个管道并尝试该管道。为了避免在两个管道都没有数据时占用所有CPU,通常需要使用poll之类的函数来等待管道中有数据。

    Rust标准库不公开这些管道的非阻塞模式,但它提供了方便的wait_with_output方法,完全按照我刚才描述的方式执行!但是,顾名思义,它只在程序结束时返回。此外,stdout和stderr被读入Vec s,因此如果输出很大,你的程序将消耗大量内存;您无法以流式方式处理数据。

    use std::io::{BufRead, BufReader};
    use std::process::{Command, Stdio};
    
    fn main() {
        let child = Command::new("./1.sh")
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .unwrap();
    
        let output = child.wait_with_output().unwrap();
    
        let out = BufReader::new(&*output.stdout);
        let err = BufReader::new(&*output.stderr);
    
        out.lines().for_each(|line|
            println!("out: {}", line.unwrap());
        );
        err.lines().for_each(|line|
            println!("err: {}", line.unwrap());
        );
    
        println!("{}", output.status);
    }
    

    如果要手动使用非阻塞模式,可以使用AsRawFd在类Unix系统上恢复文件描述符,或者使用AsRawHandle在Windows上恢复文件句柄,然后将这些文件描述符传递给适当的操作系统API。

  2. 在单独的线程上读取每个管道。我们可以继续在主线程上读取其中一个并为另一个管道生成一个线程。

    use std::io::{BufRead, BufReader};
    use std::process::{Command, Stdio};
    use std::thread;
    
    fn main() {
        let mut child = Command::new("./1.sh")
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .unwrap();
    
        let out = BufReader::new(child.stdout.take().unwrap());
        let err = BufReader::new(child.stderr.take().unwrap());
    
        let thread = thread::spawn(move || {
            err.lines().for_each(|line|
                println!("err: {}", line.unwrap());
            );
        });
    
        out.lines().for_each(|line|
            println!("out: {}", line.unwrap());
        );
    
        thread.join().unwrap();
    
        let status = child.wait().unwrap();
        println!("{}", status);
    }