Skip to content

Commit e629345

Browse files
committed
fix(unix): retry EINTR in poll/select/read so unrelated signals don't surface as spurious key events
`read_single_key_impl` blocks in `poll`/`select` on the tty fd. Any signal that interrupts that syscall — SIGCHLD from a dying child, SIGWINCH from a terminal resize, etc. — currently propagates as `io::ErrorKind::Interrupted`, which `read_single_key` then treats as 'the user pressed Ctrl-C' and forwards by calling `libc::raise(SIGINT)` on the process. With the default SIGINT handler this kills the program with exit code 130; with cliclack-style consumers it cancels the prompt. The original intent of `Err(Interrupted)` is to signal a Ctrl-C byte (`\\x03`) read from the tty, which `read_bytes` synthesizes explicitly. Real EINTR from a signal is unrelated and should be retried per the standard idiom. This patch retries EINTR inside the syscall wrappers (`poll_fd`, `select_fd`, `read_bytes`) so `Err(Interrupted)` reaching `read_single_key` unambiguously means a Ctrl-C byte was read, and unrelated signals are transparent to callers. Repro before this patch: any program that fork+execs a child and uses a console-based prompt (cliclack, dialoguer, etc.) shortly after the child exits will randomly bail at the prompt because SIGCHLD interrupts `select` mid-prompt.
1 parent 586efad commit e629345

1 file changed

Lines changed: 70 additions & 44 deletions

File tree

src/unix_term.rs

Lines changed: 70 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -157,43 +157,57 @@ fn poll_fd(fd: RawFd, timeout: i32) -> io::Result<bool> {
157157
events: libc::POLLIN,
158158
revents: 0,
159159
};
160-
let ret = unsafe { libc::poll(&mut pollfd as *mut _, 1, timeout) };
161-
if ret < 0 {
162-
Err(io::Error::last_os_error())
163-
} else {
164-
Ok(pollfd.revents & libc::POLLIN != 0)
160+
loop {
161+
let ret = unsafe { libc::poll(&mut pollfd as *mut _, 1, timeout) };
162+
if ret < 0 {
163+
let err = io::Error::last_os_error();
164+
if err.kind() == io::ErrorKind::Interrupted {
165+
// Interrupted by an unrelated signal (e.g. SIGCHLD, SIGWINCH);
166+
// do not surface as a spurious key event. Retry the wait.
167+
continue;
168+
}
169+
return Err(err);
170+
}
171+
return Ok(pollfd.revents & libc::POLLIN != 0);
165172
}
166173
}
167174

168175
#[cfg(target_os = "macos")]
169176
fn select_fd(fd: RawFd, timeout: i32) -> io::Result<bool> {
170177
unsafe {
171-
let mut read_fd_set: libc::fd_set = mem::zeroed();
178+
loop {
179+
let mut read_fd_set: libc::fd_set = mem::zeroed();
172180

173-
let mut timeout_val;
174-
let timeout = if timeout < 0 {
175-
ptr::null_mut()
176-
} else {
177-
timeout_val = libc::timeval {
178-
tv_sec: (timeout / 1000) as _,
179-
tv_usec: (timeout * 1000) as _,
181+
let mut timeout_val;
182+
let timeout = if timeout < 0 {
183+
ptr::null_mut()
184+
} else {
185+
timeout_val = libc::timeval {
186+
tv_sec: (timeout / 1000) as _,
187+
tv_usec: (timeout * 1000) as _,
188+
};
189+
&mut timeout_val
180190
};
181-
&mut timeout_val
182-
};
183-
184-
libc::FD_ZERO(&mut read_fd_set);
185-
libc::FD_SET(fd, &mut read_fd_set);
186-
let ret = libc::select(
187-
fd + 1,
188-
&mut read_fd_set,
189-
ptr::null_mut(),
190-
ptr::null_mut(),
191-
timeout,
192-
);
193-
if ret < 0 {
194-
Err(io::Error::last_os_error())
195-
} else {
196-
Ok(libc::FD_ISSET(fd, &read_fd_set))
191+
192+
libc::FD_ZERO(&mut read_fd_set);
193+
libc::FD_SET(fd, &mut read_fd_set);
194+
let ret = libc::select(
195+
fd + 1,
196+
&mut read_fd_set,
197+
ptr::null_mut(),
198+
ptr::null_mut(),
199+
timeout,
200+
);
201+
if ret < 0 {
202+
let err = io::Error::last_os_error();
203+
if err.kind() == io::ErrorKind::Interrupted {
204+
// Interrupted by an unrelated signal (e.g. SIGCHLD, SIGWINCH);
205+
// do not surface as a spurious key event. Retry the wait.
206+
continue;
207+
}
208+
return Err(err);
209+
}
210+
return Ok(libc::FD_ISSET(fd, &read_fd_set));
197211
}
198212
}
199213
}
@@ -230,22 +244,34 @@ fn read_single_char(fd: RawFd) -> io::Result<Option<char>> {
230244
// Similar to libc::read. Read count bytes into slice buf from descriptor fd.
231245
// If successful, return the number of bytes read.
232246
// Will return an error if nothing was read, i.e when called at end of file.
247+
//
248+
// Note: this function returns `ErrorKind::Interrupted` *only* to signal that
249+
// the user typed Ctrl-C (i.e. the first byte read was `\x03`). Real EINTR
250+
// from `libc::read` (caused by an unrelated signal interrupting the syscall)
251+
// is retried internally, so callers can treat `Err(Interrupted)` as
252+
// unambiguously "Ctrl-C was pressed".
233253
fn read_bytes(fd: RawFd, buf: &mut [u8], count: u8) -> io::Result<u8> {
234-
let read = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, count as usize) };
235-
if read < 0 {
236-
Err(io::Error::last_os_error())
237-
} else if read == 0 {
238-
Err(io::Error::new(
239-
io::ErrorKind::UnexpectedEof,
240-
"Reached end of file",
241-
))
242-
} else if buf[0] == b'\x03' {
243-
Err(io::Error::new(
244-
io::ErrorKind::Interrupted,
245-
"read interrupted",
246-
))
247-
} else {
248-
Ok(read as u8)
254+
loop {
255+
let read = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, count as usize) };
256+
if read < 0 {
257+
let err = io::Error::last_os_error();
258+
if err.kind() == io::ErrorKind::Interrupted {
259+
continue;
260+
}
261+
return Err(err);
262+
} else if read == 0 {
263+
return Err(io::Error::new(
264+
io::ErrorKind::UnexpectedEof,
265+
"Reached end of file",
266+
));
267+
} else if buf[0] == b'\x03' {
268+
return Err(io::Error::new(
269+
io::ErrorKind::Interrupted,
270+
"read interrupted",
271+
));
272+
} else {
273+
return Ok(read as u8);
274+
}
249275
}
250276
}
251277

0 commit comments

Comments
 (0)