crossterm-rs / crossterm

Cross platform terminal library rust
MIT License
3.29k stars 280 forks source link

`crossterm::event::poll(Duration::ZERO)` incorrectly returns false with `use-dev-tty` enabled #839

Open Hirevo opened 1 year ago

Hirevo commented 1 year ago

Describe the bug crossterm::event::poll(Duration::ZERO) can, in some situation, return false despite currently having one in its buffer.
This can lead some setups to be out-of-sync due to the subsequent crossterm::event::read returning an older event than the one meant by crossterm::event::poll.

To Reproduce Here is a minimum example I put together, using a similar structure to the event-poll-read example.

The context of this setup is that I was trying to use crossterm to read events from stdin (using its read and poll functions), but being able to handle signal interruptions myself during the polling through EINTR.

Due to both read and poll functions catching and handling EINTR errors themselves, I used mio myself to poll standard input.

[dependencies]
anyhow = "1.0.75"
crossterm = { version = "0.27.0", features = ["use-dev-tty"] }
mio = "0.8.9"
use std::{io, time::Duration};

use crossterm::{
    cursor::position,
    event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode},
};
use mio::unix::SourceFd;
use mio::{Events, Interest, Poll, Token};

const HELP: &str = r#"Blocking poll() & non-blocking read()
 - Keyboard, mouse and terminal resize events enabled
 - Hit "c" to print current cursor position
 - Use Esc to quit
"#;

fn print_events() -> anyhow::Result<()> {
    let mut events = Events::with_capacity(1);
    let mut mio_poll = Poll::new()?;

    // Register a READABLE interest on standard input
    mio_poll.registry()
        .register(&mut SourceFd(&0), Token(0), Interest::READABLE)?;

    loop {
        // Check if we have a buffered event ready?
        if !crossterm::event::poll(Duration::ZERO)? {
            // We don't have an event ready, so start polling stdin ourselves
            events.clear();
            match mio_poll.poll(&mut events, None) {
                Ok(_) => {
                    // an event might be ready on stdin
                }
                Err(err) if err.kind() == io::ErrorKind::Interrupted => {
                    // we got signaled, let's handle it here
                    println!("Signal received !\r");
                    // then we go back to polling
                    continue;
                }
                Err(err) => {
                    // we got an `mio` error
                    return Err(err.into());
                }
            }
        }

        // It's guaranteed that read() won't block if `poll` returns `Ok(true)`
        let event = crossterm::event::read()?;

        println!("Event::{:?}\r", event);

        if event == Event::Key(KeyCode::Char('c').into()) {
            println!("Cursor position: {:?}\r", position());
        }

        if event == Event::Key(KeyCode::Esc.into()) {
            break;
        }
    }

    Ok(())
}

fn main() -> anyhow::Result<()> {
    println!("{}", HELP);

    enable_raw_mode()?;

    let mut stdout = io::stdout();
    execute!(stdout, EnableMouseCapture)?;

    if let Err(e) = print_events() {
        println!("Error: {:?}\r", e);
    }

    execute!(stdout, DisableMouseCapture)?;

    disable_raw_mode()?;
    Ok(())
}

Running this program, it should behave just like event-poll-read initially, but if multiple events are submitted at once (by pasting some text, for example, since bracketed paste is not enabled), it starts breaking and older keypresses are processed when newer keypresses are done.

This is due to poll(Duration::ZERO) returning false when some events are available, causing read to not be called until an even newer event comes up, and that read call will returns this older event instead of the new one (which will be waiting in the buffer).

When the use-dev-tty feature is disabled, this setup works as expected, without having this issue.

After having read some of crossterm's code, since stdin is not redirected in this example,use-dev-tty seems to be using file descriptor 0 directly instead of /dev/tty since they are equivalent (isatty(0) is true in this case), so my mio polling and crossterm's polling should have perfectly identical results, and should behave identically than without use-dev-tty.

Expected behavior I expected this setup to work just like the event-poll-read, except with the added benefit of handling EINTR myself.
I expected no behaviour change compared to without use-dev-tty enabled.

OS & Terminal/Console I've reproduced this bug on two different machines.

First machine:

Second machine: