crossterm icon indicating copy to clipboard operation
crossterm copied to clipboard

`libc::read` syscall can block reading `stdin`

Open ReagentX opened this issue 1 year ago • 4 comments

The following syscall can block waiting for stdin:

https://github.com/crossterm-rs/crossterm/blob/58f580eaad4e80ccb8a09541b760a329971bb4bc/src/event/sys/unix/file_descriptor.rs#L33-L37

Steps to reproduce

Create a new binary project and place the following code in main.rs. Once done, run with cargo run. Wait a few seconds for the loop to emit some text like:

Before poll
           Poll failed
                      Before poll
                                 Poll failed

Once that has passed, press and release a single key.

Code to reproduce:

use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;

use crossterm::{
    event::{poll, read, Event, KeyCode},
    terminal::{disable_raw_mode, enable_raw_mode},
    Result,
};

fn main() -> Result<()> {
    enable_raw_mode()?;
    let thread = thread::spawn(|| {
        Command::new("cat")
            .arg("-")
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .unwrap();
    });

    loop {
        eprintln!("Before poll");
        if poll(Duration::from_millis(1000))? {
            eprintln!("Poll passed");
            match read()? {
                Event::Key(k) => {
                    println!("{:?}", k);
                    if k.code == KeyCode::Down {
                        disable_raw_mode()?;
                        break;
                    }
                }
                Event::Mouse(_) => todo!(),
                Event::Resize(_, _) => todo!(),
            }
        } else {
            eprintln!("Poll failed");
        }
    }

    thread.join().unwrap();
    Ok(())
}

Expected behavior

The key is printed to the console

Actual behavior

The key is not printed, and execution stops after printing Before poll inside of the libc::read() syscall.

If we add some logs around the syscall, we can prove that execution gets stuck there:

println!("Attempting to read {} bytes from {}\n", size, self.fd);
let r = libc::read(
    self.fd,
    buffer.as_mut_ptr() as *mut libc::c_void,
    size as size_t,
) as isize;
println!("Finished read!\n");
r

Performing the steps above, this emits the following to my terminal and hangs:

Before poll
           Poll failed
                      Before poll
                                 Poll failed
                                            Before poll
                                                       Poll failed
                                                                  Before poll
                                                                             Attempting to read 1204 bytes from 0

System Info:

  • crossterm = "0.24.0"
  • MacOS 10.15.7 (19H1922)
  • rustc 1.62.1

ReagentX avatar Jul 26 '22 21:07 ReagentX

https://github.com/crossterm-rs/crossterm/issues/397 https://github.com/crossterm-rs/crossterm/pull/407 https://github.com/tokio-rs/mio/issues/1377

sigmaSd avatar Jul 27 '22 05:07 sigmaSd

I don't think those issues are the same problem, none of them mention the syscall and the unmerged PR doesn't alter the blocking libc::read() call:

https://github.com/crossterm-rs/crossterm/blob/f0a7b11a3f357d0573c295615a5b358717cecd2d/src/event/sys/unix/file_descriptor.rs#L34-L39

The problem is not the surrounding polling code, its that once libc::read() is called, it doesn't yield control back until it finishes, and if something else took over stdin it will not finish. The crossterm Waker successfully wakes on input, but it blocks on the libc::read() call made after it detects input.

The waker

https://github.com/crossterm-rs/crossterm/blob/f909b3db954fd47a7d1f04a55dd7f75ca058ffb5/src/event/read.rs#L64

Correctly invokes try_read() of the UnixInternalEventSource:

https://github.com/crossterm-rs/crossterm/blob/f909b3db954fd47a7d1f04a55dd7f75ca058ffb5/src/event/source/unix.rs#L103

But it hangs inside of that self.tty_fd.read() call waiting for libc::read() to return.

ReagentX avatar Jul 27 '22 05:07 ReagentX

I forgot to like this issue https://github.com/crossterm-rs/crossterm/issues/396

It seems to be the same problem, the gist of it you're having 2 process trying to read from stdin at the same time, this is just known to give unexpected result. The fix would not be by making changes to read call, but to make crossterm read events from /dev/tty instead of stdin this would allow other process to read stdin as they want

You could see here https://github.com/crossterm-rs/crossterm/blob/0c205907744cecd7d3f16b617953d8ed53b43b46/src/event/sys/unix/file_descriptor.rs#L67 that we currently default to stdin and thats what this pr https://github.com/crossterm-rs/crossterm/pull/407 tries to fix

sigmaSd avatar Jul 27 '22 05:07 sigmaSd

I saw those issues and I do not think they are related. When a parent process creates a child, that child inherits the fds 0, 1, 2 from the parent. /dev/tty gets resolved to a specific tty that reads those file descriptors. For example, stdin/stdout/stderr are available within the process itself and the tty is "accessible" external to the process, i.e. to crossterm. Wouldn't reading from /dev/tty just read from that tty's fd 0, which is shared with a child process?

Even if they are separate, libc::read() always blocks until it is finished, so all of the polling code before it ends up not mattering. That call probably needs to be guarded by libc::poll() or something to prevent a deadlock. In the example I provided, crossterm does receive the input, it just gets blocked trying to read it.

ReagentX avatar Jul 27 '22 14:07 ReagentX