diff --git a/src/builtins/fg.rs b/src/builtins/fg.rs index e135a088f..2ab762368 100644 --- a/src/builtins/fg.rs +++ b/src/builtins/fg.rs @@ -6,7 +6,9 @@ use crate::tokenizer::tok_command; use crate::wutil::perror; use crate::{env::EnvMode, tty_handoff::TtyHandoff}; -use libc::{STDIN_FILENO, TCSADRAIN}; +use libc::STDIN_FILENO; +use nix::sys::termios; +use std::os::fd::BorrowedFd; use super::prelude::*; @@ -142,9 +144,14 @@ pub fn fg(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Built } let tmodes = job_group.tmodes.borrow(); if job_group.wants_terminal() && tmodes.is_some() { - let termios = tmodes.as_ref().unwrap(); - let res = unsafe { libc::tcsetattr(STDIN_FILENO, TCSADRAIN, termios) }; - if res < 0 { + let tmodes = tmodes.as_ref().unwrap(); + if termios::tcsetattr( + unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }, + termios::SetArg::TCSADRAIN, + tmodes, + ) + .is_err() + { perror("tcsetattr"); } } diff --git a/src/builtins/fish_key_reader.rs b/src/builtins/fish_key_reader.rs index ca1c94757..e33a23bac 100644 --- a/src/builtins/fish_key_reader.rs +++ b/src/builtins/fish_key_reader.rs @@ -47,7 +47,7 @@ fn should_exit( for evt in [VINTR, VEOF] { let modes = shell_modes(); - let cc = Key::from_single_byte(modes.c_cc[evt]); + let cc = Key::from_single_byte(modes.control_chars[evt]); if match_key_event_to_key(&key_evt, &cc).is_some() { if recent_keys @@ -61,7 +61,7 @@ fn should_exit( } streams.err.append(&wgettext_fmt!( "Press ctrl-%c again to exit\n", - char::from(modes.c_cc[evt] + 0x60) + char::from(modes.control_chars[evt] + 0x60) )); return false; } @@ -162,8 +162,8 @@ fn setup_and_process_keys( let modes = shell_modes(); streams.err.appendln(&wgettext_fmt!( "or press ctrl-%c or ctrl-%c twice in a row.", - char::from(modes.c_cc[VINTR] + 0x60), - char::from(modes.c_cc[VEOF] + 0x60) + char::from(modes.control_chars[VINTR] + 0x60), + char::from(modes.control_chars[VEOF] + 0x60) )); streams.err.appendln(L!("\n")); } diff --git a/src/common.rs b/src/common.rs index 854b6dc3a..d44b4c34e 100644 --- a/src/common.rs +++ b/src/common.rs @@ -19,6 +19,7 @@ use fish_widestring::{ ENCODE_DIRECT_END, decode_byte_from_char, encode_byte_to_char, subslice_position, }; +use nix::sys::termios::Termios; use std::env; use std::ffi::{CStr, CString, OsStr, OsString}; use std::os::unix::prelude::*; @@ -884,7 +885,7 @@ pub fn read_unquoted_escape( Some(in_pos) } -pub fn shell_modes() -> MutexGuard<'static, libc::termios> { +pub fn shell_modes() -> MutexGuard<'static, Termios> { crate::reader::SHELL_MODES.lock().unwrap() } diff --git a/src/input_common.rs b/src/input_common.rs index ded93594c..61fe0c1b6 100644 --- a/src/input_common.rs +++ b/src/input_common.rs @@ -878,7 +878,7 @@ fn readch(&mut self) -> CharEvent { self.push_back(evt); } }); - let vintr = shell_modes().c_cc[libc::VINTR]; + let vintr = shell_modes().control_chars[libc::VINTR]; if vintr != 0 && key.is_some_and(|key| { match_key_event_to_key(&key, &Key::from_single_byte(vintr)) @@ -1619,7 +1619,7 @@ fn prepare_to_select(&mut self) {} fn select_interrupted(&mut self) {} fn enqueue_interrupt_key(&mut self) { - let vintr = shell_modes().c_cc[libc::VINTR]; + let vintr = shell_modes().control_chars[libc::VINTR]; if vintr != 0 { let interrupt_evt = CharEvent::from_key(KeyEvent::from_single_byte(vintr)); if stop_query(self.blocking_query()) { diff --git a/src/job_group.rs b/src/job_group.rs index d90d2b7f1..45aa79a57 100644 --- a/src/job_group.rs +++ b/src/job_group.rs @@ -2,6 +2,7 @@ use crate::prelude::*; use crate::proc::{JobGroupRef, Pid}; use crate::signal::Signal; +use nix::sys::termios::Termios; use std::cell::RefCell; use std::num::NonZeroU32; use std::sync::atomic::{AtomicI32, Ordering}; @@ -60,7 +61,7 @@ fn to_arg(self) -> fish_printf::Arg<'a> { pub struct JobGroup { /// If set, the saved terminal modes of this job. This needs to be saved so that we can restore /// the terminal to the same state when resuming a stopped job. - pub tmodes: RefCell>, + pub tmodes: RefCell>, /// Whether job control is enabled in this `JobGroup` or not. /// /// If this is set, then the first process in the root job must be external, as it will become diff --git a/src/reader/reader.rs b/src/reader/reader.rs index 8ed726d45..fc0ca7a93 100644 --- a/src/reader/reader.rs +++ b/src/reader/reader.rs @@ -127,10 +127,10 @@ }; use fish_wcstringutil::{IsPrefix, is_prefix}; use libc::{ - _POSIX_VDISABLE, ECHO, EINTR, EIO, EISDIR, ENOTTY, EPERM, ESRCH, FLUSHO, ICANON, ICRNL, IEXTEN, - INLCR, IXOFF, IXON, O_NONBLOCK, O_RDONLY, ONLCR, OPOST, SIGINT, STDERR_FILENO, STDIN_FILENO, - STDOUT_FILENO, TCSANOW, VMIN, VQUIT, VSUSP, VTIME, c_char, + _POSIX_VDISABLE, EIO, EISDIR, ENOTTY, EPERM, ESRCH, O_NONBLOCK, O_RDONLY, SIGINT, + STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO, VMIN, VQUIT, VSUSP, VTIME, c_char, }; +use nix::sys::termios::{self, SetArg, Termios, tcgetattr}; use nix::{ fcntl::OFlag, sys::{ @@ -144,7 +144,6 @@ cell::UnsafeCell, cmp, io::BufReader, - mem::MaybeUninit, num::NonZeroUsize, ops::{ControlFlow, Range}, os::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd}, @@ -169,16 +168,20 @@ enum ExitState { static EXIT_STATE: AtomicU8 = AtomicU8::new(ExitState::None as u8); -pub static SHELL_MODES: LazyLock> = - LazyLock::new(|| Mutex::new(unsafe { std::mem::zeroed() })); +fn zeroed_termios() -> Termios { + let termios: libc::termios = unsafe { std::mem::zeroed() }; + termios.into() +} + +pub static SHELL_MODES: LazyLock> = LazyLock::new(|| Mutex::new(zeroed_termios())); /// The valid terminal modes on startup. /// Warning: this is read from the SIGTERM handler! Hence the raw global. static TERMINAL_MODE_ON_STARTUP: OnceLock = OnceLock::new(); /// Mode we use to execute programs. -static TTY_MODES_FOR_EXTERNAL_CMDS: LazyLock> = - LazyLock::new(|| Mutex::new(unsafe { std::mem::zeroed() })); +static TTY_MODES_FOR_EXTERNAL_CMDS: LazyLock> = + LazyLock::new(|| Mutex::new(zeroed_termios())); static RUN_COUNT: AtomicU64 = AtomicU64::new(0); @@ -223,13 +226,11 @@ fn redirect_tty_after_sighup() { }; let fd = devnull.as_raw_fd(); for stdfd in [STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO] { - let mut t = std::mem::MaybeUninit::uninit(); - unsafe { - if libc::tcgetattr(stdfd, t.as_mut_ptr()) != 0 - && matches!(errno::errno().0, EIO | ENOTTY) - { - libc::dup2(fd, stdfd); - } + if matches!( + tcgetattr(unsafe { BorrowedFd::borrow_raw(stdfd) }), + Err(e) if matches!(e, nix::Error::EIO | nix::Error::ENOTTY) + ) { + unsafe { libc::dup2(fd, stdfd) }; } } } @@ -986,18 +987,24 @@ fn read_ni(parser: &Parser, fd: RawFd, io: &IoChain) -> Result<(), ErrorCode> { } } -const FLOW_CONTROL_FLAGS: libc::tcflag_t = IXON | IXOFF; +const FLOW_CONTROL_FLAGS: termios::InputFlags = { + use termios::InputFlags; + InputFlags::IXON.union(InputFlags::IXOFF) +}; /// Initialize the reader. pub fn reader_init(will_restore_foreground_pgroup: bool) { - // Save the initial terminal mode. - // Note this field is read by a signal handler, so do it atomically, with a leaked mode. - let mut terminal_mode_on_startup = unsafe { std::mem::zeroed::() }; - let ret = unsafe { libc::tcgetattr(libc::STDIN_FILENO, &mut terminal_mode_on_startup) }; - // TODO: rationalize behavior if initial tcgetattr() fails. - if ret == 0 { - TERMINAL_MODE_ON_STARTUP.get_or_init(|| terminal_mode_on_startup); - } + let terminal_mode_on_startup = match tcgetattr(unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }) + { + Ok(modes) => { + // Save the initial terminal mode. + // Note this field is read by a signal handler, so do it atomically, with a leaked mode. + // TODO: rationalize behavior if initial tcgetattr() fails. + TERMINAL_MODE_ON_STARTUP.get_or_init(|| libc::termios::from(modes.clone())); + modes + } + Err(_) => zeroed_termios(), + }; if !cfg!(test) { assert!(AT_EXIT.get().is_none()); @@ -1009,12 +1016,12 @@ pub fn reader_init(will_restore_foreground_pgroup: bool) { term_fix_external_modes(&mut external_modes); // Disable flow control by default. - external_modes.c_iflag &= !FLOW_CONTROL_FLAGS; + external_modes.input_flags &= !FLOW_CONTROL_FLAGS; // Set the mode used for the terminal, initialized to the current mode. { let mut shell_modes = shell_modes(); - *shell_modes = external_modes; + *shell_modes = external_modes.clone(); term_fix_shell_modes(&mut shell_modes); } @@ -1044,7 +1051,7 @@ pub fn safe_restore_term_mode() { return; } if let Some(modes) = safe_get_terminal_mode_on_startup() { - unsafe { libc::tcsetattr(STDIN_FILENO, TCSANOW, modes) }; + unsafe { libc::tcsetattr(STDIN_FILENO, libc::TCSANOW, modes) }; } } @@ -1206,7 +1213,7 @@ pub fn reader_reading_interrupted(data: &mut ReaderData) -> i32 { /// commandline. pub fn reader_readline( parser: &Parser, - old_modes: Option, + old_modes: Option, nchars: Option, ) -> Option { let data = current_data().unwrap(); @@ -2535,7 +2542,7 @@ impl<'a> Reader<'a> { /// Return the command, or none if we were asked to cancel (e.g. SIGHUP). fn readline( &mut self, - old_modes: Option, + old_modes: Option, nchars: Option, ) -> Option { let mut tty = TtyHandoff::new(reader_save_screen_state); @@ -2633,7 +2640,12 @@ fn readline( // The order of the two conditions below is important. Try to restore the mode // in all cases, but only complain if interactive. if let Some(old_modes) = old_modes { - if unsafe { libc::tcsetattr(self.conf.inputfd, TCSANOW, &old_modes) } == -1 + if termios::tcsetattr( + unsafe { BorrowedFd::borrow_raw(self.conf.inputfd) }, + SetArg::TCSANOW, + &old_modes, + ) + .is_err() && is_interactive_session() { perror("tcsetattr"); @@ -4702,32 +4714,39 @@ fn select_completion_in_direction( // Turning off OPOST or ONLCR breaks output (staircase effect), we don't allow it. // See #7133. -fn term_fix_oflag(modes: &mut libc::termios) { - modes.c_oflag |= { +fn term_fix_oflag(modes: &mut Termios) { + modes.output_flags |= { + use termios::OutputFlags; // turn on "implementation-defined post processing" - this often changes how line breaks work. - OPOST + OutputFlags::OPOST // "translate newline to carriage return-newline" - without you see staircase output. - | ONLCR + | OutputFlags::ONLCR }; } /// Restore terminal settings we care about, to prevent a broken shell. -fn term_fix_shell_modes(modes: &mut libc::termios) { - modes.c_iflag &= { +fn term_fix_shell_modes(modes: &mut Termios) { + modes.input_flags &= { + use termios::InputFlags; // disable mapping CR (\cM) to NL (\cJ) - !ICRNL + !InputFlags::ICRNL // disable mapping NL (\cJ) to CR (\cM) - & !INLCR + & !InputFlags::INLCR }; - modes.c_lflag &= { - !ECHO - & !ICANON - & !IEXTEN // turn off handling of discard and lnext characters - & !FLUSHO + modes.local_flags &= { + use termios::LocalFlags; + let echo = LocalFlags::ECHO; + let flusho = LocalFlags::FLUSHO; + let icanon = LocalFlags::ICANON; + let iexten = LocalFlags::IEXTEN; + !echo + & !icanon + & !iexten // turn off handling of discard and lnext characters + & !flusho }; term_fix_oflag(modes); - let c_cc = &mut modes.c_cc; + let c_cc = &mut modes.control_chars; c_cc[VMIN] = 1; c_cc[VTIME] = 0; @@ -4741,57 +4760,76 @@ fn term_fix_shell_modes(modes: &mut libc::termios) { c_cc[VQUIT] = disabling_char; } -fn term_fix_external_modes(modes: &mut libc::termios) { +fn term_fix_external_modes(modes: &mut Termios) { term_fix_oflag(modes); // These cause other ridiculous behaviors like input not being shown. - modes.c_lflag = (modes.c_lflag | ECHO | ICANON | IEXTEN) & !FLUSHO; - modes.c_iflag = (modes.c_iflag | ICRNL) & !INLCR; + modes.local_flags = { + use termios::LocalFlags; + let echo = LocalFlags::ECHO; + let flusho = LocalFlags::FLUSHO; + let icanon = LocalFlags::ICANON; + let iexten = LocalFlags::IEXTEN; + (modes.local_flags | echo | icanon | iexten) & !flusho + }; + modes.input_flags = { + use termios::InputFlags; + let icrnl = InputFlags::ICRNL; + let inlcr = InputFlags::INLCR; + (modes.input_flags | icrnl) & !inlcr + }; } /// Give up control of terminal. fn term_donate(quiet: bool /* = false */) { - while unsafe { - libc::tcsetattr( - STDIN_FILENO, - TCSANOW, - &*TTY_MODES_FOR_EXTERNAL_CMDS.lock().unwrap(), - ) - } == -1 - { - if errno().0 != EINTR { - if !quiet { - flog!( - warning, - wgettext!("Could not set terminal mode for new job") - ); - perror("tcsetattr"); + loop { + match termios::tcsetattr( + unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }, + SetArg::TCSANOW, + &TTY_MODES_FOR_EXTERNAL_CMDS.lock().unwrap(), + ) { + Ok(_) => (), + Err(nix::Error::EINTR) => continue, + Err(_) => { + if !quiet { + flog!( + warning, + wgettext!("Could not set terminal mode for new job") + ); + perror("tcsetattr"); + } + break; } - break; } + break; } } /// Copy the (potentially changed) terminal modes and use them from now on. pub fn term_copy_modes() { - let mut modes = MaybeUninit::uninit(); - unsafe { libc::tcgetattr(STDIN_FILENO, modes.as_mut_ptr()) }; - let mut external_modes = unsafe { modes.assume_init() }; + let mut external_modes = tcgetattr(unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }) + .unwrap_or_else(|_| zeroed_termios()); // We still want to fix most egregious breakage. // E.g. OPOST is *not* something that should be set globally, // and 99% triggered by a crashed program. term_fix_external_modes(&mut external_modes); + let external_flow_control = external_modes.input_flags & FLOW_CONTROL_FLAGS; *TTY_MODES_FOR_EXTERNAL_CMDS.lock().unwrap() = external_modes; let mut shell_modes = shell_modes(); - shell_modes.c_iflag = - (shell_modes.c_iflag & !FLOW_CONTROL_FLAGS) | (external_modes.c_iflag & FLOW_CONTROL_FLAGS); + shell_modes.input_flags = + (shell_modes.input_flags & !FLOW_CONTROL_FLAGS) | external_flow_control; } pub fn set_shell_modes(fd: RawFd, whence: &str) -> bool { let ok = loop { - let ok = unsafe { libc::tcsetattr(fd, TCSANOW, &*shell_modes()) } != -1; - if ok || errno().0 != EINTR { - break ok; + match termios::tcsetattr( + unsafe { BorrowedFd::borrow_raw(fd) }, + SetArg::TCSANOW, + &shell_modes(), + ) { + Ok(_) => break true, + Err(nix::Error::EINTR) => continue, + Err(_) => break false, } }; if !ok { @@ -4804,18 +4842,14 @@ pub fn set_shell_modes(fd: RawFd, whence: &str) -> bool { ok } -pub fn set_shell_modes_temporarily(inputfd: RawFd) -> Option { +pub fn set_shell_modes_temporarily(inputfd: RawFd) -> Option { // It may happen that a command we ran when job control was disabled nevertheless stole the tty // from us. In that case when we read from our fd, it will trigger SIGTTIN. So just // unconditionally reclaim the tty. See #9181. unsafe { libc::tcsetpgrp(inputfd, libc::getpgrp()) }; // Get the current terminal modes. These will be restored when the function returns. - let old_modes = { - let mut old_modes = MaybeUninit::uninit(); - let ok = unsafe { libc::tcgetattr(inputfd, old_modes.as_mut_ptr()) } == 0; - ok.then(|| unsafe { old_modes.assume_init() }) - }; + let old_modes = tcgetattr(unsafe { BorrowedFd::borrow_raw(inputfd) }).ok(); // Set the new modes. set_shell_modes(inputfd, "readline"); diff --git a/src/screen.rs b/src/screen.rs index 23bb43872..ddc400967 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -19,7 +19,8 @@ use std::sync::atomic::AtomicU32; use std::time::SystemTime; -use libc::{ONLCR, STDERR_FILENO, STDOUT_FILENO}; +use libc::{STDERR_FILENO, STDOUT_FILENO}; +use nix::sys::termios; use crate::common::{ get_ellipsis_char, get_omitted_newline_str, has_working_tty_timestamps, shell_modes, wcs2bytes, @@ -929,7 +930,9 @@ fn do_move(&mut self, new_x: usize, new_y: usize) { let s = if y_steps < 0 { Some(CursorUp) } else if y_steps > 0 { - if (shell_modes().c_oflag & ONLCR) != 0 + if shell_modes() + .output_flags + .contains(termios::OutputFlags::ONLCR) && (!use_terminfo() || crate::terminal::term() .cursor_down diff --git a/src/tty_handoff.rs b/src/tty_handoff.rs index f17195e80..95afd8a26 100644 --- a/src/tty_handoff.rs +++ b/src/tty_handoff.rs @@ -20,8 +20,9 @@ use crate::wutil::{perror, wcstoi}; use fish_widestring::ToWString; use libc::{EINVAL, ENOTTY, EPERM, STDIN_FILENO, WNOHANG}; +use nix::sys::termios::tcgetattr; use nix::unistd::getpgrp; -use std::mem::MaybeUninit; +use std::os::fd::BorrowedFd; use std::sync::{ OnceLock, atomic::{AtomicBool, AtomicPtr, Ordering}, @@ -408,12 +409,17 @@ pub fn to_job_group(&mut self, jg: &JobGroupRef) { /// Save the current tty modes into the owning job group, if we are transferred. pub fn save_tty_modes(&mut self) { - if let Some(ref mut owner) = self.owner { - let mut tmodes = MaybeUninit::uninit(); - if unsafe { libc::tcgetattr(STDIN_FILENO, tmodes.as_mut_ptr()) } == 0 { - owner.tmodes.replace(Some(unsafe { tmodes.assume_init() })); - } else if errno::errno().0 != ENOTTY { - perror("tcgetattr"); + let Some(ref mut owner) = self.owner else { + return; + }; + match tcgetattr(unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }) { + Ok(modes) => { + owner.tmodes.replace(Some(modes)); + } + Err(err) => { + if err != nix::Error::ENOTTY { + perror("tcgetattr"); + } } } }