【问题标题】:Capture both stdout & stderr via pipe通过管道捕获标准输出和标准错误
【发布时间】:2018-08-10 06:56:54
【问题描述】:

我想从子进程中同时读取 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 (322d7f7b9 2018-02-25)

【问题讨论】:

    标签: rust pipe stdout stderr


    【解决方案1】:

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

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

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

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

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

      Rust 标准库没有为这些管道公开非阻塞模式,但它提供了方便的 wait_with_output 方法,它完全符合我刚刚描述的功能!但是,顾名思义,它仅在程序结束时返回。还有,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);
      }
      

      如果您想手动使用非阻塞模式,您可以在类 Unix 系统上使用AsRawFd 恢复文件描述符或在 Windows 上使用AsRawHandle 恢复文件句柄,然后您可以将它们传递给相应的操作系统 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);
      }
      

    【讨论】: