`command.stdin(Stdio::null())` fixed issue, but I don't know why

When I ran the following code with cargo test a few times:

use std::process::{Command, Stdio};

pub fn run() {
    let mut command = Command::new("script");

    command.arg("-qec");
    command.arg("echo");
    command.arg("Test!");
    command.arg("/dev/null");

    let child = command.spawn().expect("failed to spawn command");

    child.wait_with_output().unwrap();
}

fn main() {}

#[cfg(test)]
mod tests {
    use super::run;

    #[test]
    fn test_run_1() {
        run();
    }

    #[test]
    fn test_run_2() {
        run();
    }

    #[test]
    fn test_run_3() {
        run();
    }
}

Sometimes the terminal would mess up:

It feels like the terminal entered raw mode, because characters wouldn't appear if I typed. I had to run reset to get the terminal back to normal.

To stop that from happening, I had to add command.stdin(Stdio::null()) after command.arg("/dev/null").

But I don't know why the problem was fixed. Any ideas?

Note: It has something to do with Command::new("script"), I think.

Indeed. script seems to allocate a new pseudo terminal to run your command in and proxies between the pseudo terminal shown to you and the child one and for this it sets the pseudo terminal shown to you in raw mode: util-linux/lib/pty-session.c at ae62db725024363a0c45e839b6559a41984acf54 · util-linux/util-linux · GitHub

Also script is not designed to be run this way. From the main page:

script is primarily designed for interactive terminal sessions. When stdin is not a terminal (for example: echo foo | script), then the session can hang, because the interactive
shell within the script session misses EOF and script has no clue when to close the session. See the NOTES section for more information.

In your case what happens I think is that the three script instances get confused by the others enabling raw mode and then not correctly restoring it again after they exit.

1 Like

Thanks for the detailed explanation.

The reason I'm using Command::new("script") is this.

Should I be using something else?

I agree that script isn’t designed for this, though it does work for it. Some alternatives are:

  • The unbuffer command from “expect” doesn’t appear to change its own terminal settings, so it shouldn’t interfere like this if you use it instead.
  • If you run the tests one by one with cargo test -- --test-threads=1, you don’t have to worry about script running on different threads interfering with itself. This isn’t really a solution to the problem, though.
  • You may be able to create the PTY directly from Rust instead of using script or unbuffer. It looks like this is what the pty-process crate does, though I haven’t used it myself.
1 Like

Answering the specific question if why does setting stdin to null fix the issue, it's because that logic @bjorn3 linked above is conditional on the stdin being a tty, and null is not a tty!:

You might be able to use other, non-tty options for stdin like pipe() if null is causing an issue?

3 Likes

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.