Skip to content

Commit 88cc245

Browse files
Claudesinelaw
authored andcommitted
fix(event): drain tty fd to EAGAIN before returning from try_read
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.
1 parent c02a080 commit 88cc245

1 file changed

Lines changed: 19 additions & 3 deletions

File tree

src/event/source/unix/mio.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ impl EventSource for UnixInternalEventSource {
9393
for token in self.events.iter().map(|x| x.token()) {
9494
match token {
9595
TTY_TOKEN => {
96+
// Drain the fd all the way to `WouldBlock` before
97+
// returning, even after the parser has produced its
98+
// first event. mio registers the fd with `EPOLLET`
99+
// (edge-triggered), so if we return early while the
100+
// kernel still has buffered bytes, the next
101+
// `epoll_wait` will not re-fire for them — they'll
102+
// sit unread until *new* data arrives and re-arms
103+
// the edge. That is the root cause of the classic
104+
// "large paste blocks until I press a key" bug.
96105
loop {
97106
match self.tty_fd.read(&mut self.tty_buffer) {
98107
Ok(read_count) => {
@@ -102,6 +111,13 @@ impl EventSource for UnixInternalEventSource {
102111
read_count == TTY_BUFFER_SIZE,
103112
);
104113
}
114+
if read_count < TTY_BUFFER_SIZE {
115+
// Short read also signals "no more
116+
// data right now" on a tty; avoid
117+
// an extra syscall that would just
118+
// return WouldBlock.
119+
break;
120+
}
105121
}
106122
Err(e) => {
107123
// No more data to read at the moment. We will receive another event
@@ -114,10 +130,10 @@ impl EventSource for UnixInternalEventSource {
114130
}
115131
}
116132
};
133+
}
117134

118-
if let Some(event) = self.parser.next() {
119-
return Ok(Some(event));
120-
}
135+
if let Some(event) = self.parser.next() {
136+
return Ok(Some(event));
121137
}
122138
}
123139
SIGNAL_TOKEN => {

0 commit comments

Comments
 (0)