crossterm
crossterm copied to clipboard
`libc::read` syscall can block reading `stdin`
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
https://github.com/crossterm-rs/crossterm/issues/397 https://github.com/crossterm-rs/crossterm/pull/407 https://github.com/tokio-rs/mio/issues/1377
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.
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
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.