Termion::async_stdin() causes loop/command to get stuck

If you run this code and don't press a key, "Still running..." will be printed, then the command will finish (as expected).

If you run this code and press any key, "Still running..." will be printed, and the command will get stuck until you press a key again. Note: Sometimes this doesn't happen. Sometimes you have to run the code four or five times to see the issue.

use std::{process::Command, thread, time::Duration};
use termion::{event::Key, input::TermRead};

pub struct CmdRunner {}

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

        command.arg("-qec").arg("sleep 4").arg("/dev/null");

        let mut child = command.spawn().expect("failed to spawn command");
        let mut stdin = termion::async_stdin().keys();

        loop {
            match child.try_wait() {
                Ok(Some(_)) => {
                    print!("Child process has exited\r\n");
                    break;
                }
                Ok(None) => {
                    let input = stdin.next();

                    if let Some(Ok(key)) = input {
                        print!("A key was pressed!\r\n");

                        match key {
                            Key::Ctrl('c') => {
                                print!("Ctrl + C was pressed!")
                            }
                            _ => {}
                        }
                    }

                    print!("Still running...\r\n");
                }
                Err(e) => {
                    eprint!("Error while waiting for child process: {}", e);
                    break;
                }
            }

            thread::sleep(Duration::from_millis(100));
        }
    }
}

fn main() {
    let mut command = CmdRunner {};

    command.run();
}

If you comment out this line:

let mut stdin = termion::async_stdin().keys();

And these lines:

if let Some(Ok(key)) = input {
    print!("A key was pressed!\r\n");

    match key {
        Key::Ctrl('c') => {
            print!("Ctrl + C was pressed!")
        }
         _ => {}
    }
}

The command will finish as expected even if you press a key while it's running. Note: I can't just remove those lines because I have to listen to keys.

So I think something in termion::async_stdin is causing the loop (and command) to get stuck—which I find very strange.

I checked the source code, and I couldn't find any clues (or maybe I don't understand the code enough).

What could be causing this issue, and how to fix it?

I've tried this several times on Linux and it works fine for me. Are you on a Mac?

1 Like

I'm on Ubuntu 22.10.

Sometimes you have to try like 4 or 5 times to see the bug.

For example, here I pressed enter while the command was running. And the fourth time I did it, it got stuck: https://i.stack.imgur.com/taEC0.png

Hmm, I'm still not able to reproduce the problem.

I've noticed that my output looks like this:

Still running...
Still running...
Still running...
Still running...
Still running...
A key was pressed!
Still running...
Still running...
Still running...
Still running...
A key was pressed!
Still running...

but your output only has Still running...?

I pressed the Enter key once: https://i.stack.imgur.com/lDVSx.png

Maybe try that? Press Enter once, then let it run. And try that four or five times until the bug appears.

By the way, thanks for helping me with this. I've been trying to fix this bug for weeks.

OK, I'm able to reproduce this bug now.

It's kind of weird. It's most likely to happen when I press Enter (or some other key) around the 1.5 second mark (I added an incrementing integer counter to the "Still running..." line to help me time the test).

I also deleted the redirection of the script command's output to /dev/null so I could see what this command was doing. Here's the output from script on a normal run:

Script started on 2023-05-26 17:18:42+08:00 [COMMAND="sleep 4" TERM="xterm-256color" TTY="/dev/pts/26" COLUMNS="118" LINES="12"]

Script done on 2023-05-26 17:18:46+08:00 [COMMAND_EXIT_CODE="0"]

Here's the output from script on a run where I pressed Enter and then had to manually bail out after 8 seconds:

Script started on 2023-05-26 17:20:25+08:00 [COMMAND="sleep 4" TERM="xterm-256color" TTY="/dev/pts/26" COLUMNS="118" LINES="12"]


Script done on 2023-05-26 17:20:33+08:00 [COMMAND_EXIT_CODE="0"]

As you can see, pressing Enter seems to stop script from exiting normally for some reason. The newline also leaked into the output from script.

Not sure what to make of it yet. Just throwing it out there in case anyone else has some ideas.

1 Like

Your debugging skills are amazing. Thanks for showing me all this!

By the way, I think both outputs are the same? The only difference is the time?

I'm curious what you're using script for here. If it's just to sleep for a few seconds, you could just replace it directly with Command::new("sleep").arg("4"), which seems more robust.

There's an extra newline in the malfunctioning output.

1 Like

Oh, this is the reason:

It enables colors for command output.

My example is just a simplified version of the real code.

I suspect the problem is with the child process running the script command while sharing stdin with termion in your Rust code. You can try adding

command.stdin(std::process::Stdio::null());

before spawning the child command, which I think fixes the problem, but it might defeat the whole point of what you're trying to do.

Hey, I tried that, and it seems like it fixed the issue. Thanks a lot! Rust Playground.

I also tried it in the original code. I could even run git commands successfully: https://i.stack.imgur.com/Q0raM.png

[...] but it might defeat the whole point of what you're trying to do.

Why do you say this?

OK, great! To be honest, I was a bit confused about what your program was supposed to do, as the snippet posted here isn't complete. Glad it didn't mess up your workflow.

I'm not sure exactly what the problem was, but it looks like sometimes when termion reads the key you press, it locks stdin and doesn't release the mutex, which causes script to block until you get termion to release the lock with your second (or third...) keypress.

Oh, I should have explained what my program does. It lets you create a menu with commands that can be triggered with a key (like in the photo I posted in my last reply).

So with this line:

command.stdin(std::process::Stdio::null());

Termion is no longer sharing stdin with my Rust code (the parent thread)? Which is why my Rust code's stdin doesn't block anymore?

Yeah, disconnecting stdin from the child thread removes the data race.

If I understand correctly, parent thread = termion, child thread = script.

Thanks a lot for the help and explanation!

1 Like

No worries. Don't forget to mark the answer if you found it helpful. :grin:

1 Like

Marked! I forgot you could mark an answer on this site, ha.

One last question, how did you print this?

[COMMAND="sleep 4" TERM="xterm-256color" TTY="/dev/pts/26" COLUMNS="118" LINES="12"]

I tried this:

fn main() {
    let mut command = CmdRunner {};
    let command_env = env::vars().collect::<Vec<_>>();

    println!("{:?}", command_env);
    command.run();
}

But that information wasn't printed.

You need to delete .arg("/dev/null"); from your command, or replace it with a filename. If you don't specify a filename, the output you want is written to a file called typescript in the folder where you run your program.

1 Like

Oh, it worked. Thanks again!