From 3e3e2776a4ae01727e103dd883885ccdef221efc Mon Sep 17 00:00:00 2001 From: jh-block Date: Tue, 5 May 2026 15:50:18 +0200 Subject: [PATCH 1/2] fix(unix): retry EINTR in poll/select/read Fixes #212. read_single_key treats any Err(Interrupted) from read_single_key_impl as Ctrl-C and forwards it via libc::raise(SIGINT). That's correct only for the synthesized Err(Interrupted) read_bytes returns when it sees a \x03 byte. EINTR returned by poll/select/read because of an unrelated signal (SIGCHLD, SIGWINCH, etc.) takes the same path and incorrectly kills the program with SIGINT. Retry EINTR inside the syscall wrappers so Err(Interrupted) reaching read_single_key unambiguously means a \x03 byte was read. --- src/unix_term.rs | 67 ++++++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/src/unix_term.rs b/src/unix_term.rs index ec445d4b..d2d63013 100644 --- a/src/unix_term.rs +++ b/src/unix_term.rs @@ -157,21 +157,25 @@ fn poll_fd(fd: RawFd, timeout: i32) -> io::Result { events: libc::POLLIN, revents: 0, }; - let ret = unsafe { libc::poll(&mut pollfd as *mut _, 1, timeout) }; - if ret < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(pollfd.revents & libc::POLLIN != 0) + loop { + let ret = unsafe { libc::poll(&mut pollfd as *mut _, 1, timeout) }; + if ret < 0 { + let err = io::Error::last_os_error(); + if err.kind() == io::ErrorKind::Interrupted { + continue; + } + break Err(err); + } + break Ok(pollfd.revents & libc::POLLIN != 0); } } #[cfg(target_os = "macos")] fn select_fd(fd: RawFd, timeout: i32) -> io::Result { - unsafe { - let mut read_fd_set: libc::fd_set = mem::zeroed(); - + loop { + let mut read_fd_set: libc::fd_set = unsafe { mem::zeroed() }; let mut timeout_val; - let timeout = if timeout < 0 { + let timeout_ptr: *mut libc::timeval = if timeout < 0 { ptr::null_mut() } else { timeout_val = libc::timeval { @@ -181,20 +185,25 @@ fn select_fd(fd: RawFd, timeout: i32) -> io::Result { &mut timeout_val }; - libc::FD_ZERO(&mut read_fd_set); - libc::FD_SET(fd, &mut read_fd_set); - let ret = libc::select( - fd + 1, - &mut read_fd_set, - ptr::null_mut(), - ptr::null_mut(), - timeout, - ); + let ret = unsafe { + libc::FD_ZERO(&mut read_fd_set); + libc::FD_SET(fd, &mut read_fd_set); + libc::select( + fd + 1, + &mut read_fd_set, + ptr::null_mut(), + ptr::null_mut(), + timeout_ptr, + ) + }; if ret < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(libc::FD_ISSET(fd, &read_fd_set)) + let err = io::Error::last_os_error(); + if err.kind() == io::ErrorKind::Interrupted { + continue; + } + break Err(err); } + break Ok(unsafe { libc::FD_ISSET(fd, &read_fd_set) }); } } @@ -231,10 +240,18 @@ fn read_single_char(fd: RawFd) -> io::Result> { // If successful, return the number of bytes read. // Will return an error if nothing was read, i.e when called at end of file. fn read_bytes(fd: RawFd, buf: &mut [u8], count: u8) -> io::Result { - let read = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, count as usize) }; - if read < 0 { - Err(io::Error::last_os_error()) - } else if read == 0 { + let read = loop { + let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, count as usize) }; + if n < 0 { + let err = io::Error::last_os_error(); + if err.kind() == io::ErrorKind::Interrupted { + continue; + } + return Err(err); + } + break n; + }; + if read == 0 { Err(io::Error::new( io::ErrorKind::UnexpectedEof, "Reached end of file", From e08b5697fa0d0b8a40629d44230640b2ceda561a Mon Sep 17 00:00:00 2001 From: jh-block Date: Tue, 5 May 2026 15:50:18 +0200 Subject: [PATCH 2/2] Collapse nested if into match guard in read_secure Silences clippy::collapsible_match warning on clippy 1.95+. --- src/windows_term/mod.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/windows_term/mod.rs b/src/windows_term/mod.rs index e3c7a277..846316cb 100644 --- a/src/windows_term/mod.rs +++ b/src/windows_term/mod.rs @@ -405,11 +405,9 @@ pub(crate) fn read_secure() -> io::Result { Key::Enter => { break; } - Key::Char('\x08') => { - if !rv.is_empty() { - let new_len = rv.len() - 1; - rv.truncate(new_len); - } + Key::Char('\x08') if !rv.is_empty() => { + let new_len = rv.len() - 1; + rv.truncate(new_len); } Key::Char(c) => { rv.push(c);