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

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

Open
Hirevo opened this issue Nov 3, 2023 · 0 comments · May be fixed by #840
Open

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

Hirevo opened this issue Nov 3, 2023 · 0 comments · May be fixed by #840

Comments

@Hirevo
Copy link

Hirevo commented Nov 3, 2023

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:

  • OS: macOS Sonoma 14.1
  • Rust: 1.73.0 (stable)
  • Terminal: Alacritty 0.12.3

Second machine:

  • OS: EndeavourOS
  • Kernel: 6.5.8-arch1-1
  • DE: GNOME 45.0
  • Rust: 1.73.0 (stable)
  • Terminal: GNOME Terminal 3.50.1
@Hirevo Hirevo linked a pull request Nov 3, 2023 that will close this issue
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

Successfully merging a pull request may close this issue.

1 participant