mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-05-31 20:31:19 -03:00
Block interrupts and uvar events while decoding key
readb() has only one caller that passes blocking=false: try_readb(). This function is used while decoding keys; anything but a successful read is treated as "end of input sequence". This means that key input sequences such as \e[1;3D can be torn apart by - signals (EINTR) which is more likely sincee1be842(Work around torn byte sequences in qemu kbd input with 1ms timeout, 2025-03-04). - universal variable notifications (from other fish processes) Fix this by blocking signals and not listening on the uvar fd. We do something similar when matching key sequences against bindings, so extract a function and use it for key decoding too. Ref: https://github.com/fish-shell/fish-shell/issues/11668#issuecomment-3101341081 (cherry picked from commitda96172739)
This commit is contained in:
@@ -24,8 +24,8 @@
|
||||
use std::mem::MaybeUninit;
|
||||
use std::os::fd::RawFd;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::ptr;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
// The range of key codes for inputrc-style keyboard functions.
|
||||
pub const R_END_INPUT_FUNCTIONS: usize = (ReadlineCmd::ReverseRepeatJump as usize) + 1;
|
||||
@@ -479,7 +479,7 @@ pub fn from_check_exit() -> CharEvent {
|
||||
/// This calls select() on three fds: input (e.g. stdin), the ioport notifier fd (for main thread
|
||||
/// requests), and the uvar notifier. This returns either the byte which was read, or one of the
|
||||
/// special values below.
|
||||
enum ReadbResult {
|
||||
enum InputEventTrigger {
|
||||
// A byte was successfully read.
|
||||
Byte(u8),
|
||||
|
||||
@@ -494,12 +494,22 @@ enum ReadbResult {
|
||||
|
||||
// Our ioport reported a change, so service main thread requests.
|
||||
IOPortNotified,
|
||||
|
||||
NothingToRead,
|
||||
}
|
||||
|
||||
fn readb(in_fd: RawFd, blocking: bool) -> ReadbResult {
|
||||
fn readb(in_fd: RawFd) -> Option<u8> {
|
||||
assert!(in_fd >= 0, "Invalid in fd");
|
||||
let mut arr: [u8; 1] = [0];
|
||||
if read_blocked(in_fd, &mut arr) != Ok(1) {
|
||||
// The terminal has been closed.
|
||||
return None;
|
||||
}
|
||||
let c = arr[0];
|
||||
FLOG!(reader, "Read byte", char_to_symbol(char::from(c)));
|
||||
// The common path is to return a u8.
|
||||
Some(c)
|
||||
}
|
||||
|
||||
fn next_input_event(in_fd: RawFd) -> InputEventTrigger {
|
||||
let mut fdset = FdReadableSet::new();
|
||||
loop {
|
||||
fdset.clear();
|
||||
@@ -517,57 +527,89 @@ fn readb(in_fd: RawFd, blocking: bool) -> ReadbResult {
|
||||
}
|
||||
|
||||
// Here's where we call select().
|
||||
let select_res = fdset.check_readable(if blocking {
|
||||
FdReadableSet::kNoTimeout
|
||||
} else {
|
||||
1000
|
||||
});
|
||||
let select_res = fdset.check_readable(FdReadableSet::kNoTimeout);
|
||||
if select_res < 0 {
|
||||
let err = errno::errno().0;
|
||||
if err == libc::EINTR || err == libc::EAGAIN {
|
||||
// A signal.
|
||||
return ReadbResult::Interrupted;
|
||||
return InputEventTrigger::Interrupted;
|
||||
} else {
|
||||
// Some fd was invalid, so probably the tty has been closed.
|
||||
return ReadbResult::Eof;
|
||||
return InputEventTrigger::Eof;
|
||||
}
|
||||
}
|
||||
|
||||
if blocking {
|
||||
// select() did not return an error, so we may have a readable fd.
|
||||
// The priority order is: uvars, stdin, ioport.
|
||||
// Check to see if we want a universal variable barrier.
|
||||
if let Some(notifier_fd) = notifier_fd {
|
||||
if fdset.test(notifier_fd) && notifier.notification_fd_became_readable(notifier_fd)
|
||||
{
|
||||
return ReadbResult::UvarNotified;
|
||||
}
|
||||
// select() did not return an error, so we may have a readable fd.
|
||||
// The priority order is: uvars, stdin, ioport.
|
||||
// Check to see if we want a universal variable barrier.
|
||||
if let Some(notifier_fd) = notifier_fd {
|
||||
if fdset.test(notifier_fd) && notifier.notification_fd_became_readable(notifier_fd) {
|
||||
return InputEventTrigger::UvarNotified;
|
||||
}
|
||||
}
|
||||
|
||||
// Check stdin.
|
||||
if fdset.test(in_fd) {
|
||||
let mut arr: [u8; 1] = [0];
|
||||
if read_blocked(in_fd, &mut arr) != Ok(1) {
|
||||
// The terminal has been closed.
|
||||
return ReadbResult::Eof;
|
||||
}
|
||||
FLOG!(reader, "Read byte", arr[0]);
|
||||
// The common path is to return a u8.
|
||||
return ReadbResult::Byte(arr[0]);
|
||||
}
|
||||
if !blocking {
|
||||
return ReadbResult::NothingToRead;
|
||||
return readb(in_fd).map_or(InputEventTrigger::Eof, InputEventTrigger::Byte);
|
||||
}
|
||||
|
||||
// Check for iothread completions only if there is no data to be read from the stdin.
|
||||
// This gives priority to the foreground.
|
||||
if fdset.test(ioport_fd) {
|
||||
return ReadbResult::IOPortNotified;
|
||||
return InputEventTrigger::IOPortNotified;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_fd_readable(in_fd: RawFd, timeout: Duration) -> bool {
|
||||
use std::ptr;
|
||||
// We are not prepared to handle a signal immediately; we only want to know if we get input on
|
||||
// our fd before the timeout. Use pselect to block all signals; we will handle signals
|
||||
// before the next call to readch().
|
||||
let mut sigs = MaybeUninit::uninit();
|
||||
let mut sigs = unsafe {
|
||||
libc::sigfillset(sigs.as_mut_ptr());
|
||||
sigs.assume_init()
|
||||
};
|
||||
|
||||
// pselect expects timeouts in nanoseconds.
|
||||
const NSEC_PER_MSEC: u64 = 1000 * 1000;
|
||||
const NSEC_PER_SEC: u64 = NSEC_PER_MSEC * 1000;
|
||||
let wait_nsec: u64 = (timeout.as_millis() as u64) * NSEC_PER_MSEC;
|
||||
let timeout = libc::timespec {
|
||||
tv_sec: (wait_nsec / NSEC_PER_SEC).try_into().unwrap(),
|
||||
tv_nsec: (wait_nsec % NSEC_PER_SEC).try_into().unwrap(),
|
||||
};
|
||||
|
||||
// We have one fd of interest.
|
||||
let mut fdset = MaybeUninit::uninit();
|
||||
let mut fdset = unsafe {
|
||||
libc::FD_ZERO(fdset.as_mut_ptr());
|
||||
fdset.assume_init()
|
||||
};
|
||||
unsafe {
|
||||
libc::FD_SET(in_fd, &mut fdset);
|
||||
}
|
||||
|
||||
let res = unsafe {
|
||||
libc::pselect(
|
||||
in_fd + 1,
|
||||
&mut fdset,
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
&timeout,
|
||||
&sigs,
|
||||
)
|
||||
};
|
||||
|
||||
// Prevent signal starvation on WSL causing the `torn_escapes.py` test to fail
|
||||
if is_windows_subsystem_for_linux(WSL::V1) {
|
||||
// Merely querying the current thread's sigmask is sufficient to deliver a pending signal
|
||||
let _ = unsafe { libc::pthread_sigmask(0, ptr::null(), &mut sigs) };
|
||||
}
|
||||
res > 0
|
||||
}
|
||||
|
||||
// Update the wait_on_escape_ms value in response to the fish_escape_delay_ms user variable being
|
||||
// set.
|
||||
pub fn update_wait_on_escape_ms(vars: &EnvStack) {
|
||||
@@ -831,25 +873,24 @@ fn readch(&mut self) -> CharEvent {
|
||||
return mevt;
|
||||
}
|
||||
|
||||
let rr = readb(self.get_in_fd(), /*blocking=*/ true);
|
||||
match rr {
|
||||
ReadbResult::Eof => {
|
||||
match next_input_event(self.get_in_fd()) {
|
||||
InputEventTrigger::Eof => {
|
||||
return CharEvent::Eof;
|
||||
}
|
||||
|
||||
ReadbResult::Interrupted => {
|
||||
InputEventTrigger::Interrupted => {
|
||||
self.select_interrupted();
|
||||
}
|
||||
|
||||
ReadbResult::UvarNotified => {
|
||||
InputEventTrigger::UvarNotified => {
|
||||
self.uvar_change_notified();
|
||||
}
|
||||
|
||||
ReadbResult::IOPortNotified => {
|
||||
InputEventTrigger::IOPortNotified => {
|
||||
self.ioport_notified();
|
||||
}
|
||||
|
||||
ReadbResult::Byte(read_byte) => {
|
||||
InputEventTrigger::Byte(read_byte) => {
|
||||
let mut have_escape_prefix = false;
|
||||
let mut buffer = vec![read_byte];
|
||||
let key_with_escape = if read_byte == 0x1b {
|
||||
@@ -874,8 +915,8 @@ fn readch(&mut self) -> CharEvent {
|
||||
let mut i = 0;
|
||||
let ok = loop {
|
||||
if i == buffer.len() {
|
||||
buffer.push(match readb(self.get_in_fd(), /*blocking=*/ true) {
|
||||
ReadbResult::Byte(b) => b,
|
||||
buffer.push(match next_input_event(self.get_in_fd()) {
|
||||
InputEventTrigger::Byte(b) => b,
|
||||
_ => 0,
|
||||
});
|
||||
}
|
||||
@@ -916,15 +957,16 @@ fn readch(&mut self) -> CharEvent {
|
||||
CharEvent::from_key_seq(KeyEvent::from_raw(c), seq)
|
||||
};
|
||||
}
|
||||
ReadbResult::NothingToRead => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_readb(&mut self, buffer: &mut Vec<u8>) -> Option<u8> {
|
||||
let ReadbResult::Byte(next) = readb(self.get_in_fd(), /*blocking=*/ false) else {
|
||||
let fd = self.get_in_fd();
|
||||
if !check_fd_readable(fd, Duration::from_millis(1)) {
|
||||
return None;
|
||||
};
|
||||
}
|
||||
let next = readb(fd)?;
|
||||
buffer.push(next);
|
||||
Some(next)
|
||||
}
|
||||
@@ -1298,54 +1340,11 @@ fn readch_timed(&mut self, wait_time_ms: usize) -> Option<CharEvent> {
|
||||
}
|
||||
terminal_protocols_enable_ifn();
|
||||
|
||||
// We are not prepared to handle a signal immediately; we only want to know if we get input on
|
||||
// our fd before the timeout. Use pselect to block all signals; we will handle signals
|
||||
// before the next call to readch().
|
||||
let mut sigs = MaybeUninit::uninit();
|
||||
let mut sigs = unsafe {
|
||||
libc::sigfillset(sigs.as_mut_ptr());
|
||||
sigs.assume_init()
|
||||
};
|
||||
|
||||
// pselect expects timeouts in nanoseconds.
|
||||
const NSEC_PER_MSEC: u64 = 1000 * 1000;
|
||||
const NSEC_PER_SEC: u64 = NSEC_PER_MSEC * 1000;
|
||||
let wait_nsec: u64 = (wait_time_ms as u64) * NSEC_PER_MSEC;
|
||||
let timeout = libc::timespec {
|
||||
tv_sec: (wait_nsec / NSEC_PER_SEC).try_into().unwrap(),
|
||||
tv_nsec: (wait_nsec % NSEC_PER_SEC).try_into().unwrap(),
|
||||
};
|
||||
|
||||
// We have one fd of interest.
|
||||
let mut fdset = MaybeUninit::uninit();
|
||||
let mut fdset = unsafe {
|
||||
libc::FD_ZERO(fdset.as_mut_ptr());
|
||||
fdset.assume_init()
|
||||
};
|
||||
let in_fd = self.get_in_fd();
|
||||
unsafe {
|
||||
libc::FD_SET(in_fd, &mut fdset);
|
||||
};
|
||||
let res = unsafe {
|
||||
libc::pselect(
|
||||
in_fd + 1,
|
||||
&mut fdset,
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
&timeout,
|
||||
&sigs,
|
||||
)
|
||||
};
|
||||
|
||||
// Prevent signal starvation on WSL causing the `torn_escapes.py` test to fail
|
||||
if is_windows_subsystem_for_linux(WSL::V1) {
|
||||
// Merely querying the current thread's sigmask is sufficient to deliver a pending signal
|
||||
let _ = unsafe { libc::pthread_sigmask(0, ptr::null(), &mut sigs) };
|
||||
}
|
||||
if res > 0 {
|
||||
return Some(self.readch());
|
||||
}
|
||||
None
|
||||
check_fd_readable(
|
||||
self.get_in_fd(),
|
||||
Duration::from_millis(u64::try_from(wait_time_ms).unwrap()),
|
||||
)
|
||||
.then(|| self.readch())
|
||||
}
|
||||
|
||||
/// Return the fd from which to read.
|
||||
|
||||
Reference in New Issue
Block a user