Skip to content

Commit 4d1f3ea

Browse files
committed
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.
1 parent 629de02 commit 4d1f3ea

File tree

4 files changed

+257
-80
lines changed

4 files changed

+257
-80
lines changed

src/backend/libc/termios/syscalls.rs

Lines changed: 132 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ use crate::ffi::CStr;
2020
)
2121
))]
2222
use core::mem::MaybeUninit;
23+
#[cfg(linux_kernel)]
24+
use core::sync::atomic::{AtomicBool, Ordering};
2325
#[cfg(not(target_os = "wasi"))]
2426
use {crate::io, crate::pid::Pid};
2527
#[cfg(not(any(target_os = "espidf", target_os = "wasi")))]
@@ -28,6 +30,10 @@ use {
2830
crate::utils::as_mut_ptr,
2931
};
3032

33+
/// Is `TCGETS2` known to be available?
34+
#[cfg(linux_kernel)]
35+
static TCGETS2_KNOWN: AtomicBool = AtomicBool::new(false);
36+
3137
#[cfg(not(any(target_os = "espidf", target_os = "wasi")))]
3238
pub(crate) fn tcgetattr(fd: BorrowedFd<'_>) -> io::Result<Termios> {
3339
// 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<Termios> {
3844
use crate::utils::default_array;
3945

4046
let termios2 = unsafe {
41-
let mut termios2 = MaybeUninit::<c::termios2>::uninit();
47+
let mut termios2 = MaybeUninit::<termios2>::uninit();
4248

4349
// QEMU's `TCGETS2` doesn't currently set `input_speed` or
4450
// `output_speed` on PowerPC, so zero out the fields ourselves.
@@ -47,11 +53,26 @@ pub(crate) fn tcgetattr(fd: BorrowedFd<'_>) -> io::Result<Termios> {
4753
termios2.write(core::mem::zeroed());
4854
}
4955

50-
ret(c::ioctl(
56+
let res = ret(c::ioctl(
5157
borrowed_fd(fd),
5258
c::TCGETS2 as _,
5359
termios2.as_mut_ptr(),
54-
))?;
60+
));
61+
match res {
62+
Ok(()) => TCGETS2_KNOWN.store(true, Ordering::Relaxed),
63+
Err(io::Errno::NOTTY) => tcgetattr_fallback(fd, &mut termios2)?,
64+
Err(err) => {
65+
TCGETS2_KNOWN.store(true, Ordering::Relaxed);
66+
return Err(err);
67+
}
68+
}
69+
70+
// QEMU's `TCGETS2` doesn't currently set `input_speed` or
71+
// `output_speed` on PowerPC, so set them manually if we can.
72+
#[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))]
73+
{
74+
infer_input_output_speed(termios2.assume_init_mut())
75+
}
5576

5677
termios2.assume_init()
5778
};
@@ -68,32 +89,6 @@ pub(crate) fn tcgetattr(fd: BorrowedFd<'_>) -> io::Result<Termios> {
6889
output_speed: termios2.c_ospeed,
6990
};
7091

71-
// QEMU's `TCGETS2` doesn't currently set `input_speed` or
72-
// `output_speed` on PowerPC, so set them manually if we can.
73-
#[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))]
74-
{
75-
use crate::termios::speed;
76-
77-
if result.output_speed == 0 && (termios2.c_cflag & c::CBAUD) != c::BOTHER {
78-
if let Some(output_speed) = speed::decode(termios2.c_cflag & c::CBAUD) {
79-
result.output_speed = output_speed;
80-
}
81-
}
82-
if result.input_speed == 0
83-
&& ((termios2.c_cflag & c::CIBAUD) >> c::IBSHIFT) != c::BOTHER
84-
{
85-
// For input speeds, `B0` is special-cased to mean the input
86-
// speed is the same as the output speed.
87-
if ((termios2.c_cflag & c::CIBAUD) >> c::IBSHIFT) == c::B0 {
88-
result.input_speed = result.output_speed;
89-
} else if let Some(input_speed) =
90-
speed::decode((termios2.c_cflag & c::CIBAUD) >> c::IBSHIFT)
91-
{
92-
result.input_speed = input_speed;
93-
}
94-
}
95-
}
96-
9792
result.special_codes.0[..termios2.c_cc.len()].copy_from_slice(&termios2.c_cc);
9893

9994
Ok(result)
@@ -111,6 +106,65 @@ pub(crate) fn tcgetattr(fd: BorrowedFd<'_>) -> io::Result<Termios> {
111106
}
112107
}
113108

109+
/// Workaround for WSL where `TCGETS2` is unavailable.
110+
#[cfg(linux_kernel)]
111+
#[cold]
112+
unsafe fn tcgetattr_fallback(
113+
fd: BorrowedFd<'_>,
114+
result: &mut MaybeUninit<termios2>,
115+
) -> io::Result<()> {
116+
// If we've already seen `TCGETS2` succeed or fail in a way other than
117+
// `NOTTY`, then can trust a `NOTTY` error from it.
118+
if TCGETS2_KNOWN.load(Ordering::Relaxed) {
119+
return Err(io::Errno::NOTTY);
120+
}
121+
122+
// Ensure that the `c_ispeed` and `c_ospeed` fields are initialized,
123+
// because `TCGETS` won't write to them.
124+
result.write(core::mem::zeroed());
125+
126+
let res = ret(c::ioctl(
127+
borrowed_fd(fd),
128+
c::TCGETS as _,
129+
result.as_mut_ptr(),
130+
));
131+
match res {
132+
Ok(()) => {
133+
infer_input_output_speed(result.assume_init_mut());
134+
Ok(())
135+
}
136+
Err(io::Errno::NOTTY) => Err(io::Errno::NOTTY),
137+
Err(err) => {
138+
TCGETS2_KNOWN.store(true, Ordering::Relaxed);
139+
Err(err)
140+
}
141+
}
142+
}
143+
144+
/// Fill in the `input_speed` and `output_speed` fields of `Termios` using
145+
/// information available in other fields.
146+
#[cfg(linux_kernel)]
147+
#[cold]
148+
fn infer_input_output_speed(result: &mut termios2) {
149+
use crate::termios::speed;
150+
151+
if result.c_ospeed == 0 && (result.c_cflag & c::CBAUD) != c::BOTHER {
152+
if let Some(output_speed) = speed::decode(result.c_cflag & c::CBAUD) {
153+
result.c_ospeed = output_speed;
154+
}
155+
}
156+
if result.c_ispeed == 0 && ((result.c_cflag & c::CIBAUD) >> c::IBSHIFT) != c::BOTHER {
157+
// For input speeds, `B0` is special-cased to mean the input
158+
// speed is the same as the output speed.
159+
if ((result.c_cflag & c::CIBAUD) >> c::IBSHIFT) == c::B0 {
160+
result.c_ispeed = result.c_ospeed;
161+
} else if let Some(input_speed) = speed::decode((result.c_cflag & c::CIBAUD) >> c::IBSHIFT)
162+
{
163+
result.c_ispeed = input_speed;
164+
}
165+
}
166+
}
167+
114168
#[cfg(not(target_os = "wasi"))]
115169
pub(crate) fn tcgetpgrp(fd: BorrowedFd<'_>) -> io::Result<Pid> {
116170
unsafe {
@@ -133,6 +187,22 @@ pub(crate) fn tcsetpgrp(fd: BorrowedFd<'_>, pid: Pid) -> io::Result<()> {
133187
unsafe { ret(c::tcsetpgrp(borrowed_fd(fd), pid.as_raw_nonzero().get())) }
134188
}
135189

190+
#[cfg(linux_kernel)]
191+
use linux_raw_sys::general::termios2;
192+
193+
#[cfg(linux_kernel)]
194+
#[cfg(not(any(target_arch = "sparc", target_arch = "sparc64")))]
195+
use linux_raw_sys::ioctl::{TCSETS, TCSETS2};
196+
197+
// linux-raw-sys' ioctl-generation script for sparc isn't working yet,
198+
// so as a temporary workaround, declare these manually.
199+
#[cfg(linux_kernel)]
200+
#[cfg(any(target_arch = "sparc", target_arch = "sparc64"))]
201+
const TCSETS: u32 = 0x8024_5409;
202+
#[cfg(linux_kernel)]
203+
#[cfg(any(target_arch = "sparc", target_arch = "sparc64"))]
204+
const TCSETS2: u32 = 0x802c_540d;
205+
136206
#[cfg(not(any(target_os = "espidf", target_os = "wasi")))]
137207
pub(crate) fn tcsetattr(
138208
fd: BorrowedFd<'_>,
@@ -145,17 +215,7 @@ pub(crate) fn tcsetattr(
145215
{
146216
use crate::termios::speed;
147217
use crate::utils::default_array;
148-
use linux_raw_sys::general::{termios2, BOTHER, CBAUD, IBSHIFT};
149-
150-
#[cfg(not(any(target_arch = "sparc", target_arch = "sparc64")))]
151-
use linux_raw_sys::ioctl::{TCSETS, TCSETS2};
152-
153-
// linux-raw-sys' ioctl-generation script for sparc isn't working yet,
154-
// so as a temporary workaround, declare these manually.
155-
#[cfg(any(target_arch = "sparc", target_arch = "sparc64"))]
156-
const TCSETS: u32 = 0x8024_5409;
157-
#[cfg(any(target_arch = "sparc", target_arch = "sparc64"))]
158-
const TCSETS2: u32 = 0x802c_540d;
218+
use linux_raw_sys::general::{BOTHER, CBAUD, IBSHIFT};
159219

160220
// Translate from `optional_actions` into an ioctl request code. On
161221
// MIPS, `optional_actions` already has `TCGETS` added to it.
@@ -194,7 +254,14 @@ pub(crate) fn tcsetattr(
194254
.c_cc
195255
.copy_from_slice(&termios.special_codes.0[..nccs]);
196256

197-
unsafe { ret(c::ioctl(borrowed_fd(fd), request as _, &termios2)) }
257+
unsafe {
258+
let res = ret(c::ioctl(borrowed_fd(fd), request as _, &termios2));
259+
match res {
260+
Ok(()) => Ok(()),
261+
Err(io::Errno::NOTTY) => tcsetattr_fallback(fd, optional_actions, &termios2),
262+
Err(err) => Err(err),
263+
}
264+
}
198265
}
199266

200267
#[cfg(not(linux_kernel))]
@@ -207,6 +274,30 @@ pub(crate) fn tcsetattr(
207274
}
208275
}
209276

277+
/// Workaround for WSL where `TCSETS2` is unavailable.
278+
#[cfg(linux_kernel)]
279+
#[cold]
280+
unsafe fn tcsetattr_fallback(
281+
fd: BorrowedFd<'_>,
282+
optional_actions: OptionalActions,
283+
result: &termios2,
284+
) -> io::Result<()> {
285+
// Translate from `optional_actions` into an ioctl request code. On
286+
// MIPS, `optional_actions` already has `TCGETS` added to it.
287+
let request = if cfg!(any(
288+
target_arch = "mips",
289+
target_arch = "mips32r6",
290+
target_arch = "mips64",
291+
target_arch = "mips64r6"
292+
)) {
293+
optional_actions as u32
294+
} else {
295+
optional_actions as u32 + TCSETS
296+
};
297+
298+
ret(c::ioctl(borrowed_fd(fd), request as _, result))
299+
}
300+
210301
#[cfg(not(target_os = "wasi"))]
211302
pub(crate) fn tcsendbreak(fd: BorrowedFd<'_>) -> io::Result<()> {
212303
unsafe { ret(c::tcsendbreak(borrowed_fd(fd), 0)) }

src/backend/linux_raw/c.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ pub(crate) use linux_raw_sys::{
190190
VKILL, VLNEXT, VMIN, VQUIT, VREPRINT, VSTART, VSTOP, VSUSP, VSWTC, VT0, VT1, VTDLY, VTIME,
191191
VWERASE, XCASE, XTABS,
192192
},
193-
ioctl::{TCGETS2, TCSETS2, TCSETSF2, TCSETSW2, TIOCEXCL, TIOCNXCL},
193+
ioctl::{TCGETS, TCGETS2, TCSETS, TCSETS2, TCSETSF2, TCSETSW2, TIOCEXCL, TIOCNXCL},
194194
};
195195

196196
// On MIPS, `TCSANOW` et al have `TCSETS` added to them, so we need it to

0 commit comments

Comments
 (0)