捕获 stdout 和 stderr 到管道

Capture both stdout & stderr via pipe

我想从子进程读取 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

如果我删除这段代码中与 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-nightly (322d7f7b9 2018-02-25)

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

当您从管道读取时,如果没有传入数据,默认情况下,您的程序将阻塞,直到有一些数据可用。当另一个程序终止或决定关闭其管道末端时,如果您在读取了其他程序发送的所有内容后从管道读取,则读取长度为 return 零字节,表示 "end of the file"(即它与普通文件的机制相同)。

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

我知道有两种方法可以解决这个问题:

  1. 将管道设置为非阻塞模式。非阻塞模式意味着如果读取或写入会阻塞,它会立即 returns 并用一个明显的错误代码表示这种情况。当这种情况发生时,您可以切换到下一个管道并尝试使用那个管道。为了避免在两个管道都还没有数据时消耗所有 CPU,您通常希望使用像 poll 这样的函数来等待任何一个管道都有数据。

    Rust 标准库没有为这些管道公开非阻塞模式,但它提供了方便的 wait_with_output 方法,它完全符合我刚才描述的内容!但是,顾名思义,程序结束时仅 returns。此外,stdout 和 stderr 被读入 Vecs,因此如果输出很大,您的程序将消耗大量内存;您无法以流方式处理数据。

    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 or the file handle on Windows with AsRawHandle 在 Unix-like 系统上恢复文件描述符,然后将它们传递给适当的操作系统 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);
    }