Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

libc::read syscall can block reading stdin #692

Open
ReagentX opened this issue Jul 26, 2022 · 4 comments
Open

libc::read syscall can block reading stdin #692

ReagentX opened this issue Jul 26, 2022 · 4 comments

Comments

@ReagentX
Copy link

ReagentX commented Jul 26, 2022

The following syscall can block waiting for stdin:

libc::read(
self.fd,
buffer.as_mut_ptr() as *mut libc::c_void,
size as size_t,
) as isize

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
@sigmaSd
Copy link
Contributor

sigmaSd commented Jul 27, 2022

#397
#407
tokio-rs/mio#1377

@ReagentX
Copy link
Author

ReagentX commented Jul 27, 2022

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:

let result = unsafe {
libc::read(
self.fd,
buffer.as_mut_ptr() as *mut libc::c_void,
size as size_t,
) as isize

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

let maybe_event = match event_source.try_read(poll_timeout.leftover()) {

Correctly invokes try_read() of the UnixInternalEventSource:

match self.tty_fd.read(&mut self.tty_buffer, TTY_BUFFER_SIZE) {

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

@sigmaSd
Copy link
Contributor

sigmaSd commented Jul 27, 2022

I forgot to like this issue #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

pub fn tty_fd() -> Result<FileDesc> {
that we currently default to stdin and thats what this pr #407 tries to fix

@ReagentX
Copy link
Author

ReagentX commented Jul 27, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants