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
This commit is contained in:
Josh McKinney 2024-06-16 05:56:13 -07:00 committed by GitHub
parent be8cb8ce8e
commit fe440284bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 195 additions and 25 deletions

View File

@ -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 # Version 0.27.1
## Added ⭐ ## Added ⭐

View File

@ -10,7 +10,7 @@ keywords = ["event", "color", "cli", "input", "terminal"]
exclude = ["target", "Cargo.lock"] exclude = ["target", "Cargo.lock"]
readme = "README.md" readme = "README.md"
edition = "2021" edition = "2021"
rust-version = "1.58.0" rust-version = "1.63.0"
categories = ["command-line-interface", "command-line-utilities"] categories = ["command-line-interface", "command-line-utilities"]
[lib] [lib]
@ -71,7 +71,14 @@ crossterm_winapi = { version = "0.9.1", optional = true }
# UNIX dependencies # UNIX dependencies
# #
[target.'cfg(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 } signal-hook = { version = "0.3.17", optional = true }
filedescriptor = { version = "0.8", optional = true } filedescriptor = { version = "0.8", optional = true }
mio = { version = "0.8", features = ["os-poll"], optional = true } mio = { version = "0.8", features = ["os-poll"], optional = true }

View File

@ -26,7 +26,7 @@ pub(crate) struct UnixInternalEventSource {
events: Events, events: Events,
parser: Parser, parser: Parser,
tty_buffer: [u8; TTY_BUFFER_SIZE], tty_buffer: [u8; TTY_BUFFER_SIZE],
tty_fd: FileDesc, tty_fd: FileDesc<'static>,
signals: Signals, signals: Signals,
#[cfg(feature = "event-stream")] #[cfg(feature = "event-stream")]
waker: Waker, waker: Waker,
@ -37,7 +37,7 @@ impl UnixInternalEventSource {
UnixInternalEventSource::from_file_descriptor(tty_fd()?) UnixInternalEventSource::from_file_descriptor(tty_fd()?)
} }
pub(crate) fn from_file_descriptor(input_fd: FileDesc) -> io::Result<Self> { pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result<Self> {
let poll = Poll::new()?; let poll = Poll::new()?;
let registry = poll.registry(); let registry = poll.registry();

View File

@ -1,6 +1,10 @@
#[cfg(feature = "libc")]
use std::os::unix::prelude::AsRawFd; use std::os::unix::prelude::AsRawFd;
use std::{collections::VecDeque, io, os::unix::net::UnixStream, time::Duration}; 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 signal_hook::low_level::pipe;
use crate::event::timeout::PollTimeout; use crate::event::timeout::PollTimeout;
@ -38,7 +42,7 @@ const TTY_BUFFER_SIZE: usize = 1_024;
pub(crate) struct UnixInternalEventSource { pub(crate) struct UnixInternalEventSource {
parser: Parser, parser: Parser,
tty_buffer: [u8; TTY_BUFFER_SIZE], tty_buffer: [u8; TTY_BUFFER_SIZE],
tty: FileDesc, tty: FileDesc<'static>,
winch_signal_receiver: UnixStream, winch_signal_receiver: UnixStream,
#[cfg(feature = "event-stream")] #[cfg(feature = "event-stream")]
wake_pipe: WakePipe, wake_pipe: WakePipe,
@ -56,7 +60,7 @@ impl UnixInternalEventSource {
UnixInternalEventSource::from_file_descriptor(tty_fd()?) UnixInternalEventSource::from_file_descriptor(tty_fd()?)
} }
pub(crate) fn from_file_descriptor(input_fd: FileDesc) -> io::Result<Self> { pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result<Self> {
Ok(UnixInternalEventSource { Ok(UnixInternalEventSource {
parser: Parser::default(), parser: Parser::default(),
tty_buffer: [0u8; TTY_BUFFER_SIZE], tty_buffer: [0u8; TTY_BUFFER_SIZE],
@ -64,7 +68,10 @@ impl UnixInternalEventSource {
winch_signal_receiver: { winch_signal_receiver: {
let (receiver, sender) = nonblocking_unix_pair()?; let (receiver, sender) = nonblocking_unix_pair()?;
// Unregistering is unnecessary because EventSource is a singleton // Unregistering is unnecessary because EventSource is a singleton
#[cfg(feature = "libc")]
pipe::register(libc::SIGWINCH, sender)?; pipe::register(libc::SIGWINCH, sender)?;
#[cfg(not(feature = "libc"))]
pipe::register(rustix::process::Signal::Winch as i32, sender)?;
receiver receiver
}, },
#[cfg(feature = "event-stream")] #[cfg(feature = "event-stream")]
@ -157,7 +164,10 @@ impl EventSource for UnixInternalEventSource {
} }
} }
if fds[1].revents & POLLIN != 0 { if fds[1].revents & POLLIN != 0 {
#[cfg(feature = "libc")]
let fd = FileDesc::new(self.winch_signal_receiver.as_raw_fd(), false); 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 // drain the pipe
while read_complete(&fd, &mut [0; 1024])? != 0 {} while read_complete(&fd, &mut [0; 1024])? != 0 {}
// TODO Should we remove tput? // TODO Should we remove tput?

View File

@ -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::{ use std::{
fs, io, fs,
marker::PhantomData,
os::unix::{ os::unix::{
io::{IntoRawFd, RawFd}, io::{IntoRawFd, RawFd},
prelude::AsRawFd, prelude::AsRawFd,
}, },
}; };
use libc::size_t;
/// A file descriptor wrapper. /// A file descriptor wrapper.
/// ///
/// It allows to retrieve raw file descriptor, write to the file descriptor and /// It allows to retrieve raw file descriptor, write to the file descriptor and
/// mainly it closes the file descriptor once dropped. /// mainly it closes the file descriptor once dropped.
#[derive(Debug)] #[derive(Debug)]
pub struct FileDesc { #[cfg(feature = "libc")]
pub struct FileDesc<'a> {
fd: RawFd, fd: RawFd,
close_on_drop: bool, 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`. /// Constructs a new `FileDesc` with the given `RawFd`.
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `fd` - raw file descriptor /// * `fd` - raw file descriptor
/// * `close_on_drop` - specify if the raw file descriptor should be closed once the `FileDesc` is dropped /// * `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 { pub fn new(fd: RawFd, close_on_drop: bool) -> FileDesc<'static> {
FileDesc { fd, close_on_drop } FileDesc {
fd,
close_on_drop,
phantom: PhantomData,
}
} }
pub fn read(&self, buffer: &mut [u8]) -> io::Result<usize> { pub fn read(&self, buffer: &mut [u8]) -> io::Result<usize> {
@ -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<usize> {
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) { fn drop(&mut self) {
if self.close_on_drop { if self.close_on_drop {
// Note that errors are ignored when closing a file descriptor. The // 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 { fn as_raw_fd(&self) -> RawFd {
self.raw_fd() 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`. /// Creates a file descriptor pointing to the standard input or `/dev/tty`.
pub fn tty_fd() -> io::Result<FileDesc> { pub fn tty_fd() -> io::Result<FileDesc<'static>> {
let (fd, close_on_drop) = if unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } { let (fd, close_on_drop) = if unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } {
(libc::STDIN_FILENO, false) (libc::STDIN_FILENO, false)
} else { } else {
@ -87,3 +137,18 @@ pub fn tty_fd() -> io::Result<FileDesc> {
Ok(FileDesc::new(fd, close_on_drop)) 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<FileDesc<'static>> {
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)
}

View File

@ -4,16 +4,24 @@ use crate::terminal::{
sys::file_descriptor::{tty_fd, FileDesc}, sys::file_descriptor::{tty_fd, FileDesc},
WindowSize, WindowSize,
}; };
#[cfg(feature = "libc")]
use libc::{ use libc::{
cfmakeraw, ioctl, tcgetattr, tcsetattr, termios as Termios, winsize, STDOUT_FILENO, TCSANOW, cfmakeraw, ioctl, tcgetattr, tcsetattr, termios as Termios, winsize, STDOUT_FILENO, TCSANOW,
TIOCGWINSZ, TIOCGWINSZ,
}; };
use parking_lot::Mutex; use parking_lot::Mutex;
use std::fs::File; #[cfg(not(feature = "libc"))]
use rustix::{
fd::AsFd,
termios::{Termios, Winsize},
};
use std::os::unix::io::{IntoRawFd, RawFd}; use std::{fs::File, io, process};
#[cfg(feature = "libc")]
use std::{io, mem, process}; use std::{
mem,
os::unix::io::{IntoRawFd, RawFd},
};
// Some(Termios) -> we're in the raw mode and this is the previous mode // Some(Termios) -> we're in the raw mode and this is the previous mode
// None -> we're not in the raw 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() TERMINAL_MODE_PRIOR_RAW_MODE.lock().is_some()
} }
#[cfg(feature = "libc")]
impl From<winsize> for WindowSize { impl From<winsize> for WindowSize {
fn from(size: winsize) -> WindowSize { fn from(size: winsize) -> WindowSize {
WindowSize { WindowSize {
@ -33,8 +42,20 @@ impl From<winsize> for WindowSize {
} }
} }
} }
#[cfg(not(feature = "libc"))]
impl From<Winsize> 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)] #[allow(clippy::useless_conversion)]
#[cfg(feature = "libc")]
pub(crate) fn window_size() -> io::Result<WindowSize> { pub(crate) fn window_size() -> io::Result<WindowSize> {
// http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc // http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc
let mut size = winsize { let mut size = winsize {
@ -59,6 +80,19 @@ pub(crate) fn window_size() -> io::Result<WindowSize> {
Err(std::io::Error::last_os_error().into()) Err(std::io::Error::last_os_error().into())
} }
#[cfg(not(feature = "libc"))]
pub(crate) fn window_size() -> io::Result<WindowSize> {
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)] #[allow(clippy::useless_conversion)]
pub(crate) fn size() -> io::Result<(u16, u16)> { pub(crate) fn size() -> io::Result<(u16, u16)> {
if let Ok(window_size) = window_size() { 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()) tput_size().ok_or_else(|| std::io::Error::last_os_error().into())
} }
#[cfg(feature = "libc")]
pub(crate) fn enable_raw_mode() -> io::Result<()> { pub(crate) fn enable_raw_mode() -> io::Result<()> {
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock(); let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
if original_mode.is_some() { if original_mode.is_some() {
return Ok(()); return Ok(());
} }
@ -79,13 +113,27 @@ pub(crate) fn enable_raw_mode() -> io::Result<()> {
let fd = tty.raw_fd(); let fd = tty.raw_fd();
let mut ios = get_terminal_attr(fd)?; let mut ios = get_terminal_attr(fd)?;
let original_mode_ios = ios; let original_mode_ios = ios;
raw_terminal_attr(&mut ios); raw_terminal_attr(&mut ios);
set_terminal_attr(fd, &ios)?; set_terminal_attr(fd, &ios)?;
// Keep it last - set the original mode only if we were able to switch to the raw mode // Keep it last - set the original mode only if we were able to switch to the raw mode
*original_mode = Some(original_mode_ios); *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(()) 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 /// 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 /// 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. /// effectively disabling the raw mode and doing nothing else.
#[cfg(feature = "libc")]
pub(crate) fn disable_raw_mode() -> io::Result<()> { pub(crate) fn disable_raw_mode() -> io::Result<()> {
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock(); let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
if let Some(original_mode_ios) = original_mode.as_ref() { if let Some(original_mode_ios) = original_mode.as_ref() {
let tty = tty_fd()?; let tty = tty_fd()?;
set_terminal_attr(tty.raw_fd(), original_mode_ios)?; set_terminal_attr(tty.raw_fd(), original_mode_ios)?;
// Keep it last - remove the original mode only if we were able to switch back // Keep it last - remove the original mode only if we were able to switch back
*original_mode = None; *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<Termios> {
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(()) 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. // Transform the given mode into an raw mode (non-canonical) mode.
fn raw_terminal_attr(termios: &mut Termios) { fn raw_terminal_attr(termios: &mut Termios) {
unsafe { cfmakeraw(termios) } unsafe { cfmakeraw(termios) }
} }
#[cfg(feature = "libc")]
fn get_terminal_attr(fd: RawFd) -> io::Result<Termios> { fn get_terminal_attr(fd: RawFd) -> io::Result<Termios> {
unsafe { unsafe {
let mut termios = mem::zeroed(); let mut termios = mem::zeroed();
@ -227,10 +300,12 @@ fn get_terminal_attr(fd: RawFd) -> io::Result<Termios> {
} }
} }
#[cfg(feature = "libc")]
fn set_terminal_attr(fd: RawFd, termios: &Termios) -> io::Result<()> { fn set_terminal_attr(fd: RawFd, termios: &Termios) -> io::Result<()> {
wrap_with_result(unsafe { tcsetattr(fd, TCSANOW, termios) }) wrap_with_result(unsafe { tcsetattr(fd, TCSANOW, termios) })
} }
#[cfg(feature = "libc")]
fn wrap_with_result(result: i32) -> io::Result<()> { fn wrap_with_result(result: i32) -> io::Result<()> {
if result == -1 { if result == -1 {
Err(io::Error::last_os_error()) Err(io::Error::last_os_error())

View File

@ -26,7 +26,7 @@ pub trait IsTty {
/// On UNIX, the `isatty()` function returns true if a file /// On UNIX, the `isatty()` function returns true if a file
/// descriptor is a terminal. /// descriptor is a terminal.
#[cfg(unix)] #[cfg(all(unix, feature = "libc"))]
impl<S: AsRawFd> IsTty for S { impl<S: AsRawFd> IsTty for S {
fn is_tty(&self) -> bool { fn is_tty(&self) -> bool {
let fd = self.as_raw_fd(); let fd = self.as_raw_fd();
@ -34,6 +34,14 @@ impl<S: AsRawFd> IsTty for S {
} }
} }
#[cfg(all(unix, not(feature = "libc")))]
impl<S: AsRawFd> 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. /// On windows, `GetConsoleMode` will return true if we are in a terminal.
/// Otherwise false. /// Otherwise false.
#[cfg(windows)] #[cfg(windows)]