Piping stdin and stdout with an interactive child process (raw mode)

Hey there,

I've been working on a little project which essentially emulates a Windows terminal (either cmd or powershell) from a Rust program.

I am using crossterm to handle input events. I've enabled raw mode as I would prefer to have more control over stdin/stdout.

Below is a small example of what I am trying to achieve.

fn main() {
    crossterm::terminal::enable_raw_mode().unwrap();

    let command = "python"; // can be anything - echo/dir etc...

    let mut child = std::process::Command::new("powershell") // or cmd /c
        .arg("-Command")
        .arg(command)
        // .stdin(std::process::Stdio::piped())
        // .stdout(std::process::Stdio::piped())
        // .stderr(std::process::Stdio::piped())
        .spawn()
        .unwrap();
    // User should now be able to interact with this process until it is complete.
    child.wait.unwrap();
}

Using this particular snippet of code, I found that commands such as echo work as intended.

In contrast, programs such as python in interactive mode, had strange behaviour where stdout would be inherited and display output if stdin was disregarded. When adding an stdin pipe, however, the output of Python could not be seen.

I was able to get glimpses of outputs when using the following snippet (and uncommenting the lines in the child command).

    let mut child_stdin = child.stdin.take().unwrap();
    let mut child_stdout = child.stdout.take().unwrap();
    let mut child_stderr = child.stderr.take().unwrap();

    // STDIN
    std::thread::spawn(move || {
        let mut buffer = [0; 1024];
        loop {
            let n = std::io::stdin().read(&mut buffer).unwrap();
            std::io::stdout().write(&mut buffer[..n]).unwrap();
            std::io::stdout().flush().unwrap();
            let n = child_stdin.write(&mut buffer[..n]).unwrap();
            if n > 0 {
                child_stdin.flush().unwrap();
            }
        }
    });

    // STDOUT
    std::thread::spawn(move || {
        let mut buffer = [0; 1024];
        loop {
            let n = child_stdout.read(&mut buffer).unwrap();
            if n > 0 {
                std::io::stdout().write(&buffer[..n]).unwrap();
                std::io::stdout().flush().unwrap();
            }
        }
    });

    // STDERR
    std::thread::spawn(move || {
        let mut buffer = [0; 1024];
        loop {
            let n = child_stderr.read(&mut buffer).unwrap();
            if n > 0 {
                print!("{}", String::from_utf8_lossy(&buffer[..n])); // trying print out of desperation :/
                std::io::stdout().flush().unwrap();
            }
        }
    });

Unfortunately, even though I was able to see glimpses of the Python shell, I would see errors such as Unable to Initialize Device Prn.

Ultimately, it did not work in any intended way.

There were many more things I tried, however, all followed the same sort of style and I simply do not know enough about what is going on in the background.

My last resort is to disable raw input and suppress SIGINT, however, I believe it is doable another way.

Any advice/guidance to useful resources regarding Windows terminals/Rust child processes and emulating shells would be greatly appreciated.

I believe Windows Python uses the Windows console I/O API rather than using standard I/O when run "interactively" and that "interactively" means without any arguments. In other words, it interacts with its environment in a way that is not at all the same as the Linux version of Python.

Were I in your shoes I'd try two things...

  • Get rid of PowerShell; run Python directly. I assume Python will detect that standard input / standard output have been redirected and use those pipes instead of the console API.
  • Search for a command line argument that instructs Python to use standard I/O then include that argument when running Python.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.