From 1bb5a1393542bda2c7a4dd9b5a5595e9b5ad3571 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Wed, 28 Aug 2024 12:41:28 -0700 Subject: [PATCH] Work around missing `TCGETS2`/`TCSETS2` on WSL. WSL appears to be lacking support for `TCGETS2` and `TCSETS2`, so teach rustix's `tcgetattr` and `tcsetattr` how to fall back to `TCGETS` and `TCSETS` as needed. This approach preserves rustix's ability to support arbitrary speed values, while falling back as needed to support WSL. This is expected to fix crossterm-rs/crossterm#912. --- src/backend/libc/termios/syscalls.rs | 173 +++++++++++++++++----- src/backend/linux_raw/c.rs | 2 +- src/backend/linux_raw/termios/syscalls.rs | 139 +++++++++++++---- src/termios/types.rs | 20 ++- 4 files changed, 255 insertions(+), 79 deletions(-) diff --git a/src/backend/libc/termios/syscalls.rs b/src/backend/libc/termios/syscalls.rs index 9eba5637f..4f0a157bf 100644 --- a/src/backend/libc/termios/syscalls.rs +++ b/src/backend/libc/termios/syscalls.rs @@ -20,6 +20,8 @@ use crate::ffi::CStr; ) ))] use core::mem::MaybeUninit; +#[cfg(linux_kernel)] +use core::sync::atomic::{AtomicBool, Ordering}; #[cfg(not(target_os = "wasi"))] use {crate::io, crate::pid::Pid}; #[cfg(not(any(target_os = "espidf", target_os = "wasi")))] @@ -28,6 +30,10 @@ use { crate::utils::as_mut_ptr, }; +/// Is `TCGETS2` known to be available? +#[cfg(linux_kernel)] +static TCGETS2_KNOWN: AtomicBool = AtomicBool::new(false); + #[cfg(not(any(target_os = "espidf", target_os = "wasi")))] pub(crate) fn tcgetattr(fd: BorrowedFd<'_>) -> io::Result { // If we have `TCGETS2`, use it, so that we fill in the `c_ispeed` and @@ -38,7 +44,7 @@ pub(crate) fn tcgetattr(fd: BorrowedFd<'_>) -> io::Result { use crate::utils::default_array; let termios2 = unsafe { - let mut termios2 = MaybeUninit::::uninit(); + let mut termios2 = MaybeUninit::::uninit(); // QEMU's `TCGETS2` doesn't currently set `input_speed` or // `output_speed` on PowerPC, so zero out the fields ourselves. @@ -47,11 +53,26 @@ pub(crate) fn tcgetattr(fd: BorrowedFd<'_>) -> io::Result { termios2.write(core::mem::zeroed()); } - ret(c::ioctl( + let res = ret(c::ioctl( borrowed_fd(fd), c::TCGETS2 as _, termios2.as_mut_ptr(), - ))?; + )); + match res { + Ok(()) => TCGETS2_KNOWN.store(true, Ordering::Relaxed), + Err(io::Errno::NOTTY) => tcgetattr_fallback(fd, &mut termios2)?, + Err(err) => { + TCGETS2_KNOWN.store(true, Ordering::Relaxed); + return Err(err); + } + } + + // QEMU's `TCGETS2` doesn't currently set `input_speed` or + // `output_speed` on PowerPC, so set them manually if we can. + #[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))] + { + infer_input_output_speed(termios2.assume_init_mut()) + } termios2.assume_init() }; @@ -68,32 +89,6 @@ pub(crate) fn tcgetattr(fd: BorrowedFd<'_>) -> io::Result { output_speed: termios2.c_ospeed, }; - // QEMU's `TCGETS2` doesn't currently set `input_speed` or - // `output_speed` on PowerPC, so set them manually if we can. - #[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))] - { - use crate::termios::speed; - - if result.output_speed == 0 && (termios2.c_cflag & c::CBAUD) != c::BOTHER { - if let Some(output_speed) = speed::decode(termios2.c_cflag & c::CBAUD) { - result.output_speed = output_speed; - } - } - if result.input_speed == 0 - && ((termios2.c_cflag & c::CIBAUD) >> c::IBSHIFT) != c::BOTHER - { - // For input speeds, `B0` is special-cased to mean the input - // speed is the same as the output speed. - if ((termios2.c_cflag & c::CIBAUD) >> c::IBSHIFT) == c::B0 { - result.input_speed = result.output_speed; - } else if let Some(input_speed) = - speed::decode((termios2.c_cflag & c::CIBAUD) >> c::IBSHIFT) - { - result.input_speed = input_speed; - } - } - } - result.special_codes.0[..termios2.c_cc.len()].copy_from_slice(&termios2.c_cc); Ok(result) @@ -111,6 +106,65 @@ pub(crate) fn tcgetattr(fd: BorrowedFd<'_>) -> io::Result { } } +/// Workaround for WSL where `TCGETS2` is unavailable. +#[cfg(linux_kernel)] +#[cold] +unsafe fn tcgetattr_fallback( + fd: BorrowedFd<'_>, + result: &mut MaybeUninit, +) -> io::Result<()> { + // If we've already seen `TCGETS2` succeed or fail in a way other than + // `NOTTY`, then can trust a `NOTTY` error from it. + if TCGETS2_KNOWN.load(Ordering::Relaxed) { + return Err(io::Errno::NOTTY); + } + + // Ensure that the `c_ispeed` and `c_ospeed` fields are initialized, + // because `TCGETS` won't write to them. + result.write(core::mem::zeroed()); + + let res = ret(c::ioctl( + borrowed_fd(fd), + c::TCGETS as _, + result.as_mut_ptr(), + )); + match res { + Ok(()) => { + infer_input_output_speed(result.assume_init_mut()); + Ok(()) + } + Err(io::Errno::NOTTY) => Err(io::Errno::NOTTY), + Err(err) => { + TCGETS2_KNOWN.store(true, Ordering::Relaxed); + Err(err) + } + } +} + +/// Fill in the `input_speed` and `output_speed` fields of `Termios` using +/// information available in other fields. +#[cfg(linux_kernel)] +#[cold] +fn infer_input_output_speed(result: &mut termios2) { + use crate::termios::speed; + + if result.c_ospeed == 0 && (result.c_cflag & c::CBAUD) != c::BOTHER { + if let Some(output_speed) = speed::decode(result.c_cflag & c::CBAUD) { + result.c_ospeed = output_speed; + } + } + if result.c_ispeed == 0 && ((result.c_cflag & c::CIBAUD) >> c::IBSHIFT) != c::BOTHER { + // For input speeds, `B0` is special-cased to mean the input + // speed is the same as the output speed. + if ((result.c_cflag & c::CIBAUD) >> c::IBSHIFT) == c::B0 { + result.c_ispeed = result.c_ospeed; + } else if let Some(input_speed) = speed::decode((result.c_cflag & c::CIBAUD) >> c::IBSHIFT) + { + result.c_ispeed = input_speed; + } + } +} + #[cfg(not(target_os = "wasi"))] pub(crate) fn tcgetpgrp(fd: BorrowedFd<'_>) -> io::Result { unsafe { @@ -133,6 +187,22 @@ pub(crate) fn tcsetpgrp(fd: BorrowedFd<'_>, pid: Pid) -> io::Result<()> { unsafe { ret(c::tcsetpgrp(borrowed_fd(fd), pid.as_raw_nonzero().get())) } } +#[cfg(linux_kernel)] +use linux_raw_sys::general::termios2; + +#[cfg(linux_kernel)] +#[cfg(not(any(target_arch = "sparc", target_arch = "sparc64")))] +use linux_raw_sys::ioctl::{TCSETS, TCSETS2}; + +// linux-raw-sys' ioctl-generation script for sparc isn't working yet, +// so as a temporary workaround, declare these manually. +#[cfg(linux_kernel)] +#[cfg(any(target_arch = "sparc", target_arch = "sparc64"))] +const TCSETS: u32 = 0x8024_5409; +#[cfg(linux_kernel)] +#[cfg(any(target_arch = "sparc", target_arch = "sparc64"))] +const TCSETS2: u32 = 0x802c_540d; + #[cfg(not(any(target_os = "espidf", target_os = "wasi")))] pub(crate) fn tcsetattr( fd: BorrowedFd<'_>, @@ -145,17 +215,7 @@ pub(crate) fn tcsetattr( { use crate::termios::speed; use crate::utils::default_array; - use linux_raw_sys::general::{termios2, BOTHER, CBAUD, IBSHIFT}; - - #[cfg(not(any(target_arch = "sparc", target_arch = "sparc64")))] - use linux_raw_sys::ioctl::{TCSETS, TCSETS2}; - - // linux-raw-sys' ioctl-generation script for sparc isn't working yet, - // so as a temporary workaround, declare these manually. - #[cfg(any(target_arch = "sparc", target_arch = "sparc64"))] - const TCSETS: u32 = 0x8024_5409; - #[cfg(any(target_arch = "sparc", target_arch = "sparc64"))] - const TCSETS2: u32 = 0x802c_540d; + use linux_raw_sys::general::{BOTHER, CBAUD, IBSHIFT}; // Translate from `optional_actions` into an ioctl request code. On // MIPS, `optional_actions` already has `TCGETS` added to it. @@ -194,7 +254,14 @@ pub(crate) fn tcsetattr( .c_cc .copy_from_slice(&termios.special_codes.0[..nccs]); - unsafe { ret(c::ioctl(borrowed_fd(fd), request as _, &termios2)) } + unsafe { + let res = ret(c::ioctl(borrowed_fd(fd), request as _, &termios2)); + match res { + Ok(()) => Ok(()), + Err(io::Errno::NOTTY) => tcsetattr_fallback(fd, optional_actions, &termios2), + Err(err) => Err(err), + } + } } #[cfg(not(linux_kernel))] @@ -207,6 +274,30 @@ pub(crate) fn tcsetattr( } } +/// Workaround for WSL where `TCSETS2` is unavailable. +#[cfg(linux_kernel)] +#[cold] +unsafe fn tcsetattr_fallback( + fd: BorrowedFd<'_>, + optional_actions: OptionalActions, + result: &termios2, +) -> io::Result<()> { + // Translate from `optional_actions` into an ioctl request code. On + // MIPS, `optional_actions` already has `TCGETS` added to it. + let request = if cfg!(any( + target_arch = "mips", + target_arch = "mips32r6", + target_arch = "mips64", + target_arch = "mips64r6" + )) { + optional_actions as u32 + } else { + optional_actions as u32 + TCSETS + }; + + ret(c::ioctl(borrowed_fd(fd), request as _, result)) +} + #[cfg(not(target_os = "wasi"))] pub(crate) fn tcsendbreak(fd: BorrowedFd<'_>) -> io::Result<()> { unsafe { ret(c::tcsendbreak(borrowed_fd(fd), 0)) } diff --git a/src/backend/linux_raw/c.rs b/src/backend/linux_raw/c.rs index 70bc46a8c..5667daf30 100644 --- a/src/backend/linux_raw/c.rs +++ b/src/backend/linux_raw/c.rs @@ -190,7 +190,7 @@ pub(crate) use linux_raw_sys::{ VKILL, VLNEXT, VMIN, VQUIT, VREPRINT, VSTART, VSTOP, VSUSP, VSWTC, VT0, VT1, VTDLY, VTIME, VWERASE, XCASE, XTABS, }, - ioctl::{TCGETS2, TCSETS2, TCSETSF2, TCSETSW2, TIOCEXCL, TIOCNXCL}, + ioctl::{TCGETS, TCGETS2, TCSETS, TCSETS2, TCSETSF2, TCSETSW2, TIOCEXCL, TIOCNXCL}, }; // On MIPS, `TCSANOW` et al have `TCSETS` added to them, so we need it to diff --git a/src/backend/linux_raw/termios/syscalls.rs b/src/backend/linux_raw/termios/syscalls.rs index 75eeb5edf..cccf85fa0 100644 --- a/src/backend/linux_raw/termios/syscalls.rs +++ b/src/backend/linux_raw/termios/syscalls.rs @@ -19,6 +19,7 @@ use crate::termios::{ #[cfg(all(feature = "alloc", feature = "procfs"))] use crate::{ffi::CStr, fs::FileType, path::DecInt}; use core::mem::MaybeUninit; +use core::sync::atomic::{AtomicBool, Ordering}; use linux_raw_sys::general::IBSHIFT; use linux_raw_sys::ioctl::{ TCFLSH, TCSBRK, TCXONC, TIOCGPGRP, TIOCGSID, TIOCGWINSZ, TIOCSPGRP, TIOCSWINSZ, @@ -33,6 +34,10 @@ pub(crate) fn tcgetwinsize(fd: BorrowedFd<'_>) -> io::Result { } } +/// Is `TCGETS2` known to be available? +#[cfg(linux_kernel)] +static TCGETS2_KNOWN: AtomicBool = AtomicBool::new(false); + #[inline] pub(crate) fn tcgetattr(fd: BorrowedFd<'_>) -> io::Result { unsafe { @@ -45,41 +50,90 @@ pub(crate) fn tcgetattr(fd: BorrowedFd<'_>) -> io::Result { result.write(core::mem::zeroed()); } - ret(syscall!(__NR_ioctl, fd, c_uint(c::TCGETS2), &mut result))?; + let res = ret(syscall!(__NR_ioctl, fd, c_uint(c::TCGETS2), &mut result)); + match res { + Ok(()) => TCGETS2_KNOWN.store(true, Ordering::Relaxed), + Err(io::Errno::NOTTY) => tcgetattr_fallback(fd, &mut result)?, + Err(err) => { + TCGETS2_KNOWN.store(true, Ordering::Relaxed); + return Err(err); + } + } let result = result.assume_init(); // QEMU's `TCGETS2` doesn't currently set `input_speed` or // `output_speed` on PowerPC, so set them manually if we can. #[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))] - let result = { - use crate::termios::speed; - let mut result = result; - if result.output_speed == 0 && (result.control_modes.bits() & c::CBAUD) != c::BOTHER { - if let Some(output_speed) = speed::decode(result.control_modes.bits() & c::CBAUD) { - result.output_speed = output_speed; - } - } - if result.input_speed == 0 - && ((result.control_modes.bits() & c::CIBAUD) >> c::IBSHIFT) != c::BOTHER - { - // For input speeds, `B0` is special-cased to mean the input - // speed is the same as the output speed. - if ((result.control_modes.bits() & c::CIBAUD) >> c::IBSHIFT) == c::B0 { - result.input_speed = result.output_speed; - } else if let Some(input_speed) = - speed::decode((result.control_modes.bits() & c::CIBAUD) >> c::IBSHIFT) - { - result.input_speed = input_speed; - } - } - result - }; + { + infer_input_output_speed(&mut result); + } Ok(result) } } +/// Workaround for WSL where `TCGETS2` is unavailable. +#[cold] +unsafe fn tcgetattr_fallback( + fd: BorrowedFd<'_>, + result: &mut MaybeUninit, +) -> io::Result<()> { + // If we've already seen `TCGETS2` succeed or fail in a way other than + // `NOTTY`, then can trust a `NOTTY` error from it. + if TCGETS2_KNOWN.load(Ordering::Relaxed) { + return Err(io::Errno::NOTTY); + } + + // Ensure that the `input_speed` and `output_speed` fields are initialized, + // because `TCGETS` won't write to them. + result.write(core::mem::zeroed()); + + let res = ret(syscall!( + __NR_ioctl, + fd, + c_uint(c::TCGETS), + result.as_mut_ptr() + )); + match res { + Ok(()) => { + infer_input_output_speed(result.assume_init_mut()); + Ok(()) + } + Err(io::Errno::NOTTY) => Err(io::Errno::NOTTY), + Err(err) => { + TCGETS2_KNOWN.store(true, Ordering::Relaxed); + Err(err) + } + } +} + +/// Fill in the `input_speed` and `output_speed` fields of `Termios` using +/// information available in other fields. +#[cold] +fn infer_input_output_speed(result: &mut Termios) { + use crate::termios::speed; + + if result.output_speed == 0 && (result.control_modes.bits() & c::CBAUD) != c::BOTHER { + if let Some(output_speed) = speed::decode(result.control_modes.bits() & c::CBAUD) { + result.output_speed = output_speed; + } + } + if result.input_speed == 0 + && ((result.control_modes.bits() & c::CIBAUD) >> c::IBSHIFT) != c::BOTHER + { + // For input speeds, `B0` is special-cased to mean the input + // speed is the same as the output speed. + if ((result.control_modes.bits() & c::CIBAUD) >> c::IBSHIFT) == c::B0 { + result.input_speed = result.output_speed; + } else if let Some(input_speed) = + speed::decode((result.control_modes.bits() & c::CIBAUD) >> c::IBSHIFT) + { + result.input_speed = input_speed; + } + } +} + #[inline] pub(crate) fn tcgetpgrp(fd: BorrowedFd<'_>) -> io::Result { unsafe { @@ -118,15 +172,48 @@ pub(crate) fn tcsetattr( optional_actions as u32 }; unsafe { - ret(syscall_readonly!( + let res = ret(syscall_readonly!( __NR_ioctl, fd, c_uint(request), by_ref(termios) - )) + )); + match res { + Ok(()) => Ok(()), + Err(io::Errno::NOTTY) => tcsetattr_fallback(fd, optional_actions, termios), + Err(err) => Err(err), + } } } +/// Workaround for WSL where `TCSETS2` is unavailable. +#[cold] +unsafe fn tcsetattr_fallback( + fd: BorrowedFd<'_>, + optional_actions: OptionalActions, + termios: &Termios, +) -> io::Result<()> { + // Translate from `optional_actions` into an ioctl request code. On MIPS, + // `optional_actions` already has `TCGETS` added to it. + let request = if cfg!(any( + target_arch = "mips", + target_arch = "mips32r6", + target_arch = "mips64", + target_arch = "mips64r6" + )) { + optional_actions as u32 + } else { + optional_actions as u32 + linux_raw_sys::ioctl::TCSETS + }; + + ret(syscall_readonly!( + __NR_ioctl, + fd, + c_uint(request), + by_ref(termios) + )) +} + #[inline] pub(crate) fn tcsendbreak(fd: BorrowedFd<'_>) -> io::Result<()> { unsafe { ret(syscall_readonly!(__NR_ioctl, fd, c_uint(TCSBRK), c_uint(0))) } diff --git a/src/termios/types.rs b/src/termios/types.rs index 57e9a5d20..7e3ccabc1 100644 --- a/src/termios/types.rs +++ b/src/termios/types.rs @@ -126,7 +126,7 @@ impl Termios { /// constant value. #[inline] pub fn output_speed(&self) -> u32 { - // On Linux and BSDs, `input_speed` is the arbitrary integer speed. + // On Linux and BSDs, `output_speed` is the arbitrary integer speed. #[cfg(any(linux_kernel, bsd))] { debug_assert!(u32::try_from(self.output_speed).is_ok()); @@ -810,16 +810,14 @@ pub mod speed { /// `u32`. /// /// On BSD platforms, integer speed values are already the same as their - /// encoded values, and on Linux platforms, we use `TCGETS2`/`TCSETS2` - /// and the `c_ispeed`/`c_ospeed`` fields, except that on Linux on - /// PowerPC on QEMU, `TCGETS2`/`TCSETS2` don't set `c_ispeed`/`c_ospeed`. - #[cfg(not(any( - bsd, - all( - linux_kernel, - not(any(target_arch = "powerpc", target_arch = "powerpc64")) - ) - )))] + /// encoded values. + /// + /// On Linux platforms, we use `TCGETS2`/`TCSETS2` and the + /// `c_ispeed`/`c_ospeed` fields. However, on Linux on PowerPC on QEMU, + /// `TCGETS2`/`TCSETS2` don't set `c_ispeed`/`c_ospeed`. And, on WSL as of + /// this writing, `TCGETS2`/`TCSETS2` are unsupported, so we have a + /// fallback that uses `TCGETS`/`TCSETS`. + #[cfg(not(bsd))] pub(crate) const fn decode(encoded_speed: c::speed_t) -> Option { match encoded_speed { c::B0 => Some(0),