Skip to content

fix(event): drain tty fd to EAGAIN before returning from try_read - #1057

Open
sinelaw wants to merge 1 commit into
crossterm-rs:masterfrom
sinelaw:fix/drain-tty-fd-to-eagain
Open

fix(event): drain tty fd to EAGAIN before returning from try_read#1057
sinelaw wants to merge 1 commit into
crossterm-rs:masterfrom
sinelaw:fix/drain-tty-fd-to-eagain

Conversation

@sinelaw

@sinelaw sinelaw commented May 12, 2026

Copy link
Copy Markdown

mio registers the tty fd with Interest::READABLE, which on Linux is EPOLLIN | EPOLLET (edge-triggered). The read loop returned as soon as the parser yielded its first event, leaving any remaining bytes in the kernel buffer. The edge has already been consumed, so epoll_wait won't re-fire until new data arrives.

Symptom: a large paste (e.g. tmux paste-buffer, no bracketed paste) stalls partway through and only resumes on the next keypress, which creates a new edge and drains the leftovers.

Fix: drain to WouldBlock — or a short read, which on a tty signals the same — before returning. Events queue in the parser; subsequent try_read calls drain them with no extra syscalls. The typical small-input fast path is unchanged: one short read, exit.

Repro inside tmux:

tmux load-buffer some-long-file.md
tmux paste-buffer -t <target-pane>

Tested on tmux 3.4, Linux 6.18. The use-dev-tty path uses level-triggered poll() and is unaffected.

@sinelaw
sinelaw requested a review from TimonPost as a code owner May 12, 2026 13:36
@sinelaw
sinelaw force-pushed the fix/drain-tty-fd-to-eagain branch from 88cc245 to 1e4a201 Compare May 12, 2026 13:38
@sinelaw
sinelaw marked this pull request as draft May 12, 2026 13:39
mio registers the tty fd with Interest::READABLE, which on Linux maps
to EPOLLIN | EPOLLET — edge-triggered. The previous read loop returned
as soon as the parser produced its first event, leaving any remaining
bytes in the kernel buffer. Because the edge has already been
consumed, epoll_wait will not re-fire for those leftover bytes until
new data arrives.

In practice this surfaces as the well-known "a large paste blocks
partway through and only resumes when I press a key" symptom — for
example, pasting a few KB of text from tmux's paste-buffer (which does
not use bracketed paste, so the bytes arrive as a flood of raw
keystrokes). The first ~1 KiB is parsed, the parser returns the first
event, the loop exits with bytes still in the kernel pty, and the next
epoll_wait never fires. A real keypress later creates a new edge, the
leftover bytes finally drain, and the paste appears to "continue".

Fix: keep reading until the fd returns WouldBlock or a short read
(which also indicates no more bytes are immediately available on a
tty). All resulting events are queued in the parser. Only then do we
return the first event to the caller; subsequent try_read calls drain
the queue without any extra syscalls.

No behavior change when each try_read call corresponds to a single
small input (the typical interactive case): the read loop sees a short
read and exits immediately, identical to the old fast path.
@sinelaw
sinelaw force-pushed the fix/drain-tty-fd-to-eagain branch from 1e4a201 to 623f8b4 Compare May 12, 2026 13:49
@sinelaw

sinelaw commented May 12, 2026

Copy link
Copy Markdown
Author

I would like to add a test for this but it requires some kind of harness that actually uses a real PTY.

@sinelaw
sinelaw marked this pull request as ready for review May 13, 2026 07:33
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 this pull request may close these issues.

1 participant