Skip to content

[BUG] Termion backend drops some characters for subsequent reads from stdin #563

Closed
@arxanas

Description

@arxanas

Describe the bug

When using the termion back-end and then reading from stdin, up to two characters are dropped.

To Reproduce

Simple modification of the pause example at https://github.com/gyscos/cursive/blob/main/examples/src/bin/pause.rs:

fn main() {
    let mut siv = cursive::termion();
    siv.add_layer(
        cursive::views::Dialog::text("Please write your message.")
            .button("Ok", |s| s.quit()),
    );
    siv.run();

    println!("Enter your message here:");
    let mut line = String::new();
    std::io::stdin().read_line(&mut line).unwrap();

    println!("Your message was: {:?}", line);
}

Expected behavior:

Enter your message here:
hello
Your message was: "hello\n"

Actual behavior:

Enter your message here:
hello
Your message was: "llo\n"

Note that the first two characters of the provided message have been dropped.

Environment

  • Operating system used: macOS 10.15.7
  • Backend used: termion
  • Current locale:
LANG="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_CTYPE="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_ALL=
  • Cursive version: 0.16.3

Analysis

I believe the main problem is this loop in combination with the implementation of termion:

// We want nonblocking input, but termion is blocking by default
// Read input from a separate thread
thread::spawn(move || {
let mut events = input_file.events();
// Take all the events we can
while let Some(Ok(event)) = events.next() {
// If we can't send, it means the receiving side closed,
// so just stop.
if input_sender.send(event).is_err() {
break;
}
}
running.store(false, Ordering::Relaxed);
});

When you ask termion for the next event, it reads up to two bytes, which is why specifically up to two characters are dropped:

https://github.com/redox-os/termion/blob/dce5e7500fd709987f9bf8f3911e4daa61d0ad14/src/input.rs#L60-L85

Even after the Cursive instance is dropped, the input thread is still running in the background. Once data has been written to stdin, this thread will then read up to two bytes and attempt to run input_sender.send(event), which will fail and then cause the thread to terminate. The calling code may then attempt to read from stdin, but the bytes are already gone.

The ideal fix would be to terminate the input thread once the Cursive instance has been dropped, but I can't figure out a good way to do that, since the call to events.next() is fundamentally blocking. It might be possible to use a variant of select to poll the input to see if it has data before calling events.next(), but that seems complicated and non-portable.

I haven't been able to figure out a good workaround for user code. In particular, I expected that creating the Termion back-end with /dev/tty would fix the issue, but it doesn't seem to.

Termion has an item called termion::async_stdin, which might be relevant towards fixing this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions