From fe440284bfad313998f73db663e34003d89d7ec8 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Sun, 16 Jun 2024 05:56:13 -0700 Subject: [PATCH] Use rustix instead of libc (additive only approach) (#892) * use rustix instead of libc * make rustix the default feature * bump msrv to 1.63.0 * fix remaining libc issues - use rustix version of sigwinch signal - add a lifetime to FileDesc and replace FileDesc::Static to FileDesc::Borrowed. This made it necessary to either add a lifetime to the libc version of FileDesc or replace all the callers with multiple paths (libc, rustix). Changing FileDesc was more straightforward. There are no usages of FileDesc found in any repo on github, so this change should be reasonably safe. * add changelog entry for rustix / filedesc change --- CHANGELOG.md | 5 ++ Cargo.toml | 11 +++- src/event/source/unix/mio.rs | 4 +- src/event/source/unix/tty.rs | 14 ++++- src/terminal/sys/file_descriptor.rs | 85 +++++++++++++++++++++++---- src/terminal/sys/unix.rs | 91 ++++++++++++++++++++++++++--- src/tty.rs | 10 +++- 7 files changed, 195 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c7f661c9..41e438b90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# Unreleased + +- Use Rustix by default instead of libc. Libc can be re-enabled if necessary with the libc feature flag. +- `FileDesc` now requires a lifetime annotation. + # Version 0.27.1 ## Added ⭐ diff --git a/Cargo.toml b/Cargo.toml index 2c53c14be..cee169750 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["event", "color", "cli", "input", "terminal"] exclude = ["target", "Cargo.lock"] readme = "README.md" edition = "2021" -rust-version = "1.58.0" +rust-version = "1.63.0" categories = ["command-line-interface", "command-line-utilities"] [lib] @@ -71,7 +71,14 @@ crossterm_winapi = { version = "0.9.1", optional = true } # UNIX dependencies # [target.'cfg(unix)'.dependencies] -libc = "0.2" +# Default to using rustix for UNIX systems, but provide an option to use libc for backwards +# compatibility. +libc = { version = "0.2", default-features = false, optional = true } +rustix = { version = "0.38.34", default-features = false, features = [ + "std", + "stdio", + "termios", +] } signal-hook = { version = "0.3.17", optional = true } filedescriptor = { version = "0.8", optional = true } mio = { version = "0.8", features = ["os-poll"], optional = true } diff --git a/src/event/source/unix/mio.rs b/src/event/source/unix/mio.rs index 8372a1ddb..0368e61b6 100644 --- a/src/event/source/unix/mio.rs +++ b/src/event/source/unix/mio.rs @@ -26,7 +26,7 @@ pub(crate) struct UnixInternalEventSource { events: Events, parser: Parser, tty_buffer: [u8; TTY_BUFFER_SIZE], - tty_fd: FileDesc, + tty_fd: FileDesc<'static>, signals: Signals, #[cfg(feature = "event-stream")] waker: Waker, @@ -37,7 +37,7 @@ impl UnixInternalEventSource { UnixInternalEventSource::from_file_descriptor(tty_fd()?) } - pub(crate) fn from_file_descriptor(input_fd: FileDesc) -> io::Result { + pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result { let poll = Poll::new()?; let registry = poll.registry(); diff --git a/src/event/source/unix/tty.rs b/src/event/source/unix/tty.rs index b8cbfeffd..03d76b401 100644 --- a/src/event/source/unix/tty.rs +++ b/src/event/source/unix/tty.rs @@ -1,6 +1,10 @@ +#[cfg(feature = "libc")] use std::os::unix::prelude::AsRawFd; use std::{collections::VecDeque, io, os::unix::net::UnixStream, time::Duration}; +#[cfg(not(feature = "libc"))] +use rustix::fd::{AsFd, AsRawFd}; + use signal_hook::low_level::pipe; use crate::event::timeout::PollTimeout; @@ -38,7 +42,7 @@ const TTY_BUFFER_SIZE: usize = 1_024; pub(crate) struct UnixInternalEventSource { parser: Parser, tty_buffer: [u8; TTY_BUFFER_SIZE], - tty: FileDesc, + tty: FileDesc<'static>, winch_signal_receiver: UnixStream, #[cfg(feature = "event-stream")] wake_pipe: WakePipe, @@ -56,7 +60,7 @@ impl UnixInternalEventSource { UnixInternalEventSource::from_file_descriptor(tty_fd()?) } - pub(crate) fn from_file_descriptor(input_fd: FileDesc) -> io::Result { + pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result { Ok(UnixInternalEventSource { parser: Parser::default(), tty_buffer: [0u8; TTY_BUFFER_SIZE], @@ -64,7 +68,10 @@ impl UnixInternalEventSource { winch_signal_receiver: { let (receiver, sender) = nonblocking_unix_pair()?; // Unregistering is unnecessary because EventSource is a singleton + #[cfg(feature = "libc")] pipe::register(libc::SIGWINCH, sender)?; + #[cfg(not(feature = "libc"))] + pipe::register(rustix::process::Signal::Winch as i32, sender)?; receiver }, #[cfg(feature = "event-stream")] @@ -157,7 +164,10 @@ impl EventSource for UnixInternalEventSource { } } if fds[1].revents & POLLIN != 0 { + #[cfg(feature = "libc")] let fd = FileDesc::new(self.winch_signal_receiver.as_raw_fd(), false); + #[cfg(not(feature = "libc"))] + let fd = FileDesc::Borrowed(self.winch_signal_receiver.as_fd()); // drain the pipe while read_complete(&fd, &mut [0; 1024])? != 0 {} // TODO Should we remove tput? diff --git a/src/terminal/sys/file_descriptor.rs b/src/terminal/sys/file_descriptor.rs index 81c3fb2e3..baff266c6 100644 --- a/src/terminal/sys/file_descriptor.rs +++ b/src/terminal/sys/file_descriptor.rs @@ -1,32 +1,51 @@ +use std::io; + +#[cfg(feature = "libc")] +use libc::size_t; +#[cfg(not(feature = "libc"))] +use rustix::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd}; +#[cfg(feature = "libc")] use std::{ - fs, io, + fs, + marker::PhantomData, os::unix::{ io::{IntoRawFd, RawFd}, prelude::AsRawFd, }, }; -use libc::size_t; - /// A file descriptor wrapper. /// /// It allows to retrieve raw file descriptor, write to the file descriptor and /// mainly it closes the file descriptor once dropped. #[derive(Debug)] -pub struct FileDesc { +#[cfg(feature = "libc")] +pub struct FileDesc<'a> { fd: RawFd, close_on_drop: bool, + phantom: PhantomData<&'a ()>, } -impl FileDesc { +#[cfg(not(feature = "libc"))] +pub enum FileDesc<'a> { + Owned(OwnedFd), + Borrowed(BorrowedFd<'a>), +} + +#[cfg(feature = "libc")] +impl FileDesc<'_> { /// Constructs a new `FileDesc` with the given `RawFd`. /// /// # Arguments /// /// * `fd` - raw file descriptor /// * `close_on_drop` - specify if the raw file descriptor should be closed once the `FileDesc` is dropped - pub fn new(fd: RawFd, close_on_drop: bool) -> FileDesc { - FileDesc { fd, close_on_drop } + pub fn new(fd: RawFd, close_on_drop: bool) -> FileDesc<'static> { + FileDesc { + fd, + close_on_drop, + phantom: PhantomData, + } } pub fn read(&self, buffer: &mut [u8]) -> io::Result { @@ -51,7 +70,27 @@ impl FileDesc { } } -impl Drop for FileDesc { +#[cfg(not(feature = "libc"))] +impl FileDesc<'_> { + pub fn read(&self, buffer: &mut [u8]) -> io::Result { + let fd = match self { + FileDesc::Owned(fd) => fd.as_fd(), + FileDesc::Borrowed(fd) => fd.as_fd(), + }; + let result = rustix::io::read(fd, buffer)?; + Ok(result) + } + + pub fn raw_fd(&self) -> RawFd { + match self { + FileDesc::Owned(fd) => fd.as_raw_fd(), + FileDesc::Borrowed(fd) => fd.as_raw_fd(), + } + } +} + +#[cfg(feature = "libc")] +impl Drop for FileDesc<'_> { fn drop(&mut self) { if self.close_on_drop { // Note that errors are ignored when closing a file descriptor. The @@ -64,14 +103,25 @@ impl Drop for FileDesc { } } -impl AsRawFd for FileDesc { +impl AsRawFd for FileDesc<'_> { fn as_raw_fd(&self) -> RawFd { self.raw_fd() } } +#[cfg(not(feature = "libc"))] +impl AsFd for FileDesc<'_> { + fn as_fd(&self) -> BorrowedFd<'_> { + match self { + FileDesc::Owned(fd) => fd.as_fd(), + FileDesc::Borrowed(fd) => fd.as_fd(), + } + } +} + +#[cfg(feature = "libc")] /// Creates a file descriptor pointing to the standard input or `/dev/tty`. -pub fn tty_fd() -> io::Result { +pub fn tty_fd() -> io::Result> { let (fd, close_on_drop) = if unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } { (libc::STDIN_FILENO, false) } else { @@ -87,3 +137,18 @@ pub fn tty_fd() -> io::Result { Ok(FileDesc::new(fd, close_on_drop)) } + +#[cfg(not(feature = "libc"))] +/// Creates a file descriptor pointing to the standard input or `/dev/tty`. +pub fn tty_fd() -> io::Result> { + use std::fs::File; + + let stdin = rustix::stdio::stdin(); + let fd = if rustix::termios::isatty(stdin) { + FileDesc::Borrowed(stdin) + } else { + let dev_tty = File::options().read(true).write(true).open("/dev/tty")?; + FileDesc::Owned(dev_tty.into()) + }; + Ok(fd) +} diff --git a/src/terminal/sys/unix.rs b/src/terminal/sys/unix.rs index ed545c5b2..7129730a6 100644 --- a/src/terminal/sys/unix.rs +++ b/src/terminal/sys/unix.rs @@ -4,16 +4,24 @@ use crate::terminal::{ sys::file_descriptor::{tty_fd, FileDesc}, WindowSize, }; +#[cfg(feature = "libc")] use libc::{ cfmakeraw, ioctl, tcgetattr, tcsetattr, termios as Termios, winsize, STDOUT_FILENO, TCSANOW, TIOCGWINSZ, }; use parking_lot::Mutex; -use std::fs::File; - -use std::os::unix::io::{IntoRawFd, RawFd}; +#[cfg(not(feature = "libc"))] +use rustix::{ + fd::AsFd, + termios::{Termios, Winsize}, +}; -use std::{io, mem, process}; +use std::{fs::File, io, process}; +#[cfg(feature = "libc")] +use std::{ + mem, + os::unix::io::{IntoRawFd, RawFd}, +}; // Some(Termios) -> we're in the raw mode and this is the previous mode // None -> we're not in the raw mode @@ -23,6 +31,7 @@ pub(crate) fn is_raw_mode_enabled() -> bool { TERMINAL_MODE_PRIOR_RAW_MODE.lock().is_some() } +#[cfg(feature = "libc")] impl From for WindowSize { fn from(size: winsize) -> WindowSize { WindowSize { @@ -33,8 +42,20 @@ impl From for WindowSize { } } } +#[cfg(not(feature = "libc"))] +impl From for WindowSize { + fn from(size: Winsize) -> WindowSize { + WindowSize { + columns: size.ws_col, + rows: size.ws_row, + width: size.ws_xpixel, + height: size.ws_ypixel, + } + } +} #[allow(clippy::useless_conversion)] +#[cfg(feature = "libc")] pub(crate) fn window_size() -> io::Result { // http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc let mut size = winsize { @@ -59,6 +80,19 @@ pub(crate) fn window_size() -> io::Result { Err(std::io::Error::last_os_error().into()) } +#[cfg(not(feature = "libc"))] +pub(crate) fn window_size() -> io::Result { + let file = File::open("/dev/tty").map(|file| (FileDesc::Owned(file.into()))); + let fd = if let Ok(file) = &file { + file.as_fd() + } else { + // Fallback to libc::STDOUT_FILENO if /dev/tty is missing + rustix::stdio::stdout() + }; + let size = rustix::termios::tcgetwinsize(fd)?; + Ok(size.into()) +} + #[allow(clippy::useless_conversion)] pub(crate) fn size() -> io::Result<(u16, u16)> { if let Ok(window_size) = window_size() { @@ -68,9 +102,9 @@ pub(crate) fn size() -> io::Result<(u16, u16)> { tput_size().ok_or_else(|| std::io::Error::last_os_error().into()) } +#[cfg(feature = "libc")] pub(crate) fn enable_raw_mode() -> io::Result<()> { let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock(); - if original_mode.is_some() { return Ok(()); } @@ -79,13 +113,27 @@ pub(crate) fn enable_raw_mode() -> io::Result<()> { let fd = tty.raw_fd(); let mut ios = get_terminal_attr(fd)?; let original_mode_ios = ios; - raw_terminal_attr(&mut ios); set_terminal_attr(fd, &ios)?; - // Keep it last - set the original mode only if we were able to switch to the raw mode *original_mode = Some(original_mode_ios); + Ok(()) +} +#[cfg(not(feature = "libc"))] +pub(crate) fn enable_raw_mode() -> io::Result<()> { + let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock(); + if original_mode.is_some() { + return Ok(()); + } + + let tty = tty_fd()?; + let mut ios = get_terminal_attr(&tty)?; + let original_mode_ios = ios.clone(); + ios.make_raw(); + set_terminal_attr(&tty, &ios)?; + // Keep it last - set the original mode only if we were able to switch to the raw mode + *original_mode = Some(original_mode_ios); Ok(()) } @@ -94,16 +142,39 @@ pub(crate) fn enable_raw_mode() -> io::Result<()> { /// More precisely, reset the whole termios mode to what it was before the first call /// to [enable_raw_mode]. If you don't mess with termios outside of crossterm, it's /// effectively disabling the raw mode and doing nothing else. +#[cfg(feature = "libc")] pub(crate) fn disable_raw_mode() -> io::Result<()> { let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock(); - if let Some(original_mode_ios) = original_mode.as_ref() { let tty = tty_fd()?; set_terminal_attr(tty.raw_fd(), original_mode_ios)?; // Keep it last - remove the original mode only if we were able to switch back *original_mode = None; } + Ok(()) +} + +#[cfg(not(feature = "libc"))] +pub(crate) fn disable_raw_mode() -> io::Result<()> { + let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock(); + if let Some(original_mode_ios) = original_mode.as_ref() { + let tty = tty_fd()?; + set_terminal_attr(&tty, original_mode_ios)?; + // Keep it last - remove the original mode only if we were able to switch back + *original_mode = None; + } + Ok(()) +} + +#[cfg(not(feature = "libc"))] +fn get_terminal_attr(fd: impl AsFd) -> io::Result { + let result = rustix::termios::tcgetattr(fd)?; + Ok(result) +} +#[cfg(not(feature = "libc"))] +fn set_terminal_attr(fd: impl AsFd, termios: &Termios) -> io::Result<()> { + rustix::termios::tcsetattr(fd, rustix::termios::OptionalActions::Now, termios)?; Ok(()) } @@ -214,11 +285,13 @@ fn tput_size() -> Option<(u16, u16)> { } } +#[cfg(feature = "libc")] // Transform the given mode into an raw mode (non-canonical) mode. fn raw_terminal_attr(termios: &mut Termios) { unsafe { cfmakeraw(termios) } } +#[cfg(feature = "libc")] fn get_terminal_attr(fd: RawFd) -> io::Result { unsafe { let mut termios = mem::zeroed(); @@ -227,10 +300,12 @@ fn get_terminal_attr(fd: RawFd) -> io::Result { } } +#[cfg(feature = "libc")] fn set_terminal_attr(fd: RawFd, termios: &Termios) -> io::Result<()> { wrap_with_result(unsafe { tcsetattr(fd, TCSANOW, termios) }) } +#[cfg(feature = "libc")] fn wrap_with_result(result: i32) -> io::Result<()> { if result == -1 { Err(io::Error::last_os_error()) diff --git a/src/tty.rs b/src/tty.rs index 78e32aae7..5a710b4ab 100644 --- a/src/tty.rs +++ b/src/tty.rs @@ -26,7 +26,7 @@ pub trait IsTty { /// On UNIX, the `isatty()` function returns true if a file /// descriptor is a terminal. -#[cfg(unix)] +#[cfg(all(unix, feature = "libc"))] impl IsTty for S { fn is_tty(&self) -> bool { let fd = self.as_raw_fd(); @@ -34,6 +34,14 @@ impl IsTty for S { } } +#[cfg(all(unix, not(feature = "libc")))] +impl IsTty for S { + fn is_tty(&self) -> bool { + let fd = self.as_raw_fd(); + rustix::termios::isatty(unsafe { std::os::unix::io::BorrowedFd::borrow_raw(fd) }) + } +} + /// On windows, `GetConsoleMode` will return true if we are in a terminal. /// Otherwise false. #[cfg(windows)]