通过管道捕获标准输出和标准错误

4
我想要从一个子进程中读取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

这段代码只读取标准输出:
out: on stdout

如果我在这段代码中删除所有与标准输出相关的内容,只留下标准错误输出,它就会仅读取标准错误输出:
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-nightly (322d7f7b9 2018-02-25)。
1个回答

13
当我在Linux下运行这个程序时,它会每隔大约0.1秒打印一行来自stdout的输出,直到读取完所有100行。然后将立即打印来自stderr的100行,最后输出调用程序的退出代码并终止。
从管道中读取数据时,默认情况下,如果没有传入数据,你的程序将被阻塞直到有数据可用。当另一个程序终止或决定关闭其管道的一端时,如果你在已经读取了其他程序发送的所有内容后仍从管道中读取数据,则读取将返回零字节长度,表示“文件结束”(即与普通文件相同的机制)。
当程序向管道写入数据时,操作系统将存储数据在缓冲区中,直到管道的另一端读取它。该缓冲区具有有限的大小,因此如果缓冲区已满,则写入将被阻塞。例如,其中一端正在从stdout读取时,另一端正在向stderr写入而被阻塞。你发布的shell脚本没有输出足够的数据来阻塞,但是如果我将计数器更改为从10000开始,则在我的系统上它在5632处阻塞,因为stderr已满,因为Rust程序尚未开始读取它。
我知道两种解决此问题的方法:
1. 将管道设置为非阻塞模式。非阻塞模式意味着如果读取或写入会被阻塞,则它会立即返回带有指示这种情况的不同错误代码。当此条件发生时,你可以切换到下一个管道并尝试那个管道。为了避免在两个管道都没有数据时消耗所有CPU,通常要使用类似poll的函数等待任何一个管道有数据。
Rust标准库不会为这些管道公开非阻塞模式,但它提供了方便的wait_with_output方法,正如我刚才所描述的!但是,正如其名称所示,它仅在程序结束时返回。此外,stdout和stderr将被读入Vec中,因此如果输出很大,则你的程序将会占用大量内存;你无法以流式处理数据。
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);
}
如果您想手动使用非阻塞模式,您可以在类Unix系统上使用AsRawFd恢复文件描述符,在Windows上使用AsRawHandle恢复文件处理句柄,然后将它们传递给适当的操作系统API。

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

  • 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);
    }
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接