Time out terminal queries after a while

Add a timeout of 2 seconds queries; if any query takes longer, warn
about that and reduce the timeout  so we stop blocking the UI.  This 2
second delay could also happen when network latency is momentarily
really high, so we might want relax this in future.

Note that this timeout is only triggered by a single uninterrupted
poll() (and measured from the start of poll(), which should happen
shortly after sending the query). Any polls interrupted by signals
or uvars/IO port before the timeout would be hit do not matter.
We could change this in future.

Closes #11108
Closes #11117
This commit is contained in:
Johannes Altmanninger
2025-09-21 08:13:41 +02:00
parent 06ede39ec9
commit 7ef4e7dfe7
13 changed files with 165 additions and 31 deletions

View File

@@ -2,13 +2,53 @@
use proc_macro::TokenStream;
use std::{ffi::OsString, fs::OpenOptions, io::Write};
fn unescape_multiline_rust_string(s: String) -> String {
if !s.contains('\n') {
return s;
}
let mut unescaped = String::new();
enum State {
Ground,
Escaped,
ContinuationLineLeadingWhitespace,
}
use State::*;
let mut state = Ground;
for c in s.chars() {
match state {
Ground => match c {
'\\' => state = Escaped,
_ => {
unescaped.push(c);
}
},
Escaped => match c {
'\\' => {
unescaped.push('\\');
state = Ground
}
'\n' => state = ContinuationLineLeadingWhitespace,
_ => panic!("Unsupported escape sequence '\\{c}' in message string '{s}'"),
},
ContinuationLineLeadingWhitespace => match c {
' ' | '\t' => (),
_ => {
unescaped.push(c);
state = Ground
}
},
}
}
unescaped
}
fn append_po_entry_to_file(message: &TokenStream, file_name: &OsString) {
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(file_name)
.unwrap_or_else(|e| panic!("Could not open file {file_name:?}: {e}"));
let message_string = message.to_string();
let message_string = unescape_multiline_rust_string(message.to_string());
if message_string.contains('\n') {
panic!("Gettext strings may not contain unescaped newlines. Unescaped newline found in '{message_string}'")
}

View File

@@ -816,6 +816,11 @@ msgstr ""
msgid "%s and %s are mutually exclusive"
msgstr ""
#
#, c-format
msgid "%s could not read response to primary device attribute query after waiting for %d seconds. This is often due to a missing feature in your terminal. See 'help terminal-compatibility' or 'man fish-terminal-compatibility'. This %s process will no longer wait for outstanding queries, which disables some optional features."
msgstr ""
#, c-format
msgid "%s, version %s"
msgstr ""

View File

@@ -814,6 +814,11 @@ msgstr ""
msgid "%s and %s are mutually exclusive"
msgstr ""
#
#, c-format
msgid "%s could not read response to primary device attribute query after waiting for %d seconds. This is often due to a missing feature in your terminal. See 'help terminal-compatibility' or 'man fish-terminal-compatibility'. This %s process will no longer wait for outstanding queries, which disables some optional features."
msgstr ""
#, c-format
msgid "%s, version %s"
msgstr ""

View File

@@ -915,6 +915,11 @@ msgstr ""
msgid "%s and %s are mutually exclusive"
msgstr ""
#
#, c-format
msgid "%s could not read response to primary device attribute query after waiting for %d seconds. This is often due to a missing feature in your terminal. See 'help terminal-compatibility' or 'man fish-terminal-compatibility'. This %s process will no longer wait for outstanding queries, which disables some optional features."
msgstr ""
#, c-format
msgid "%s, version %s"
msgstr ""

View File

@@ -810,6 +810,11 @@ msgstr ""
msgid "%s and %s are mutually exclusive"
msgstr ""
#
#, c-format
msgid "%s could not read response to primary device attribute query after waiting for %d seconds. This is often due to a missing feature in your terminal. See 'help terminal-compatibility' or 'man fish-terminal-compatibility'. This %s process will no longer wait for outstanding queries, which disables some optional features."
msgstr ""
#, c-format
msgid "%s, version %s"
msgstr ""

View File

@@ -815,6 +815,11 @@ msgstr ""
msgid "%s and %s are mutually exclusive"
msgstr ""
#
#, c-format
msgid "%s could not read response to primary device attribute query after waiting for %d seconds. This is often due to a missing feature in your terminal. See 'help terminal-compatibility' or 'man fish-terminal-compatibility'. This %s process will no longer wait for outstanding queries, which disables some optional features."
msgstr ""
#, c-format
msgid "%s, version %s"
msgstr ""

View File

@@ -811,6 +811,11 @@ msgstr ""
msgid "%s and %s are mutually exclusive"
msgstr ""
#
#, c-format
msgid "%s could not read response to primary device attribute query after waiting for %d seconds. This is often due to a missing feature in your terminal. See 'help terminal-compatibility' or 'man fish-terminal-compatibility'. This %s process will no longer wait for outstanding queries, which disables some optional features."
msgstr ""
#, c-format
msgid "%s, version %s"
msgstr ""

View File

@@ -808,6 +808,11 @@ msgstr ""
msgid "%s and %s are mutually exclusive"
msgstr ""
#
#, c-format
msgid "%s could not read response to primary device attribute query after waiting for %d seconds. This is often due to a missing feature in your terminal. See 'help terminal-compatibility' or 'man fish-terminal-compatibility'. This %s process will no longer wait for outstanding queries, which disables some optional features."
msgstr ""
#, c-format
msgid "%s, version %s"
msgstr ""

View File

@@ -21,7 +21,7 @@
future_feature_flags,
input_common::{
match_key_event_to_key, CharEvent, InputEventQueue, InputEventQueuer, KeyEvent,
QueryResponseEvent, TerminalQuery,
QueryResponse, QueryResultEvent, TerminalQuery,
},
key::{char_to_symbol, Key},
nix::isatty,
@@ -95,16 +95,17 @@ fn process_input(streams: &mut IoStreams, continuous_mode: bool, verbose: bool)
handoff.enable_tty_protocols();
while (!first_char_seen || continuous_mode) && !check_exit_loop_maybe_warning(None) {
use QueryResultEvent::*;
let kevt = match queue.readch() {
CharEvent::Key(kevt) => kevt,
CharEvent::Readline(_) | CharEvent::Command(_) | CharEvent::Implicit(_) => continue,
CharEvent::QueryResponse(QueryResponseEvent::PrimaryDeviceAttributeResponse) => {
CharEvent::QueryResult(Response(QueryResponse::PrimaryDeviceAttribute) | Timeout) => {
if get_kitty_keyboard_capability() == Capability::Unknown {
set_kitty_keyboard_capability(|| {}, Capability::NotSupported);
}
continue;
}
CharEvent::QueryResponse(_) => continue,
CharEvent::QueryResult(_) => continue,
};
if verbose {
streams.out.append(L!("# decoded from: "));

View File

@@ -2,6 +2,7 @@
use std::os::unix::prelude::*;
use std::time::Duration;
#[derive(Clone, Copy)]
pub enum Timeout {
Duration(Duration),
Forever,

View File

@@ -781,7 +781,7 @@ pub fn read_char(&mut self) -> CharEvent {
match evt {
Key(_) => true,
Implicit(Eof) => true,
Readline(_) | Command(_) | Implicit(_) | QueryResponse(_) => false,
Readline(_) | Command(_) | Implicit(_) | QueryResult(_) => false,
}
});
@@ -823,7 +823,7 @@ pub fn read_char(&mut self) -> CharEvent {
self.push_front(evt);
self.mapping_execute_matching_or_generic();
}
CharEvent::Implicit(_) | CharEvent::QueryResponse(_) => {
CharEvent::Implicit(_) | CharEvent::QueryResult(_) => {
return evt;
}
}

View File

@@ -1,10 +1,11 @@
use crate::common::{
fish_reserved_codepoint, is_windows_subsystem_for_linux, read_blocked, shell_modes,
str2wcstring, WSL,
str2wcstring, PROGRAM_NAME, WSL,
};
use crate::env::{EnvStack, Environment};
use crate::fd_readable_set::{FdReadableSet, Timeout};
use crate::flog::{FloggableDebug, FloggableDisplay, FLOG};
use crate::global_safety::RelaxedAtomicBool;
use crate::key::{
self, alt, canonicalize_control_char, canonicalize_keyed_control_char, char_to_symbol,
function_key, shift, Key, Modifiers, ViewportPosition,
@@ -411,9 +412,15 @@ pub enum ImplicitEvent {
}
#[derive(Debug, Clone)]
pub enum QueryResponseEvent {
PrimaryDeviceAttributeResponse,
CursorPositionResponse(ViewportPosition),
pub enum QueryResponse {
PrimaryDeviceAttribute,
CursorPosition(ViewportPosition),
}
#[derive(Debug, Clone)]
pub enum QueryResultEvent {
Response(QueryResponse),
Timeout,
}
#[derive(Debug, Clone)]
@@ -430,7 +437,7 @@ pub enum CharEvent {
/// Any event that has no user-visible representation.
Implicit(ImplicitEvent),
QueryResponse(QueryResponseEvent),
QueryResult(QueryResultEvent),
}
impl FloggableDebug for CharEvent {}
@@ -532,6 +539,9 @@ enum InputEventTrigger {
// Our ioport reported a change, so service main thread requests.
IOPortNotified,
// No file descriptor was ready within the query timeout.
TimeoutElapsed,
}
fn readb(in_fd: RawFd) -> Option<u8> {
@@ -547,7 +557,7 @@ fn readb(in_fd: RawFd) -> Option<u8> {
Some(c)
}
fn next_input_event(in_fd: RawFd) -> InputEventTrigger {
fn next_input_event(in_fd: RawFd, timeout: Timeout) -> InputEventTrigger {
let mut fdset = FdReadableSet::new();
loop {
fdset.clear();
@@ -565,7 +575,7 @@ fn next_input_event(in_fd: RawFd) -> InputEventTrigger {
}
// Here's where we call select().
let select_res = fdset.check_readable(Timeout::Forever);
let select_res = fdset.check_readable(timeout);
if select_res < 0 {
let err = errno::errno().0;
if err == libc::EINTR || err == libc::EAGAIN {
@@ -576,6 +586,10 @@ fn next_input_event(in_fd: RawFd) -> InputEventTrigger {
return InputEventTrigger::Eof;
}
}
if select_res == 0 {
assert!(!matches!(timeout, Timeout::Forever));
return InputEventTrigger::TimeoutElapsed;
}
// select() did not return an error, so we may have a readable fd.
// The priority order is: uvars, stdin, ioport.
@@ -787,7 +801,7 @@ fn try_pop(&mut self) -> Option<CharEvent> {
if self.is_blocked_querying() {
use ImplicitEvent::*;
match self.get_input_data().queue.front()? {
CharEvent::QueryResponse(_) | CharEvent::Implicit(CheckExit | Eof) => {}
CharEvent::QueryResult(_) | CharEvent::Implicit(CheckExit | Eof) => {}
CharEvent::Key(_)
| CharEvent::Readline(_)
| CharEvent::Command(_)
@@ -817,7 +831,26 @@ fn readch(&mut self) -> CharEvent {
return mevt;
}
match next_input_event(self.get_in_fd()) {
const INITIAL_QUERY_TIMEOUT_SECONDS: u64 = 2;
static ABANDON_WAITING_FOR_QUERIES: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
match next_input_event(
self.get_in_fd(),
if self.is_blocked_querying() {
Timeout::Duration(if ABANDON_WAITING_FOR_QUERIES.load() {
// This should small enough so the delay on incompatible terminals is
// not noticeable, and high enough so we can still receive some query
// responses if we ever get into this state on a compatible terminal,
// which can happen after extreme (network) latency exceeds our initial
// timeout. In future, we should tolerate this better.
Duration::from_millis(30)
} else {
Duration::from_secs(INITIAL_QUERY_TIMEOUT_SECONDS)
})
} else {
Timeout::Forever
},
) {
InputEventTrigger::Eof => {
return CharEvent::Implicit(ImplicitEvent::Eof);
}
@@ -859,10 +892,12 @@ fn readch(&mut self) -> CharEvent {
let mut i = 0;
let ok = loop {
if i == buffer.len() {
buffer.push(match next_input_event(self.get_in_fd()) {
InputEventTrigger::Byte(b) => b,
_ => 0,
});
buffer.push(
match next_input_event(self.get_in_fd(), Timeout::Forever) {
InputEventTrigger::Byte(b) => b,
_ => 0,
},
);
}
match decode_input_byte(
&mut seq,
@@ -935,6 +970,26 @@ fn readch(&mut self) -> CharEvent {
extra.map(|extra| self.insert_front(extra));
return key_evt;
}
InputEventTrigger::TimeoutElapsed => {
if !ABANDON_WAITING_FOR_QUERIES.load() {
let program = PROGRAM_NAME.get().unwrap();
FLOG!(
warning,
wgettext_fmt!(
"%s could not read response to primary device attribute query after waiting for %d seconds. \
This is often due to a missing feature in your terminal. \
See 'help terminal-compatibility' or 'man fish-terminal-compatibility'. \
This %s process will no longer wait for outstanding queries, \
which disables some optional features.",
program,
INITIAL_QUERY_TIMEOUT_SECONDS,
program
),
);
ABANDON_WAITING_FOR_QUERIES.store(true);
}
return CharEvent::QueryResult(QueryResultEvent::Timeout);
}
}
}
}
@@ -1176,9 +1231,9 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
};
FLOG!(reader, "Received cursor position report y:", y, "x:", x);
let cursor_pos = ViewportPosition { x, y };
self.push_front(CharEvent::QueryResponse(
QueryResponseEvent::CursorPositionResponse(cursor_pos),
));
self.push_front(CharEvent::QueryResult(QueryResultEvent::Response(
QueryResponse::CursorPosition(cursor_pos),
)));
return None;
}
b'S' => masked_key(function_key(4)),
@@ -1229,9 +1284,9 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
_ => return None,
},
b'c' if private_mode == Some(b'?') => {
self.push_front(CharEvent::QueryResponse(
QueryResponseEvent::PrimaryDeviceAttributeResponse,
));
self.push_front(CharEvent::QueryResult(QueryResultEvent::Response(
QueryResponse::PrimaryDeviceAttribute,
)));
return None;
}
b'u' => {

View File

@@ -86,9 +86,10 @@
SearchFlags, SearchType,
};
use crate::input::init_input;
use crate::input_common::QueryResponse;
use crate::input_common::{
stop_query, CharEvent, CharInputStyle, CursorPositionQuery, CursorPositionQueryKind,
ImplicitEvent, InputData, QueryResponseEvent, ReadlineCmd, TerminalQuery,
ImplicitEvent, InputData, QueryResultEvent, ReadlineCmd, TerminalQuery,
};
use crate::io::IoChain;
use crate::key::ViewportPosition;
@@ -2569,12 +2570,13 @@ fn handle_char_event(&mut self, injected_event: Option<CharEvent>) -> ControlFlo
);
}
},
CharEvent::QueryResponse(query_result) => {
CharEvent::QueryResult(query_result) => {
let mut maybe_query = self.blocking_query();
let query = &mut maybe_query;
use QueryResponseEvent::*;
use QueryResponse::*;
use QueryResultEvent::*;
let query = match (&mut **query, query_result) {
(Some(TerminalQuery::Initial), PrimaryDeviceAttributeResponse) => {
(Some(TerminalQuery::Initial), Response(PrimaryDeviceAttribute) | Timeout) => {
if get_kitty_keyboard_capability() == Capability::Unknown {
set_kitty_keyboard_capability(
reader_save_screen_state,
@@ -2585,14 +2587,14 @@ fn handle_char_event(&mut self, injected_event: Option<CharEvent>) -> ControlFlo
}
(
Some(TerminalQuery::CursorPosition(cursor_pos_query)),
CursorPositionResponse(cursor_pos),
Response(CursorPosition(cursor_pos)),
) => {
cursor_pos_query.result = Some(cursor_pos);
maybe_query
}
(
Some(TerminalQuery::CursorPosition(cursor_pos_query)),
PrimaryDeviceAttributeResponse,
Response(PrimaryDeviceAttribute) | Timeout,
) => {
let cursor_pos_query = cursor_pos_query.clone();
drop(maybe_query);