Adopt TtyHandoff in remaining places

This adopts the tty handoff in remaining places. The idea is to rationalize
when we enable and disable tty protocols (such as CSI-U).

In particular this removes the tty protocol disabling in Parser::eval_node
- that is intended to execute pure fish script and should not be talking to
the tty.
This commit is contained in:
Peter Ammon
2025-06-21 12:36:32 -07:00
parent c1d165de9d
commit d27f5a5293
9 changed files with 137 additions and 70 deletions

View File

@@ -139,12 +139,13 @@ pub fn fg(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Built
// Note if tty transfer fails, we still try running the job.
parser.job_promote_at(job_pos);
let mut handoff = TtyHandoff::new();
let _ = make_fd_blocking(STDIN_FILENO);
{
let job_group = job.group();
job_group.set_is_foreground(true);
if job.entitled_to_terminal() {
crate::input_common::terminal_protocols_disable_ifn();
handoff.disable_tty_protocols();
}
let tmodes = job_group.tmodes.borrow();
if job_group.wants_terminal() && tmodes.is_some() {
@@ -155,7 +156,6 @@ pub fn fg(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Built
}
}
}
let mut handoff = TtyHandoff::new();
handoff.to_job_group(job.group.as_ref().unwrap());
let resumed = job.resume();
if resumed {

View File

@@ -7,7 +7,7 @@
//!
//! Type "exit" or "quit" to terminate the program.
use std::{cell::RefCell, ops::ControlFlow, os::unix::prelude::OsStrExt, sync::atomic::Ordering};
use std::{cell::RefCell, ops::ControlFlow, os::unix::prelude::OsStrExt};
use libc::{STDIN_FILENO, TCSANOW, VEOF, VINTR};
use once_cell::unsync::OnceCell;
@@ -30,10 +30,13 @@
proc::set_interactive_session,
reader::{check_exit_loop_maybe_warning, initial_query, reader_init},
signal::signal_set_handlers,
terminal::{Capability, KITTY_KEYBOARD_SUPPORTED},
terminal::Capability,
threads,
topic_monitor::topic_monitor_init,
tty_handoff::{initialize_tty_metadata, TtyHandoff},
tty_handoff::{
get_kitty_keyboard_capability, initialize_tty_metadata, set_kitty_keyboard_capability,
TtyHandoff,
},
wchar::prelude::*,
wgetopt::{wopt, ArgType, WGetopter, WOption},
};
@@ -96,9 +99,8 @@ fn process_input(streams: &mut IoStreams, continuous_mode: bool, verbose: bool)
CharEvent::Key(kevt) => kevt,
CharEvent::Readline(_) | CharEvent::Command(_) | CharEvent::Implicit(_) => continue,
CharEvent::QueryResponse(QueryResponseEvent::PrimaryDeviceAttribute) => {
if KITTY_KEYBOARD_SUPPORTED.load(Ordering::Relaxed) == Capability::Unknown as _ {
KITTY_KEYBOARD_SUPPORTED
.store(Capability::NotSupported as _, Ordering::Release);
if get_kitty_keyboard_capability() == Capability::Unknown {
set_kitty_keyboard_capability(Capability::NotSupported);
}
continue;
}

View File

@@ -12,7 +12,6 @@
use crate::env::READ_BYTE_LIMIT;
use crate::env::{EnvVar, EnvVarFlags};
use crate::input_common::decode_input_byte;
use crate::input_common::terminal_protocols_disable_ifn;
use crate::input_common::DecodeState;
use crate::input_common::InvalidPolicy;
use crate::nix::isatty;
@@ -22,6 +21,7 @@
use crate::tokenizer::Tokenizer;
use crate::tokenizer::TOK_ACCEPT_UNFINISHED;
use crate::tokenizer::TOK_ARGUMENT_LIST;
use crate::tty_handoff::TtyHandoff;
use crate::wcstringutil::split_about;
use crate::wcstringutil::split_string_tok;
use crate::wutil;
@@ -244,9 +244,10 @@ fn read_interactive(
let mline = {
let _interactive = parser.push_scope(|s| s.is_interactive = true);
let mut scoped_handoff = TtyHandoff::new();
scoped_handoff.enable_tty_protocols();
reader_readline(parser, NonZeroUsize::try_from(nchars).ok())
};
terminal_protocols_disable_ifn();
if let Some(line) = mline {
*buff = line;
if nchars > 0 && nchars < buff.len() {

View File

@@ -9,13 +9,10 @@
self, alt, canonicalize_control_char, canonicalize_keyed_control_char, char_to_symbol, ctrl,
function_key, shift, Key, Modifiers, ViewportPosition,
};
use crate::reader::reader_current_data;
use crate::reader::reader_test_and_clear_interrupted;
use crate::terminal::{
Capability, KITTY_KEYBOARD_SUPPORTED, SCROLL_FORWARD_SUPPORTED, SCROLL_FORWARD_TERMINFO_CODE,
};
use crate::terminal::{Capability, SCROLL_FORWARD_SUPPORTED, SCROLL_FORWARD_TERMINFO_CODE};
use crate::threads::iothread_port;
use crate::tty_handoff::set_tty_protocols_active;
use crate::tty_handoff::set_kitty_keyboard_capability;
use crate::universal_notifier::default_notifier;
use crate::wchar::{encode_byte_to_char, prelude::*};
use crate::wutil::encoding::{mbrtowc, mbstate_t, zero_mbstate};
@@ -648,22 +645,6 @@ fn parse_mask(mask: u32) -> (Modifiers, bool) {
(modifiers, caps_lock)
}
// Trampolines to enable or disable terminal protocols.
pub fn terminal_protocols_enable_ifn() {
if set_tty_protocols_active(true) {
// Our tty has been modified, so save the current timestamps so that
// the reader doesn't believe it has to repaint the prompt.
reader_current_data().map(|data| data.save_screen_state());
}
}
pub fn terminal_protocols_disable_ifn() {
if set_tty_protocols_active(false) {
// Our tty has been modified, so save the current timestamps so that
// the reader doesn't believe it has to repaint the prompt.
reader_current_data().map(|data| data.save_screen_state());
}
}
// A data type used by the input machinery.
pub struct InputData {
// The file descriptor from which we read input, often stdin.
@@ -1187,7 +1168,7 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
reader,
"Received kitty progressive enhancement flags, marking as supported"
);
KITTY_KEYBOARD_SUPPORTED.store(Capability::Supported as _, Ordering::Release);
set_kitty_keyboard_capability(Capability::Supported);
return None;
}
@@ -1421,7 +1402,6 @@ fn readch_timed(&mut self, wait_time_ms: usize) -> Option<CharEvent> {
if let Some(evt) = self.try_pop() {
return Some(evt);
}
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

View File

@@ -14,7 +14,7 @@
};
use crate::fds::{open_dir, BEST_O_SEARCH};
use crate::global_safety::RelaxedAtomicBool;
use crate::input_common::{terminal_protocols_disable_ifn, TerminalQuery};
use crate::input_common::TerminalQuery;
use crate::io::IoChain;
use crate::job_group::MaybeJobId;
use crate::operation_context::{OperationContext, EXPANSION_LIMIT_DEFAULT};
@@ -681,8 +681,6 @@ pub fn eval_node<T: Node>(
// Create a new execution context.
let mut execution_context = ExecutionContext::new(ps, block_io.clone(), &self.line_counter);
terminal_protocols_disable_ifn();
// Check the exec count so we know if anything got executed.
let prev_exec_count = self.libdata().exec_count;
let prev_status_count = self.libdata().status_count;

View File

@@ -11,6 +11,11 @@
//! When the user searches forward, i.e. presses Alt-down, the list is consulted for previous search
//! result, and subsequent backwards searches are also handled by consulting the list up until the
//! end of the list is reached, at which point regular searching will commence.
//!
//! In general interactive reads work with the tty protocols (CSI-U, etc) enabled; these are disabled
//! before calling out to fish script, wildcards, or completions. Note CSI-U protocol prevents
//! control-C from generating SIGINT, so failing to disable these would prevent cancellation of wildcard
//! expansion, etc.
use libc::{
c_char, ECHO, EINTR, EIO, EISDIR, ENOTTY, EPERM, ESRCH, ICANON, ICRNL, IEXTEN, INLCR, IXOFF,
@@ -81,14 +86,9 @@
SearchType,
};
use crate::input::init_input;
use crate::input_common::stop_query;
use crate::input_common::terminal_protocols_disable_ifn;
use crate::input_common::CursorPositionQuery;
use crate::input_common::ImplicitEvent;
use crate::input_common::QueryResponseEvent;
use crate::input_common::TerminalQuery;
use crate::input_common::{
terminal_protocols_enable_ifn, CharEvent, CharInputStyle, InputData, ReadlineCmd,
stop_query, CharEvent, CharInputStyle, CursorPositionQuery, ImplicitEvent, InputData,
QueryResponseEvent, ReadlineCmd, TerminalQuery,
};
use crate::io::IoChain;
use crate::key::ViewportPosition;
@@ -131,9 +131,7 @@
QueryCursorPosition, QueryKittyKeyboardProgressiveEnhancements, QueryPrimaryDeviceAttribute,
QueryXtgettcap, QueryXtversion,
};
use crate::terminal::{
Capability, KITTY_KEYBOARD_SUPPORTED, SCROLL_FORWARD_SUPPORTED, SCROLL_FORWARD_TERMINFO_CODE,
};
use crate::terminal::{Capability, SCROLL_FORWARD_SUPPORTED, SCROLL_FORWARD_TERMINFO_CODE};
use crate::termsize::{termsize_invalidate_tty, termsize_last, termsize_update};
use crate::text_face::parse_text_face;
use crate::text_face::TextFace;
@@ -147,7 +145,10 @@
tok_command, MoveWordStateMachine, MoveWordStyle, TokenType, Tokenizer, TOK_ACCEPT_UNFINISHED,
TOK_SHOW_COMMENTS,
};
use crate::tty_handoff::{initialize_tty_metadata, tty_metadata};
use crate::tty_handoff::{
get_kitty_keyboard_capability, get_tty_protocols_active, initialize_tty_metadata,
safe_deactivate_tty_protocols, set_kitty_keyboard_capability, tty_metadata, TtyHandoff,
};
use crate::wchar::prelude::*;
use crate::wcstringutil::string_prefixes_string_maybe_case_insensitive;
use crate::wcstringutil::{
@@ -712,6 +713,11 @@ fn read_i(parser: &Parser) {
let mut data = reader_push(parser, &history_session_id(parser.vars()), conf);
data.import_history_if_necessary();
// Set up tty protocols. These should be enabled while we're reading interactively,
// and disabled before we run fish script, wildcards, or completions. This is scoped.
// Note this may be disabled within the loop, e.g. when running fish script bound to keys.
let mut tty = TtyHandoff::new();
while !check_exit_loop_maybe_warning(Some(&mut data)) {
RUN_COUNT.fetch_add(1, Ordering::Relaxed);
@@ -723,6 +729,8 @@ fn read_i(parser: &Parser) {
continue;
}
// Got a command. Disable tty protocols while we execute it.
tty.disable_tty_protocols();
data.clear(EditableLineTag::Commandline);
data.update_buff_pos(EditableLineTag::Commandline, None);
BufferedOutputter::new(Outputter::stdoutput()).write_command(Osc133CommandStart(&command));
@@ -758,7 +766,8 @@ fn read_i(parser: &Parser) {
}
reader_pop();
// If we got SIGHUP, ensure the tty is redirected.
// If we got SIGHUP, ensure the tty is redirected and release tty handoff without
// trying to muck with protocols.
if reader_received_sighup() {
// If we are the top-level reader, then we translate SIGHUP into exit_forced.
redirect_tty_after_sighup();
@@ -892,11 +901,9 @@ pub fn reader_init(will_restore_foreground_pgroup: bool) {
}
}
// TODO(pca): this is run in our "AT_EXIT" handler from a SIGTERM handler.
// It must be made async-signal-safe (or not invoked).
pub fn reader_deinit(restore_foreground_pgroup: bool) {
safe_restore_term_mode();
crate::input_common::terminal_protocols_disable_ifn();
safe_deactivate_tty_protocols();
if restore_foreground_pgroup {
restore_term_foreground_process_group_for_exit();
}
@@ -2175,6 +2182,8 @@ impl<'a> Reader<'a> {
/// Read a command to execute, respecting input bindings.
/// Return the command, or none if we were asked to cancel (e.g. SIGHUP).
fn readline(&mut self, nchars: Option<NonZeroUsize>) -> Option<WString> {
let mut tty = TtyHandoff::new();
self.rls = Some(ReadlineLoopState::new());
// Suppress fish_trace during executing key bindings.
@@ -2242,11 +2251,18 @@ fn readline(&mut self, nchars: Option<NonZeroUsize>) -> Option<WString> {
self.force_exec_prompt_and_repaint = true;
while !self.rls().finished && !check_exit_loop_maybe_warning(Some(self)) {
// Enable tty protocols while we read input.
tty.enable_tty_protocols();
if self.handle_char_event(None).is_break() {
break;
}
}
// Disable tty protocols now that we're going to execute a command.
if tty.disable_tty_protocols() {
self.save_screen_state();
}
if self.conf.transient_prompt {
self.exec_prompt(true, true);
}
@@ -2306,10 +2322,16 @@ fn readline(&mut self, nchars: Option<NonZeroUsize>) -> Option<WString> {
fn eval_bind_cmd(&mut self, cmd: &wstr) {
let last_statuses = self.parser.vars().get_last_statuses();
let prev_exec_external_count = self.parser.libdata().exec_external_count;
// Disable TTY protocols while we run a bind command, because it may call out.
let mut scoped_tty = TtyHandoff::new();
let mut modified_tty = scoped_tty.disable_tty_protocols();
self.parser.eval(cmd, &IoChain::new());
self.parser.set_last_statuses(last_statuses);
if self.parser.libdata().exec_external_count != prev_exec_external_count
&& self.data.left_prompt_buff.contains('\n')
modified_tty |= scoped_tty.reclaim();
if modified_tty
|| (self.parser.libdata().exec_external_count != prev_exec_external_count
&& self.data.left_prompt_buff.contains('\n'))
{
self.save_screen_state();
}
@@ -2351,7 +2373,6 @@ fn read_normal_chars(&mut self) -> Option<CharEvent> {
let mut accumulated_chars = WString::new();
while accumulated_chars.len() < limit {
terminal_protocols_enable_ifn();
let evt = self.read_char();
let CharEvent::Key(kevt) = &evt else {
event_needing_handling = Some(evt);
@@ -2545,11 +2566,11 @@ fn handle_char_event(&mut self, injected_event: Option<CharEvent>) -> ControlFlo
// Rogue reply.
return ControlFlow::Continue(());
}
if KITTY_KEYBOARD_SUPPORTED.load(Ordering::Relaxed)
== Capability::Unknown as _
{
KITTY_KEYBOARD_SUPPORTED
.store(Capability::NotSupported as _, Ordering::Release);
if get_kitty_keyboard_capability() == Capability::Unknown {
set_kitty_keyboard_capability(Capability::NotSupported);
// We may have written to the tty, so save the screen state
// so we don't repaint.
self.screen.save_status();
}
}
QueryResponseEvent::CursorPositionReport(cursor_pos) => {
@@ -2796,7 +2817,12 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
}
} else {
// Either the user hit tab only once, or we had no visible completion list.
// Disable tty protocols while we compute completions, so that control-C
// triggers SIGINT (suppressed by CSI-U).
let mut tty = TtyHandoff::new();
tty.disable_tty_protocols();
self.compute_and_apply_completions(c);
tty.reclaim();
}
}
rl::PagerToggleSearch => {
@@ -4587,6 +4613,10 @@ fn exec_prompt(&mut self, full_prompt: bool, final_prompt: bool) {
// Prompts must be run non-interactively.
let _noninteractive = self.parser.push_scope(|s| s.is_interactive = false);
// Suppress TTY protocols in a scoped way so that e.g. control-C can cancel the prompt.
let mut scoped_tty = TtyHandoff::new();
scoped_tty.disable_tty_protocols();
// Update the termsize now.
// This allows prompts to react to $COLUMNS.
self.update_termsize();
@@ -5680,6 +5710,10 @@ fn check_for_orphaned_process(loop_count: usize, shell_pgid: libc::pid_t) -> boo
/// Run the specified command with the correct terminal modes, and while taking care to perform job
/// notification, set the title, etc.
fn reader_run_command(parser: &Parser, cmd: &wstr) -> EvalRes {
assert!(
!get_tty_protocols_active(),
"TTY protocols should not be active"
);
let ft = tok_command(cmd);
// Provide values for `status current-command` and `status current-commandline`
@@ -6232,6 +6266,10 @@ fn compute_and_apply_completions(&mut self, c: ReadlineCmd) {
c,
ReadlineCmd::Complete | ReadlineCmd::CompleteAndSearch
));
assert!(
!get_tty_protocols_active(),
"should not be called with TTY protocols active"
);
// Remove a trailing backslash. This may trigger an extra repaint, but this is
// rare.
@@ -6261,9 +6299,6 @@ fn compute_and_apply_completions(&mut self, c: ReadlineCmd) {
token_range.start += cmdsub_range.start;
token_range.end += cmdsub_range.start;
// Wildcard expansion and completion below check for cancellation.
terminal_protocols_disable_ifn();
// Check if we have a wildcard within this string; if so we first attempt to expand the
// wildcard; if that succeeds we don't then apply user completions (#8593).
let mut wc_expanded = WString::new();

View File

@@ -7,6 +7,7 @@
use crate::reader::{reader_handle_sigint, reader_sighup, safe_restore_term_mode};
use crate::termsize::TermsizeContainer;
use crate::topic_monitor::{topic_monitor_principal, Generation, GenerationsList, Topic};
use crate::tty_handoff::{safe_deactivate_tty_protocols, safe_mark_tty_invalid};
use crate::wchar::prelude::*;
use crate::wutil::{fish_wcstoi, perror};
use errno::{errno, set_errno};
@@ -83,13 +84,15 @@ extern "C" fn fish_signal_handler(
// Exit unless the signal was trapped.
if !observed {
reader_sighup();
safe_mark_tty_invalid();
}
topic_monitor_principal().post(Topic::sighupint);
}
libc::SIGTERM => {
// Handle sigterm. The only thing we do is restore the front process ID, then die.
// Handle sigterm. The only thing we do is restore the front process ID and disable protocols, then die.
if !observed {
safe_restore_term_mode();
safe_deactivate_tty_protocols();
// Safety: signal() and raise() are async-signal-safe.
unsafe {
libc::signal(libc::SIGTERM, libc::SIG_DFL);

View File

@@ -216,15 +216,14 @@ fn maybe_terminfo(
true
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u8)]
pub(crate) enum Capability {
pub enum Capability {
Unknown,
Supported,
NotSupported,
}
pub(crate) static KITTY_KEYBOARD_SUPPORTED: AtomicU8 = AtomicU8::new(Capability::Unknown as _);
pub(crate) static SCROLL_FORWARD_SUPPORTED: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
pub(crate) static SCROLL_FORWARD_TERMINFO_CODE: &str = "indn";

View File

@@ -12,14 +12,14 @@
KittyKeyboardProgressiveEnhancementsDisable, KittyKeyboardProgressiveEnhancementsEnable,
ModifyOtherKeysDisable, ModifyOtherKeysEnable,
};
use crate::terminal::{Capability, Output, Outputter, KITTY_KEYBOARD_SUPPORTED};
use crate::terminal::{Capability, Output, Outputter};
use crate::threads::assert_is_main_thread;
use crate::wchar_ext::ToWString;
use crate::wutil::perror;
use libc::{EINVAL, ENOTTY, EPERM, STDIN_FILENO, WNOHANG};
use std::mem::MaybeUninit;
use std::os::fd::BorrowedFd;
use std::sync::atomic::{AtomicPtr, Ordering};
use std::sync::atomic::{AtomicPtr, AtomicU8, Ordering};
// Facts about our environment, which inform how we handle the tty.
#[derive(Debug, Copy, Clone)]
@@ -60,6 +60,35 @@ fn detect() -> Self {
}
}
// Whether CSI-U ("Kitty") support is present in the TTY.
static KITTY_KEYBOARD_SUPPORTED: AtomicU8 = AtomicU8::new(Capability::Unknown as _);
// Get the support capability for CSI-U ("Kitty") protocols.
pub fn get_kitty_keyboard_capability() -> Capability {
let cap = KITTY_KEYBOARD_SUPPORTED.load(Ordering::Relaxed);
match cap {
x if x == Capability::Supported as _ => Capability::Supported,
x if x == Capability::NotSupported as _ => Capability::NotSupported,
_ => Capability::Unknown,
}
}
// Set CSI-U ("Kitty") support capability.
// This correctly handles the case where we think protocols are already enabled.
pub fn set_kitty_keyboard_capability(cap: Capability) {
assert_is_main_thread();
// Disable and renable protocols around capabilities.
let mut tty = TtyHandoff::new();
tty.disable_tty_protocols();
KITTY_KEYBOARD_SUPPORTED.store(cap as _, Ordering::Relaxed);
FLOG!(
term_protocols,
"Set Kitty keyboard capability to",
format!("{:?}", cap)
);
tty.reclaim();
}
// Helper to determine which keyboard protocols to enable.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum ProtocolKind {
@@ -215,10 +244,13 @@ pub fn initialize_tty_metadata() {
// A marker of the current state of the tty protocols.
static TTY_PROTOCOLS_ACTIVE: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
// A marker that the tty has been closed (SIGHUP, etc) and so we should not try to write to it.
static TTY_INVALID: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
// Enable or disable TTY protocols by writing the appropriate commands to the tty.
// Return true if we emitted any bytes to the tty.
// Note this does NOT intialize the TTY protocls if not already initialized.
pub fn set_tty_protocols_active(enable: bool) -> bool {
fn set_tty_protocols_active(enable: bool) -> bool {
assert_is_main_thread();
// Have protocols at all? We require someone else to have initialized them.
let Some(protocols) = tty_protocols() else {
@@ -226,11 +258,17 @@ pub fn set_tty_protocols_active(enable: bool) -> bool {
};
// Already set?
// Note we don't need atomic swaps as this is only called on the main thread.
// Also note we (logically) set and clear this even if we got SIGHUP.
if TTY_PROTOCOLS_ACTIVE.load() == enable {
return false;
}
TTY_PROTOCOLS_ACTIVE.store(enable);
// Did we get SIGHUP?
if TTY_INVALID.load() {
return false;
}
// Write the commands to the tty, ignoring errors.
let commands = protocols.safe_get_commands(enable);
let _ = common::write_loop(&libc::STDOUT_FILENO, commands);
@@ -259,10 +297,15 @@ pub fn safe_deactivate_tty_protocols() {
// No protocols set, nothing to do.
return;
};
if !TTY_PROTOCOLS_ACTIVE.load() {
return;
}
// Did we get SIGHUP?
if TTY_INVALID.load() {
return;
}
TTY_PROTOCOLS_ACTIVE.store(false);
let commands = protocols.safe_get_commands(false);
// Safety: just writing data to stdout.
@@ -270,6 +313,12 @@ pub fn safe_deactivate_tty_protocols() {
let _ = nix::unistd::write(stdout_fd, commands);
}
// Called from a signal handler to mark the tty as invalid (e.g. SIGHUP).
// This suppresses any further attempts to write protocols to the tty,
pub fn safe_mark_tty_invalid() {
TTY_INVALID.store(true);
}
// Allows transferring the tty to a job group, while it runs, in a scoped fashion.
// This has several responsibilities:
// - Invoking tcsetpgrp() to transfer the tty to the job group.