diff --git a/fish-rust/src/ffi.rs b/fish-rust/src/ffi.rs index 0988de7ea..4601d260b 100644 --- a/fish-rust/src/ffi.rs +++ b/fish-rust/src/ffi.rs @@ -6,10 +6,12 @@ use ::std::fmt::{self, Debug, Formatter}; use ::std::pin::Pin; #[rustfmt::skip] use ::std::slice; +use crate::env::flags::EnvMode; pub use crate::wait_handle::{ WaitHandleRef, WaitHandleRefFFI, WaitHandleStore, WaitHandleStoreFFI, }; -use crate::wchar::wstr; +use crate::wchar::{wstr, WString}; +use crate::wchar_ffi::WCharFromFFI; use autocxx::prelude::*; use cxx::SharedPtr; use libc::pid_t; @@ -47,7 +49,9 @@ include_cpp! { generate!("wperror") generate_pod!("pipes_ffi_t") + generate!("environment_t") generate!("env_stack_t") + generate!("env_var_t") generate!("make_pipes_ffi") generate!("valid_var_name_char") @@ -148,6 +152,66 @@ impl parser_t { let job = self.ffi_job_get_from_pid(pid.into()); unsafe { job.as_ref() } } + + /// Helper to get a variable as a string, using the default flags. + pub fn var_as_string(&mut self, name: &wstr) -> Option<WString> { + self.pin().vars().unpin().get_as_string(name) + } + + pub fn get_var_stack(&mut self) -> &mut env_stack_t { + self.pin().vars().unpin() + } + + pub fn get_var_stack_env(&mut self) -> &environment_t { + self.vars_env_ffi() + } + + pub fn set_var(&mut self, name: &wstr, value: &[&wstr], flags: EnvMode) -> libc::c_int { + self.get_var_stack().set_var(name, value, flags) + } +} + +impl environment_t { + /// Helper to get a variable as a string, using the default flags. + pub fn get_as_string(&self, name: &wstr) -> Option<WString> { + self.get_as_string_flags(name, EnvMode::DEFAULT) + } + + /// Helper to get a variable as a string, using the given flags. + pub fn get_as_string_flags(&self, name: &wstr, flags: EnvMode) -> Option<WString> { + self.get_or_null(&name.to_ffi(), flags.bits()) + .as_ref() + .map(|s| s.as_string().from_ffi()) + } +} + +impl env_stack_t { + /// Helper to get a variable as a string, using the default flags. + pub fn get_as_string(&self, name: &wstr) -> Option<WString> { + self.get_as_string_flags(name, EnvMode::DEFAULT) + } + + /// Helper to get a variable as a string, using the given flags. + pub fn get_as_string_flags(&self, name: &wstr, flags: EnvMode) -> Option<WString> { + self.get_or_null(&name.to_ffi(), flags.bits()) + .as_ref() + .map(|s| s.as_string().from_ffi()) + } + + /// Helper to set a value. + pub fn set_var(&mut self, name: &wstr, value: &[&wstr], flags: EnvMode) -> libc::c_int { + use crate::wchar_ffi::{wstr_to_u32string, W0String}; + let strings: Vec<W0String> = value.iter().map(wstr_to_u32string).collect(); + let ptrs: Vec<*const u32> = strings.iter().map(|s| s.as_ptr()).collect(); + self.pin() + .set_ffi( + &name.to_ffi(), + flags.bits(), + ptrs.as_ptr() as *const c_void, + ptrs.len(), + ) + .into() + } } pub fn try_compile(anchored: &wstr, flags: &re::flags_t) -> Pin<Box<re::regex_result_ffi>> { diff --git a/fish-rust/src/lib.rs b/fish-rust/src/lib.rs index a90b33f92..59c8990c8 100644 --- a/fish-rust/src/lib.rs +++ b/fish-rust/src/lib.rs @@ -30,6 +30,7 @@ mod parse_constants; mod redirection; mod signal; mod smoke; +mod termsize; mod threads; mod timer; mod tokenizer; diff --git a/fish-rust/src/termsize.rs b/fish-rust/src/termsize.rs new file mode 100644 index 000000000..45f1e69c6 --- /dev/null +++ b/fish-rust/src/termsize.rs @@ -0,0 +1,338 @@ +// Support for exposing the terminal size. +use crate::common::assert_sync; +use crate::env::flags::EnvMode; +use crate::ffi::{environment_t, parser_t, Repin}; +use crate::flog::FLOG; +use crate::wchar::{WString, L}; +use crate::wchar_ext::ToWString; +use crate::wchar_ffi::WCharToFFI; +use crate::wutil::fish_wcstoi; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::sync::Mutex; + +// A counter which is incremented every SIGWINCH, or when the tty is otherwise invalidated. +static TTY_TERMSIZE_GEN_COUNT: AtomicU32 = AtomicU32::new(0); + +/// Convert an environment variable to an int, or return a default value. +/// The int must be >0 and <USHRT_MAX (from struct winsize). +fn var_to_int_or(var: Option<WString>, default: isize) -> isize { + match var { + Some(s) => { + let proposed = fish_wcstoi(s.chars()); + if let Ok(proposed) = proposed { + proposed + } else { + default + } + } + None => default, + } +} + +/// \return a termsize from ioctl, or None on error or if not supported. +fn read_termsize_from_tty() -> Option<Termsize> { + let mut ret: Option<Termsize> = None; + // Note: historically we've supported libc::winsize not existing. + let mut winsize: libc::winsize = unsafe { std::mem::zeroed() }; + if unsafe { libc::ioctl(0, libc::TIOCGWINSZ, &mut winsize as *mut libc::winsize) } >= 0 { + // 0 values are unusable, fall back to the default instead. + if winsize.ws_col == 0 { + FLOG!( + term_support, + L!("Terminal has 0 columns, falling back to default width") + ); + winsize.ws_col = Termsize::DEFAULT_WIDTH as u16; + } + if winsize.ws_row == 0 { + FLOG!( + term_support, + L!("Terminal has 0 rows, falling back to default height") + ); + winsize.ws_row = Termsize::DEFAULT_HEIGHT as u16; + } + ret = Some(Termsize::new( + winsize.ws_col as isize, + winsize.ws_row as isize, + )); + } + ret +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Termsize { + /// Width of the terminal, in columns. + pub width: isize, + + /// Height of the terminal, in rows. + pub height: isize, +} + +impl Termsize { + /// Default width and height. + pub const DEFAULT_WIDTH: isize = 80; + pub const DEFAULT_HEIGHT: isize = 24; + + /// Construct from width and height. + pub fn new(width: isize, height: isize) -> Self { + Self { width, height } + } + + /// Return a default-sized termsize. + pub fn defaults() -> Self { + Self::new(Self::DEFAULT_WIDTH, Self::DEFAULT_HEIGHT) + } +} + +struct TermsizeData { + // The last termsize returned by TIOCGWINSZ, or none if none. + last_from_tty: Option<Termsize>, + // The last termsize seen from the environment (COLUMNS/LINES), or none if none. + last_from_env: Option<Termsize>, + // The last-seen tty-invalidation generation count. + // Set to a huge value so it's initially stale. + last_tty_gen_count: u32, +} + +impl TermsizeData { + const fn defaults() -> Self { + Self { + last_from_tty: None, + last_from_env: None, + last_tty_gen_count: u32::max_value(), + } + } + + /// \return the current termsize from this data. + fn current(&self) -> Termsize { + // This encapsulates our ordering logic. If we have a termsize from a tty, use it; otherwise use + // what we have seen from the environment. + if let Some(ts) = self.last_from_tty { + ts + } else if let Some(ts) = self.last_from_env { + ts + } else { + Termsize::defaults() + } + } + + /// Mark that our termsize is (for the time being) from the environment, not the tty. + fn mark_override_from_env(&mut self, ts: Termsize) { + self.last_from_env = Some(ts); + self.last_from_tty = None; + self.last_tty_gen_count = TTY_TERMSIZE_GEN_COUNT.load(Ordering::Relaxed); + } +} + +/// Termsize monitoring is more complicated than one may think. +/// The main source of complexity is the interaction between the environment variables COLUMNS/ROWS, +/// the WINCH signal, and the TIOCGWINSZ ioctl. +/// Our policy is "last seen wins": if COLUMNS or LINES is modified, we respect that until we get a +/// SIGWINCH. +pub struct TermsizeContainer { + // Our lock-protected data. + data: Mutex<TermsizeData>, + + // An indication that we are currently in the process of setting COLUMNS and LINES, and so do + // not react to any changes. + setting_env_vars: AtomicBool, + + /// A function used for accessing the termsize from the tty. This is only exposed for testing. + tty_size_reader: fn() -> Option<Termsize>, +} + +impl TermsizeContainer { + /// \return the termsize without applying any updates. + /// Return the default termsize if none. + pub fn last(&self) -> Termsize { + self.data.lock().unwrap().current() + } + + /// Initialize our termsize, using the given environment stack. + /// This will prefer to use COLUMNS and LINES, but will fall back to the tty size reader. + /// This does not change any variables in the environment. + pub fn initialize(&mut self, vars: &environment_t) -> Termsize { + let new_termsize = Termsize { + width: var_to_int_or(vars.get_as_string_flags(L!("COLUMNS"), EnvMode::GLOBAL), -1), + height: var_to_int_or(vars.get_as_string_flags(L!("LINES"), EnvMode::GLOBAL), -1), + }; + + let mut data = self.data.lock().unwrap(); + if new_termsize.width > 0 && new_termsize.height > 0 { + data.mark_override_from_env(new_termsize); + } else { + data.last_tty_gen_count = TTY_TERMSIZE_GEN_COUNT.load(Ordering::Relaxed); + data.last_from_tty = (self.tty_size_reader)(); + } + data.current() + } + + /// If our termsize is stale, update it, using \p parser firing any events that may be + /// registered for COLUMNS and LINES. + /// \return the updated termsize. + pub fn updating(&mut self, parser: &mut parser_t) -> Termsize { + let new_size; + let prev_size; + + // Take the lock in a local region. + // Capture the size before and the new size. + { + let mut data = self.data.lock().unwrap(); + prev_size = data.current(); + + // Critical read of signal-owned variable. + // This must happen before the TIOCGWINSZ ioctl. + let tty_gen_count: u32 = TTY_TERMSIZE_GEN_COUNT.load(Ordering::Relaxed); + if data.last_tty_gen_count != tty_gen_count { + // Our idea of the size of the terminal may be stale. + // Apply any updates. + data.last_tty_gen_count = tty_gen_count; + data.last_from_tty = (self.tty_size_reader)(); + } + new_size = data.current(); + } + + // Announce any updates. + if new_size != prev_size { + self.set_columns_lines_vars(new_size, parser); + } + new_size + } + + fn set_columns_lines_vars(&mut self, val: Termsize, parser: &mut parser_t) { + let saved = self.setting_env_vars.swap(true, Ordering::Relaxed); + parser.pin().set_var_and_fire( + &L!("COLUMNS").to_ffi(), + EnvMode::GLOBAL.bits(), + val.width.to_wstring().to_ffi(), + ); + parser.pin().set_var_and_fire( + &L!("LINES").to_ffi(), + EnvMode::GLOBAL.bits(), + val.height.to_wstring().to_ffi(), + ); + self.setting_env_vars.store(saved, Ordering::Relaxed); + } + + /// Note that COLUMNS and/or LINES global variables changed. + fn handle_columns_lines_var_change(&self, vars: &environment_t) { + // Do nothing if we are the ones setting it. + if self.setting_env_vars.load(Ordering::Relaxed) { + return; + } + // Construct a new termsize from COLUMNS and LINES, then set it in our data. + let new_termsize = Termsize { + width: var_to_int_or( + vars.get_as_string_flags(L!("COLUMNS"), EnvMode::GLOBAL), + Termsize::DEFAULT_WIDTH, + ), + height: var_to_int_or( + vars.get_as_string_flags(L!("LINES"), EnvMode::GLOBAL), + Termsize::DEFAULT_HEIGHT, + ), + }; + + // Store our termsize as an environment override. + self.data + .lock() + .unwrap() + .mark_override_from_env(new_termsize); + } + + /// Note that a WINCH signal is received. + /// Naturally this may be called from within a signal handler. + pub fn handle_winch() { + TTY_TERMSIZE_GEN_COUNT.fetch_add(1, Ordering::Relaxed); + } + + pub fn invalidate_tty() { + TTY_TERMSIZE_GEN_COUNT.fetch_add(1, Ordering::Relaxed); + } +} + +static SHARED_CONTAINER: TermsizeContainer = TermsizeContainer { + data: Mutex::new(TermsizeData::defaults()), + setting_env_vars: AtomicBool::new(false), + tty_size_reader: read_termsize_from_tty, +}; + +const _: () = assert_sync::<TermsizeContainer>(); + +/// Convenience helper to return the last known termsize. +pub fn termsize_last() -> Termsize { + return SHARED_CONTAINER.last(); +} + +use crate::ffi_tests::add_test; +add_test!("test_termsize", || { + let env_global = EnvMode::GLOBAL; + let parser: &mut parser_t = unsafe { &mut *parser_t::principal_parser_ffi() }; + + // Use a static variable so we can pretend we're the kernel exposing a terminal size. + static STUBBY_TERMSIZE: Mutex<Option<Termsize>> = Mutex::new(None); + fn stubby_termsize() -> Option<Termsize> { + *STUBBY_TERMSIZE.lock().unwrap() + } + let mut ts = TermsizeContainer { + data: Mutex::new(TermsizeData::defaults()), + setting_env_vars: AtomicBool::new(false), + tty_size_reader: stubby_termsize, + }; + + // Initially default value. + assert_eq!(ts.last(), Termsize::defaults()); + + // Haha we change the value, it doesn't even know. + *STUBBY_TERMSIZE.lock().unwrap() = Some(Termsize { + width: 42, + height: 84, + }); + assert_eq!(ts.last(), Termsize::defaults()); + + // Ok let's tell it. But it still doesn't update right away. + TermsizeContainer::handle_winch(); + assert_eq!(ts.last(), Termsize::defaults()); + + // Ok now we tell it to update. + ts.updating(parser); + assert_eq!(ts.last(), Termsize::new(42, 84)); + assert_eq!(parser.var_as_string(L!("COLUMNS")).unwrap(), "42"); + assert_eq!(parser.var_as_string(L!("LINES")).unwrap(), "84"); + + // Wow someone set COLUMNS and LINES to a weird value. + // Now the tty's termsize doesn't matter. + parser.set_var(L!("COLUMNS"), &[L!("75")], env_global); + parser.set_var(L!("LINES"), &[L!("150")], env_global); + ts.handle_columns_lines_var_change(parser.get_var_stack_env()); + assert_eq!(ts.last(), Termsize::new(75, 150)); + assert_eq!(parser.var_as_string(L!("COLUMNS")).unwrap(), "75"); + assert_eq!(parser.var_as_string(L!("LINES")).unwrap(), "150"); + + parser.set_var(L!("COLUMNS"), &[L!("33")], env_global); + ts.handle_columns_lines_var_change(parser.get_var_stack_env()); + assert_eq!(ts.last(), Termsize::new(33, 150)); + + // Oh it got SIGWINCH, now the tty matters again. + TermsizeContainer::handle_winch(); + assert_eq!(ts.last(), Termsize::new(33, 150)); + assert_eq!(ts.updating(parser), stubby_termsize().unwrap()); + assert_eq!(parser.var_as_string(L!("COLUMNS")).unwrap(), "42"); + assert_eq!(parser.var_as_string(L!("LINES")).unwrap(), "84"); + + // Test initialize(). + parser.set_var(L!("COLUMNS"), &[L!("83")], env_global); + parser.set_var(L!("LINES"), &[L!("38")], env_global); + ts.initialize(parser.get_var_stack_env()); + assert_eq!(ts.last(), Termsize::new(83, 38)); + + // initialize() even beats the tty reader until a sigwinch. + let mut ts2 = TermsizeContainer { + data: Mutex::new(TermsizeData::defaults()), + setting_env_vars: AtomicBool::new(false), + tty_size_reader: stubby_termsize, + }; + ts.initialize(parser.get_var_stack_env()); + ts2.updating(parser); + assert_eq!(ts.last(), Termsize::new(83, 38)); + TermsizeContainer::handle_winch(); + assert_eq!(ts2.updating(parser), stubby_termsize().unwrap()); +}); diff --git a/fish-rust/src/wchar_ext.rs b/fish-rust/src/wchar_ext.rs index 707a3da81..ccf67d1fc 100644 --- a/fish-rust/src/wchar_ext.rs +++ b/fish-rust/src/wchar_ext.rs @@ -1,6 +1,89 @@ use crate::wchar::{wstr, WString}; use widestring::utfstr::CharsUtf32; +/// Helpers to convert things to widestring. +/// This is like std::string::ToString. +pub trait ToWString { + fn to_wstring(&self) -> WString; +} + +#[inline] +fn to_wstring_impl(mut val: u64, neg: bool) -> WString { + // 20 digits max in u64: 18446744073709551616. + let mut digits = [0; 24]; + let mut ndigits = 0; + while val > 0 { + digits[ndigits] = (val % 10) as u8; + val /= 10; + ndigits += 1; + } + if ndigits == 0 { + digits[0] = 0; + ndigits = 1; + } + let mut result = WString::with_capacity(ndigits + neg as usize); + if neg { + result.push('-'); + } + for i in (0..ndigits).rev() { + result.push((digits[i] + b'0') as char); + } + result +} + +/// Implement to_wstring() for signed types. +macro_rules! impl_to_wstring_signed { + ($t:ty) => { + impl ToWString for $t { + fn to_wstring(&self) -> WString { + let val = *self as i64; + to_wstring_impl(val.unsigned_abs(), val < 0) + } + } + }; +} +impl_to_wstring_signed!(i8); +impl_to_wstring_signed!(i16); +impl_to_wstring_signed!(i32); +impl_to_wstring_signed!(i64); +impl_to_wstring_signed!(isize); + +/// Implement to_wstring() for unsigned types. +macro_rules! impl_to_wstring_unsigned { + ($t:ty) => { + impl ToWString for $t { + fn to_wstring(&self) -> WString { + to_wstring_impl(*self as u64, false) + } + } + }; +} + +impl_to_wstring_unsigned!(u8); +impl_to_wstring_unsigned!(u16); +impl_to_wstring_unsigned!(u32); +impl_to_wstring_unsigned!(u64); +impl_to_wstring_unsigned!(usize); + +#[test] +fn test_to_wstring() { + assert_eq!(0_u64.to_wstring(), "0"); + assert_eq!(1_u64.to_wstring(), "1"); + assert_eq!(0_i64.to_wstring(), "0"); + assert_eq!(1_i64.to_wstring(), "1"); + assert_eq!((-1_i64).to_wstring(), "-1"); + assert_eq!((-5_i64).to_wstring(), "-5"); + let mut val: i64 = 1; + loop { + assert_eq!(val.to_wstring(), val.to_string()); + let Some(next) = val.checked_mul(-3) else { break; }; + val = next; + } + assert_eq!(u64::MAX.to_wstring(), "18446744073709551615"); + assert_eq!(i64::MIN.to_wstring(), "-9223372036854775808"); + assert_eq!(i64::MAX.to_wstring(), "9223372036854775807"); +} + /// A thing that a wide string can start with or end with. /// It must have a chars() method which returns a double-ended char iterator. pub trait CharPrefixSuffix { diff --git a/src/env.cpp b/src/env.cpp index 6d05e653c..f8536d4d0 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -1414,6 +1414,12 @@ int env_stack_t::set(const wcstring &key, env_mode_flags_t mode, wcstring_list_t return ret.status; } +int env_stack_t::set_ffi(const wcstring &key, env_mode_flags_t mode, const void *vals, + size_t count) { + const wchar_t *const *ptr = static_cast<const wchar_t *const *>(vals); + return this->set(key, mode, wcstring_list_t(ptr, ptr + count)); +} + int env_stack_t::set_one(const wcstring &key, env_mode_flags_t mode, wcstring val) { wcstring_list_t vals; vals.push_back(std::move(val)); diff --git a/src/env.h b/src/env.h index b3901a4e4..9cf6fba69 100644 --- a/src/env.h +++ b/src/env.h @@ -242,6 +242,10 @@ class env_stack_t final : public environment_t { /// Sets the variable with the specified name to the given values. int set(const wcstring &key, env_mode_flags_t mode, wcstring_list_t vals); + /// Sets the variable with the specified name to the given values. + /// The values should have type const wchar_t *const * (but autocxx doesn't support that). + int set_ffi(const wcstring &key, env_mode_flags_t mode, const void *vals, size_t count); + /// Sets the variable with the specified name to a single value. int set_one(const wcstring &key, env_mode_flags_t mode, wcstring val); diff --git a/src/parser.cpp b/src/parser.cpp index c5f2bebd3..89a18fdf2 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -62,6 +62,8 @@ parser_t &parser_t::principal_parser() { return *principal; } +parser_t *parser_t::principal_parser_ffi() { return &principal_parser(); } + void parser_t::assert_can_execute() const { ASSERT_IS_MAIN_THREAD(); } rust::Box<WaitHandleStoreFFI> &parser_t::get_wait_handles_ffi() { return wait_handles; } diff --git a/src/parser.h b/src/parser.h index 07f36820c..661e3c54f 100644 --- a/src/parser.h +++ b/src/parser.h @@ -315,6 +315,9 @@ class parser_t : public std::enable_shared_from_this<parser_t> { /// Get the "principal" parser, whatever that is. static parser_t &principal_parser(); + /// ffi helper. Obviously this is totally bogus. + static parser_t *principal_parser_ffi(); + /// Assert that this parser is allowed to execute on the current thread. void assert_can_execute() const; @@ -388,6 +391,9 @@ class parser_t : public std::enable_shared_from_this<parser_t> { env_stack_t &vars() { return *variables; } const env_stack_t &vars() const { return *variables; } + /// Rust helper - variables as an environment_t. + const environment_t &vars_env_ffi() const { return *variables; } + int remove_var_ffi(const wcstring &key, int mode) { return vars().remove(key, mode); } /// Get the library data.