Toggle terminal protocols lazily

Closes #10494
This commit is contained in:
Johannes Altmanninger 2024-05-16 10:52:19 +02:00
parent 6f9d5cf44c
commit 29f2da8d18
9 changed files with 74 additions and 123 deletions

View file

@ -1,7 +1,7 @@
//! Implementation of the fg builtin. //! Implementation of the fg builtin.
use crate::fds::make_fd_blocking; use crate::fds::make_fd_blocking;
use crate::input_common::TERMINAL_PROTOCOLS; use crate::input_common::terminal_protocols_disable_ifn;
use crate::reader::reader_write_title; use crate::reader::reader_write_title;
use crate::tokenizer::tok_command; use crate::tokenizer::tok_command;
use crate::wutil::perror; use crate::wutil::perror;
@ -148,15 +148,17 @@ pub fn fg(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Optio
let job_group = job.group(); let job_group = job.group();
job_group.set_is_foreground(true); job_group.set_is_foreground(true);
let tmodes = job_group.tmodes.borrow(); let tmodes = job_group.tmodes.borrow();
if job_group.wants_terminal() && tmodes.is_some() { if job_group.wants_terminal() {
let termios = tmodes.as_ref().unwrap(); terminal_protocols_disable_ifn();
let res = unsafe { libc::tcsetattr(STDIN_FILENO, TCSADRAIN, termios) }; if tmodes.is_some() {
if res < 0 { let termios = tmodes.as_ref().unwrap();
perror("tcsetattr"); let res = unsafe { libc::tcsetattr(STDIN_FILENO, TCSADRAIN, termios) };
if res < 0 {
perror("tcsetattr");
}
} }
} }
} }
assert!(TERMINAL_PROTOCOLS.get().borrow().is_none());
let mut transfer = TtyTransfer::new(); let mut transfer = TtyTransfer::new();
transfer.to_job_group(job.group.as_ref().unwrap()); transfer.to_job_group(job.group.as_ref().unwrap());
let resumed = job.resume(); let resumed = job.resume();

View file

@ -12,7 +12,6 @@ use crate::env::EnvMode;
use crate::env::Environment; use crate::env::Environment;
use crate::env::READ_BYTE_LIMIT; use crate::env::READ_BYTE_LIMIT;
use crate::env::{EnvVar, EnvVarFlags}; use crate::env::{EnvVar, EnvVarFlags};
use crate::input_common::terminal_protocols_enable_scoped;
use crate::libc::MB_CUR_MAX; use crate::libc::MB_CUR_MAX;
use crate::nix::isatty; use crate::nix::isatty;
use crate::reader::commandline_set_buffer; use crate::reader::commandline_set_buffer;
@ -586,9 +585,6 @@ pub fn read(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Opt
let stream_stdin_is_a_tty = isatty(streams.stdin_fd); let stream_stdin_is_a_tty = isatty(streams.stdin_fd);
// Enable terminal protocols if noninteractive.
let _terminal_protocols = stream_stdin_is_a_tty.then(terminal_protocols_enable_scoped);
// Normally, we either consume a line of input or all available input. But if we are reading a // Normally, we either consume a line of input or all available input. But if we are reading a
// line at a time, we need a middle ground where we only consume as many lines as we need to // line at a time, we need a middle ground where we only consume as many lines as we need to
// fill the given vars. // fill the given vars.

View file

@ -24,6 +24,7 @@ use crate::fork_exec::postfork::{
#[cfg(FISH_USE_POSIX_SPAWN)] #[cfg(FISH_USE_POSIX_SPAWN)]
use crate::fork_exec::spawn::PosixSpawner; use crate::fork_exec::spawn::PosixSpawner;
use crate::function::{self, FunctionProperties}; use crate::function::{self, FunctionProperties};
use crate::input_common::terminal_protocols_disable_ifn;
use crate::io::{ use crate::io::{
BufferedOutputStream, FdOutputStream, IoBufferfill, IoChain, IoClose, IoMode, IoPipe, BufferedOutputStream, FdOutputStream, IoBufferfill, IoChain, IoClose, IoMode, IoPipe,
IoStreams, OutputStream, SeparatedBuffer, StringOutputStream, IoStreams, OutputStream, SeparatedBuffer, StringOutputStream,
@ -71,6 +72,10 @@ pub fn exec_job(parser: &Parser, job: &Job, block_io: IoChain) -> bool {
return true; return true;
} }
if job.group().wants_terminal() {
terminal_protocols_disable_ifn();
}
// Handle an exec call. // Handle an exec call.
if job.processes()[0].typ == ProcessType::exec { if job.processes()[0].typ == ProcessType::exec {
// If we are interactive, perhaps disallow exec if there are background jobs. // If we are interactive, perhaps disallow exec if there are background jobs.

View file

@ -1,8 +1,7 @@
use libc::STDOUT_FILENO; use libc::STDOUT_FILENO;
use crate::common::{ use crate::common::{
fish_reserved_codepoint, is_windows_subsystem_for_linux, read_blocked, shell_modes, ScopeGuard, fish_reserved_codepoint, is_windows_subsystem_for_linux, read_blocked, shell_modes,
ScopeGuarding,
}; };
use crate::env::{EnvStack, Environment}; use crate::env::{EnvStack, Environment};
use crate::fd_readable_set::FdReadableSet; use crate::fd_readable_set::FdReadableSet;
@ -429,115 +428,77 @@ pub fn update_wait_on_sequence_key_ms(vars: &EnvStack) {
} }
} }
pub static TERMINAL_PROTOCOLS: MainThread<RefCell<Option<TerminalProtocols>>> = static TERMINAL_PROTOCOLS: MainThread<RefCell<Option<TerminalProtocols>>> =
MainThread::new(RefCell::new(None)); MainThread::new(RefCell::new(None));
fn terminal_protocols_enable() { pub(crate) static IS_TMUX: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
assert!(TERMINAL_PROTOCOLS.get().borrow().is_none());
TERMINAL_PROTOCOLS pub(crate) fn terminal_protocols_enable_ifn() {
.get() let mut term_protocols = TERMINAL_PROTOCOLS.get().borrow_mut();
.replace(Some(TerminalProtocols::new())); if term_protocols.is_some() {
return;
}
*term_protocols = Some(TerminalProtocols::new());
reader_current_data().map(|data| data.save_screen_state());
} }
fn terminal_protocols_disable() { pub(crate) fn terminal_protocols_disable_ifn() {
assert!(TERMINAL_PROTOCOLS.get().borrow().is_some());
TERMINAL_PROTOCOLS.get().replace(None); TERMINAL_PROTOCOLS.get().replace(None);
} }
pub fn terminal_protocols_enable_scoped() -> impl ScopeGuarding<Target = ()> { pub(crate) fn terminal_protocols_try_disable_ifn() {
terminal_protocols_enable(); if let Ok(mut term_protocols) = TERMINAL_PROTOCOLS.get().try_borrow_mut() {
ScopeGuard::new((), |()| terminal_protocols_disable()) *term_protocols = None;
}
} }
pub fn terminal_protocols_disable_scoped() -> impl ScopeGuarding<Target = ()> { struct TerminalProtocols {}
terminal_protocols_disable();
ScopeGuard::new((), |()| {
// If a child is stopped, this will already be enabled.
if TERMINAL_PROTOCOLS.get().borrow().is_none() {
terminal_protocols_enable();
if let Some(data) = reader_current_data() {
data.save_screen_state();
}
}
})
}
pub struct TerminalProtocols {
focus_events: bool,
}
impl TerminalProtocols { impl TerminalProtocols {
fn new() -> Self { fn new() -> Self {
terminal_protocols_enable_impl(); let sequences = concat!(
Self { "\x1b[?2004h", // Bracketed paste
focus_events: false, "\x1b[>4;1m", // XTerm's modifyOtherKeys
"\x1b[>5u", // CSI u with kitty progressive enhancement
"\x1b=", // set application keypad mode, so the keypad keys send unique codes
);
FLOG!(
term_protocols,
format!(
"Enabling extended keys and bracketed paste: {:?}",
sequences
)
);
let _ = write_to_fd(sequences.as_bytes(), STDOUT_FILENO);
if IS_TMUX.load() {
let _ = write_to_fd("\x1b[?1004h".as_bytes(), STDOUT_FILENO);
} }
Self {}
} }
} }
impl Drop for TerminalProtocols { impl Drop for TerminalProtocols {
fn drop(&mut self) { fn drop(&mut self) {
terminal_protocols_disable_impl(); let sequences = concat!(
if self.focus_events { "\x1b[?2004l",
"\x1b[>4;0m",
"\x1b[<1u", // Konsole breaks unless we pass an explicit number of entries to pop.
"\x1b>",
);
FLOG!(
term_protocols,
format!(
"Disabling extended keys and bracketed paste: {:?}",
sequences
)
);
let _ = write_to_fd(sequences.as_bytes(), STDOUT_FILENO);
if IS_TMUX.load() {
let _ = write_to_fd("\x1b[?1004l".as_bytes(), STDOUT_FILENO); let _ = write_to_fd("\x1b[?1004l".as_bytes(), STDOUT_FILENO);
} }
} }
} }
fn terminal_protocols_enable_impl() {
let sequences = concat!(
"\x1b[?2004h", // Bracketed paste
"\x1b[>4;1m", // XTerm's modifyOtherKeys
"\x1b[>5u", // CSI u with kitty progressive enhancement
"\x1b=", // set application keypad mode, so the keypad keys send unique codes
);
FLOG!(
term_protocols,
format!(
"Enabling extended keys and bracketed paste: {:?}",
sequences
)
);
let _ = write_to_fd(sequences.as_bytes(), STDOUT_FILENO);
}
fn terminal_protocols_disable_impl() {
let sequences = concat!(
"\x1b[?2004l",
"\x1b[>4;0m",
"\x1b[<1u", // Konsole breaks unless we pass an explicit number of entries to pop.
"\x1b>",
);
FLOG!(
term_protocols,
format!(
"Disabling extended keys and bracketed paste: {:?}",
sequences
)
);
let _ = write_to_fd(sequences.as_bytes(), STDOUT_FILENO);
}
pub(crate) static IS_TMUX: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
pub(crate) fn focus_events_enable_ifn() {
if !IS_TMUX.load() {
return;
}
let mut term_protocols = TERMINAL_PROTOCOLS.get().borrow_mut();
let Some(term_protocols) = term_protocols.as_mut() else {
panic!()
};
if !term_protocols.focus_events {
term_protocols.focus_events = true;
let _ = write_to_fd("\x1b[?1004h".as_bytes(), STDOUT_FILENO);
if let Some(data) = reader_current_data() {
data.save_screen_state();
}
}
}
fn parse_mask(mask: u32) -> Modifiers { fn parse_mask(mask: u32) -> Modifiers {
Modifiers { Modifiers {
ctrl: (mask & 4) != 0, ctrl: (mask & 4) != 0,
@ -1045,7 +1006,7 @@ pub trait InputEventQueuer {
if let Some(evt) = self.try_pop() { if let Some(evt) = self.try_pop() {
return Some(evt); return Some(evt);
} }
focus_events_enable_ifn(); terminal_protocols_enable_ifn();
// We are not prepared to handle a signal immediately; we only want to know if we get input on // 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 // our fd before the timeout. Use pselect to block all signals; we will handle signals

View file

@ -14,7 +14,6 @@ use crate::expand::{
}; };
use crate::fds::{open_dir, BEST_O_SEARCH}; use crate::fds::{open_dir, BEST_O_SEARCH};
use crate::global_safety::RelaxedAtomicBool; use crate::global_safety::RelaxedAtomicBool;
use crate::input_common::{terminal_protocols_disable_scoped, TERMINAL_PROTOCOLS};
use crate::io::IoChain; use crate::io::IoChain;
use crate::job_group::MaybeJobId; use crate::job_group::MaybeJobId;
use crate::operation_context::{OperationContext, EXPANSION_LIMIT_DEFAULT}; use crate::operation_context::{OperationContext, EXPANSION_LIMIT_DEFAULT};
@ -562,11 +561,6 @@ impl Parser {
Some(ParseExecutionContext::new(ps.clone(), block_io.clone())), Some(ParseExecutionContext::new(ps.clone(), block_io.clone())),
); );
// If interactive or inside noninteractive builtin read.
let terminal_protocols_enabled = TERMINAL_PROTOCOLS.get().borrow().is_some();
let terminal_protocols_disabled =
terminal_protocols_enabled.then(terminal_protocols_disable_scoped);
// Check the exec count so we know if anything got executed. // Check the exec count so we know if anything got executed.
let prev_exec_count = self.libdata().pods.exec_count; let prev_exec_count = self.libdata().pods.exec_count;
let prev_status_count = self.libdata().pods.status_count; let prev_status_count = self.libdata().pods.status_count;
@ -578,7 +572,6 @@ impl Parser {
let new_exec_count = self.libdata().pods.exec_count; let new_exec_count = self.libdata().pods.exec_count;
let new_status_count = self.libdata().pods.status_count; let new_status_count = self.libdata().pods.status_count;
drop(terminal_protocols_disabled);
ScopeGuarding::commit(exc); ScopeGuarding::commit(exc);
self.pop_block(scope_block); self.pop_block(scope_block);

View file

@ -71,11 +71,9 @@ use crate::history::{
}; };
use crate::input::init_input; use crate::input::init_input;
use crate::input::Inputter; use crate::input::Inputter;
use crate::input_common::terminal_protocols_disable_ifn;
use crate::input_common::IS_TMUX; use crate::input_common::IS_TMUX;
use crate::input_common::{ use crate::input_common::{terminal_protocols_enable_ifn, CharEvent, CharInputStyle, ReadlineCmd};
focus_events_enable_ifn, terminal_protocols_enable_scoped, CharEvent, CharInputStyle,
ReadlineCmd,
};
use crate::io::IoChain; use crate::io::IoChain;
use crate::kill::{kill_add, kill_replace, kill_yank, kill_yank_rotate}; use crate::kill::{kill_add, kill_replace, kill_yank, kill_yank_rotate};
use crate::libc::MB_CUR_MAX; use crate::libc::MB_CUR_MAX;
@ -794,16 +792,14 @@ pub fn reader_init() -> impl ScopeGuarding<Target = ()> {
// Set up our fixed terminal modes once, // Set up our fixed terminal modes once,
// so we don't get flow control just because we inherited it. // so we don't get flow control just because we inherited it.
let mut terminal_protocols = None;
if is_interactive_session() { if is_interactive_session() {
terminal_protocols = Some(terminal_protocols_enable_scoped());
if unsafe { libc::getpgrp() == libc::tcgetpgrp(STDIN_FILENO) } { if unsafe { libc::getpgrp() == libc::tcgetpgrp(STDIN_FILENO) } {
term_donate(/*quiet=*/ true); term_donate(/*quiet=*/ true);
} }
} }
ScopeGuard::new((), move |()| { ScopeGuard::new((), move |()| {
let _terminal_protocols = terminal_protocols;
restore_term_mode(); restore_term_mode();
terminal_protocols_disable_ifn();
}) })
} }
@ -1968,7 +1964,7 @@ impl ReaderData {
let mut accumulated_chars = WString::new(); let mut accumulated_chars = WString::new();
while accumulated_chars.len() < limit { while accumulated_chars.len() < limit {
focus_events_enable_ifn(); terminal_protocols_enable_ifn();
let evt = self.inputter.read_char(); let evt = self.inputter.read_char();
let CharEvent::Key(kevt) = &evt else { let CharEvent::Key(kevt) = &evt else {
event_needing_handling = Some(evt); event_needing_handling = Some(evt);

View file

@ -2,7 +2,7 @@ use std::num::NonZeroI32;
use crate::common::{exit_without_destructors, restore_term_foreground_process_group_for_exit}; use crate::common::{exit_without_destructors, restore_term_foreground_process_group_for_exit};
use crate::event::{enqueue_signal, is_signal_observed}; use crate::event::{enqueue_signal, is_signal_observed};
use crate::input_common::TERMINAL_PROTOCOLS; use crate::input_common::terminal_protocols_try_disable_ifn;
use crate::nix::getpid; use crate::nix::getpid;
use crate::reader::{reader_handle_sigint, reader_sighup}; use crate::reader::{reader_handle_sigint, reader_sighup};
use crate::termsize::TermsizeContainer; use crate::termsize::TermsizeContainer;
@ -89,9 +89,7 @@ extern "C" fn fish_signal_handler(
// 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, then die.
if !observed { if !observed {
restore_term_foreground_process_group_for_exit(); restore_term_foreground_process_group_for_exit();
if let Ok(mut term_protocols) = TERMINAL_PROTOCOLS.get().try_borrow_mut() { terminal_protocols_try_disable_ifn();
*term_protocols = None;
}
// Safety: signal() and raise() are async-signal-safe. // Safety: signal() and raise() are async-signal-safe.
unsafe { unsafe {
libc::signal(libc::SIGTERM, libc::SIG_DFL); libc::signal(libc::SIGTERM, libc::SIG_DFL);

View file

@ -13,7 +13,7 @@ send, sendline, sleep, expect_prompt, expect_re, expect_str = (
def expect_read_prompt(): def expect_read_prompt():
expect_re(r"\r\n?read> (\x1b\[\?1004h)?$") expect_re(r"\r\n?read> $")
def expect_marker(text): def expect_marker(text):
@ -56,12 +56,12 @@ print_var_contents("foo", "bar")
# read -c (see #8633) # read -c (see #8633)
sendline(r"read -c init_text somevar && echo $somevar") sendline(r"read -c init_text somevar && echo $somevar")
expect_re(r"\r\n?read> init_text(\x1b\[\?1004h)?$") expect_re(r"\r\n?read> init_text$")
sendline("someval") sendline("someval")
expect_prompt("someval\r\n") expect_prompt("someval\r\n")
sendline(r"read --command='some other text' somevar && echo $somevar") sendline(r"read --command='some other text' somevar && echo $somevar")
expect_re(r"\r\n?read> some other text(\x1b\[\?1004h)?$") expect_re(r"\r\n?read> some other text$")
sendline("another value") sendline("another value")
expect_prompt("another value\r\n") expect_prompt("another value\r\n")

View file

@ -48,7 +48,7 @@ expect_prompt()
sendline("function postexec --on-event fish_postexec; echo fish_postexec spotted; end") sendline("function postexec --on-event fish_postexec; echo fish_postexec spotted; end")
expect_prompt() expect_prompt()
sendline("read") sendline("read")
expect_re(r"\r\n?read> (\x1b\[\?1004h)?$", timeout=10) expect_re(r"\r\n?read> $", timeout=10)
sleep(0.1) sleep(0.1)
os.kill(sp.spawn.pid, signal.SIGINT) os.kill(sp.spawn.pid, signal.SIGINT)
expect_str("fish_postexec spotted", timeout=10) expect_str("fish_postexec spotted", timeout=10)