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
## Added ⭐

View File

@ -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 }

View File

@ -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<Self> {
pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result<Self> {
let poll = Poll::new()?;
let registry = poll.registry();

View File

@ -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<Self> {
pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result<Self> {
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?

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::{
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<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) {
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<FileDesc> {
pub fn tty_fd() -> io::Result<FileDesc<'static>> {
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<FileDesc> {
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},
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;
#[cfg(not(feature = "libc"))]
use rustix::{
fd::AsFd,
termios::{Termios, Winsize},
};
use std::os::unix::io::{IntoRawFd, RawFd};
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<winsize> for WindowSize {
fn from(size: winsize) -> 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)]
#[cfg(feature = "libc")]
pub(crate) fn window_size() -> io::Result<WindowSize> {
// 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<WindowSize> {
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)]
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<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(())
}
@ -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<Termios> {
unsafe {
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<()> {
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())

View File

@ -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<S: AsRawFd> IsTty for S {
fn is_tty(&self) -> bool {
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.
/// Otherwise false.
#[cfg(windows)]