Proper piping `Write` and `Read` objects to `Child`

Hi! I've stumbled upon a strange problem, that I can't solve.

I have a program that accepts input from stdin and outputs to stdout. I want to pipe those streams with my R: impl Read for stdin and W: impl Write for stdout.

I found a method std::io::copy, and used my streams as stdout/stdin, however the Child program never ends! I even included \x04 - but it didn't work either.

Here is the code (an MWE, this code has function pass_stream, which does what I want, it passes string "Hello, World!\x04" to stdin of cat and reads from stdout of cat):

use std::io::{Cursor, Read, Write};
use std::process::{Command, Stdio};

fn main() {
    let mut input_cursor = Cursor::new("Hello, World!\x04");

    let mut output_vec: Vec<u8> = Vec::new();
    let mut output_cursor = std::io::Cursor::new(&mut output_vec);

    pass_streams("cat", &mut input_cursor, &mut output_cursor);

    println!("{}", String::from_utf8(output_vec).unwrap());
}

fn pass_streams<'r, 'w, R, W>(cmd: &'static str, input: &'r mut R, output: &'w mut W)
where
    R: Read,
    W: Write,
{
    let mut child = Command::new(cmd)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();

    let mut child_input = child.stdin.take().unwrap();
    let mut child_output = child.stdout.take().unwrap();

    std::io::copy(input, &mut child_input).unwrap();
    child_input.flush().unwrap(); // Unnecessary?
    
    child.wait().unwrap();

    println!("Start copying stdout");
    std::io::copy(&mut child_output, output).unwrap();
    println!("End copying stdout");
}

Playground link: Rust Playground

The output is:

Start copying stdout

And the program never ends.

I know, there could be other ways to solve the problem, I can use wait_with_output probably, but still how to use it solely with Read and Write?

It seems that I miss a tiny bit, but I can't google it..

The answer to this earlier thread may help:

1 Like

The link in the previous post will be more useful for actually solving your problem, but to help your understanding of what's going on — '\x04' a.k.a. ^D will not help you here. ^D meaning end-of-file is part of the user interface provided by TTY/PTY (terminal) devices, and elsewhere is just another byte of data. When you are using pipes (or sockets), to signal end-of-file, you must close the write end of the pipe after you are done writing, which in Rust is done by dropping child_input.

(This is the situation on Unix-like systems. Windows may be different in the details, but closing the pipe is still a proper solution.)

3 Likes

Thanks for showing what's actually going on and for dropping! It solved the problems.

I initially thought about this -- maybe there is a special way of closing streams? I found shutdown method for AsyncWriteExt in tokio::fs, however, there was no such method for std::io::Write. Thus, I posted here.

It turned out to be much simpler than I thought :sweat_smile: