Skip to content

Commit 559e9d6

Browse files
committed
fix(unix): stop event reads after input closes
Propagate unexpected TTY read failures, report EOF, and stop both Unix event backends after hangup instead of retrying in a hot loop. Refs #793 Refs #1110
1 parent cdc30a9 commit 559e9d6

3 files changed

Lines changed: 82 additions & 44 deletions

File tree

src/event/source/unix.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,42 @@ pub(crate) use self::tty::UnixInternalEventSource;
99

1010
#[cfg(not(feature = "use-dev-tty"))]
1111
pub(crate) use self::mio::UnixInternalEventSource;
12+
13+
#[cfg(test)]
14+
mod tests {
15+
use std::io;
16+
use std::os::unix::net::UnixStream;
17+
#[cfg(feature = "libc")]
18+
use std::os::unix::prelude::IntoRawFd;
19+
use std::sync::mpsc;
20+
use std::thread;
21+
use std::time::Duration;
22+
23+
use crate::event::source::EventSource;
24+
use crate::terminal::sys::file_descriptor::FileDesc;
25+
26+
use super::UnixInternalEventSource;
27+
28+
#[test]
29+
fn eof_is_reported_without_spinning() {
30+
let (reader, writer) = UnixStream::pair().unwrap();
31+
reader.set_nonblocking(true).unwrap();
32+
drop(writer);
33+
#[cfg(feature = "libc")]
34+
let input = FileDesc::new(reader.into_raw_fd(), true);
35+
#[cfg(not(feature = "libc"))]
36+
let input = FileDesc::Owned(reader.into());
37+
let mut source = UnixInternalEventSource::from_file_descriptor(input).unwrap();
38+
let (sender, receiver) = mpsc::channel();
39+
let worker = thread::spawn(move || {
40+
sender.send(source.try_read(None)).unwrap();
41+
});
42+
43+
let error = receiver
44+
.recv_timeout(Duration::from_secs(2))
45+
.expect("event source spun after terminal EOF")
46+
.unwrap_err();
47+
worker.join().unwrap();
48+
assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof);
49+
}
50+
}

src/event/source/unix/mio.rs

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -92,34 +92,27 @@ impl EventSource for UnixInternalEventSource {
9292

9393
for token in self.events.iter().map(|x| x.token()) {
9494
match token {
95-
TTY_TOKEN => {
96-
loop {
97-
match self.tty_fd.read(&mut self.tty_buffer) {
98-
Ok(read_count) => {
99-
if read_count > 0 {
100-
self.parser.advance(
101-
&self.tty_buffer[..read_count],
102-
read_count == TTY_BUFFER_SIZE,
103-
);
104-
}
105-
}
106-
Err(e) => {
107-
// No more data to read at the moment. We will receive another event
108-
if e.kind() == io::ErrorKind::WouldBlock {
109-
break;
110-
}
111-
// once more data is available to read.
112-
else if e.kind() == io::ErrorKind::Interrupted {
113-
continue;
114-
}
115-
}
116-
};
117-
118-
if let Some(event) = self.parser.next() {
119-
return Ok(Some(event));
95+
TTY_TOKEN => loop {
96+
match self.tty_fd.read(&mut self.tty_buffer) {
97+
Ok(0) => {
98+
return Err(io::Error::new(
99+
io::ErrorKind::UnexpectedEof,
100+
"input source closed",
101+
));
120102
}
103+
Ok(read_count) => self.parser.advance(
104+
&self.tty_buffer[..read_count],
105+
read_count == TTY_BUFFER_SIZE,
106+
),
107+
Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
108+
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
109+
Err(e) => return Err(e),
110+
};
111+
112+
if let Some(event) = self.parser.next() {
113+
return Ok(Some(event));
121114
}
122-
}
115+
},
123116
SIGNAL_TOKEN => {
124117
if self.signals.pending().next() == Some(signal_hook::consts::SIGWINCH) {
125118
// TODO Should we remove tput?

src/event/source/unix/tty.rs

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use signal_hook::low_level::pipe;
99

1010
use crate::event::Event;
1111
use crate::event::timeout::PollTimeout;
12-
use filedescriptor::{POLLIN, poll, pollfd};
12+
use filedescriptor::{POLLERR, POLLHUP, POLLIN, poll, pollfd};
1313

1414
#[cfg(feature = "event-stream")]
1515
use crate::event::sys::Waker;
@@ -85,12 +85,19 @@ impl UnixInternalEventSource {
8585
///
8686
/// Similar to `std::io::Read::read_to_end`, except this function
8787
/// only fills the given buffer and does not read beyond that.
88-
fn read_complete(fd: &FileDesc, buf: &mut [u8]) -> io::Result<usize> {
88+
/// It returns `None` for `WouldBlock` and an error when the input closes.
89+
fn read_complete(fd: &FileDesc, buf: &mut [u8]) -> io::Result<Option<usize>> {
8990
loop {
9091
match fd.read(buf) {
91-
Ok(x) => return Ok(x),
92+
Ok(0) => {
93+
return Err(io::Error::new(
94+
io::ErrorKind::UnexpectedEof,
95+
"input source closed",
96+
));
97+
}
98+
Ok(read_count) => return Ok(Some(read_count)),
9299
Err(e) => match e.kind() {
93-
io::ErrorKind::WouldBlock => return Ok(0),
100+
io::ErrorKind::WouldBlock => return Ok(None),
94101
io::ErrorKind::Interrupted => continue,
95102
_ => return Err(e),
96103
},
@@ -144,31 +151,30 @@ impl EventSource for UnixInternalEventSource {
144151
Ok(_) => (),
145152
};
146153
if fds[0].revents & POLLIN != 0 {
147-
loop {
148-
let read_count = read_complete(&self.tty, &mut self.tty_buffer)?;
149-
if read_count > 0 {
150-
self.parser.advance(
151-
&self.tty_buffer[..read_count],
152-
read_count == TTY_BUFFER_SIZE,
153-
);
154-
}
154+
while let Some(read_count) = read_complete(&self.tty, &mut self.tty_buffer)? {
155+
self.parser.advance(
156+
&self.tty_buffer[..read_count],
157+
read_count == TTY_BUFFER_SIZE,
158+
);
155159

156160
if let Some(event) = self.parser.next() {
157161
return Ok(Some(event));
158162
}
159-
160-
if read_count == 0 {
161-
break;
162-
}
163163
}
164164
}
165+
if fds[0].revents & (POLLERR | POLLHUP) != 0 {
166+
return Err(io::Error::new(
167+
io::ErrorKind::UnexpectedEof,
168+
"input source closed",
169+
));
170+
}
165171
if fds[1].revents & POLLIN != 0 {
166172
#[cfg(feature = "libc")]
167173
let fd = FileDesc::new(self.winch_signal_receiver.as_raw_fd(), false);
168174
#[cfg(not(feature = "libc"))]
169175
let fd = FileDesc::Borrowed(self.winch_signal_receiver.as_fd());
170176
// drain the pipe
171-
while read_complete(&fd, &mut [0; 1024])? != 0 {}
177+
while read_complete(&fd, &mut [0; 1024])?.is_some() {}
172178
// TODO Should we remove tput?
173179
//
174180
// This can take a really long time, because terminal::size can
@@ -188,7 +194,7 @@ impl EventSource for UnixInternalEventSource {
188194
#[cfg(not(feature = "libc"))]
189195
let fd = FileDesc::Borrowed(self.wake_pipe.receiver.as_fd());
190196
// drain the pipe
191-
while read_complete(&fd, &mut [0; 1024])? != 0 {}
197+
while read_complete(&fd, &mut [0; 1024])?.is_some() {}
192198

193199
return Err(std::io::Error::new(
194200
std::io::ErrorKind::Interrupted,

0 commit comments

Comments
 (0)