Skip to main content

wasmer_wasix/os/tty/
tty_sys.rs

1use super::TtyBridge;
2use crate::WasiTtyState;
3
4/// [`TtyBridge`] implementation for Unix systems.
5#[derive(Debug, Default, Clone)]
6pub struct SysTty;
7
8impl TtyBridge for SysTty {
9    fn reset(&self) {
10        sys::reset().ok();
11    }
12
13    fn tty_get(&self) -> WasiTtyState {
14        let echo = sys::is_mode_echo();
15        let line_buffered = sys::is_mode_line_buffering();
16        let line_feeds = sys::is_mode_line_feeds();
17        let stdin_tty = sys::is_stdin_tty();
18        let stdout_tty = sys::is_stdout_tty();
19        let stderr_tty = sys::is_stderr_tty();
20        let (cols, rows) = sys_terminal_size::get_terminal_size();
21
22        WasiTtyState {
23            cols,
24            rows,
25            width: 800,
26            height: 600,
27            stdin_tty,
28            stdout_tty,
29            stderr_tty,
30            echo,
31            line_buffered,
32            line_feeds,
33        }
34    }
35
36    fn tty_set(&self, tty_state: WasiTtyState) {
37        if tty_state.echo {
38            sys::set_mode_echo().ok();
39        } else {
40            sys::set_mode_no_echo().ok();
41        }
42        if tty_state.line_buffered {
43            sys::set_mode_line_buffered().ok();
44        } else {
45            sys::set_mode_no_line_buffered().ok();
46        }
47        if tty_state.line_feeds {
48            sys::set_mode_line_feeds().ok();
49        } else {
50            sys::set_mode_no_line_feeds().ok();
51        }
52    }
53}
54
55mod sys_terminal_size {
56    static DEFAULT_SIZE: (u32, u32) = (80, 25);
57
58    #[cfg(not(target_arch = "wasm32"))]
59    pub fn get_terminal_size() -> (u32, u32) {
60        if let Some((terminal_size::Width(width), terminal_size::Height(height))) =
61            terminal_size::terminal_size()
62        {
63            (width.into(), height.into())
64        } else {
65            DEFAULT_SIZE
66        }
67    }
68
69    #[cfg(target_arch = "wasm32")]
70    pub fn get_terminal_size() -> (u32, u32) {
71        DEFAULT_SIZE
72    }
73}
74
75#[allow(unused_mut, unused_imports)]
76#[cfg(all(unix, not(target_os = "ios")))]
77mod sys {
78    use {
79        libc::{
80            ECHO, ECHOCTL, ECHOE, ECHOK, ECHONL, ICANON, ICRNL, IEXTEN, IGNCR, INLCR, ISIG, IXON,
81            ONLCR, OPOST, TCSANOW, c_int, tcsetattr, termios,
82        },
83        std::mem,
84        std::os::unix::io::AsRawFd,
85    };
86
87    fn io_result(ret: libc::c_int) -> std::io::Result<()> {
88        match ret {
89            0 => Ok(()),
90            _ => Err(std::io::Error::last_os_error()),
91        }
92    }
93
94    pub fn reset() -> Result<(), anyhow::Error> {
95        let mut termios = mem::MaybeUninit::<termios>::uninit();
96        io_result(unsafe { ::libc::tcgetattr(0, termios.as_mut_ptr()) })?;
97        let mut termios = unsafe { termios.assume_init() };
98
99        termios.c_lflag |= ISIG | IEXTEN | ECHO | ECHOE | ECHOK | ECHOCTL;
100        set_line_buffering(&mut termios, true);
101
102        unsafe { tcsetattr(0, TCSANOW, &termios) };
103        Ok(())
104    }
105
106    pub fn is_stdin_tty() -> bool {
107        ::termios::Termios::from_fd(0).is_ok()
108    }
109
110    pub fn is_stdout_tty() -> bool {
111        ::termios::Termios::from_fd(1).is_ok()
112    }
113
114    pub fn is_stderr_tty() -> bool {
115        ::termios::Termios::from_fd(2).is_ok()
116    }
117
118    pub fn is_mode_echo() -> bool {
119        if let Ok(termios) = ::termios::Termios::from_fd(0) {
120            (termios.c_lflag & ::termios::ECHO) != 0
121        } else {
122            false
123        }
124    }
125
126    pub fn is_mode_line_buffering() -> bool {
127        if let Ok(termios) = ::termios::Termios::from_fd(0) {
128            (termios.c_lflag & ::termios::ICANON) != 0
129        } else {
130            false
131        }
132    }
133
134    pub fn is_mode_line_feeds() -> bool {
135        if let Ok(termios) = ::termios::Termios::from_fd(0) {
136            (termios.c_lflag & ::termios::ONLCR) != 0
137        } else {
138            false
139        }
140    }
141
142    pub fn set_mode_no_echo() -> Result<(), anyhow::Error> {
143        let mut termios = mem::MaybeUninit::<termios>::uninit();
144        io_result(unsafe { ::libc::tcgetattr(0, termios.as_mut_ptr()) })?;
145        let mut termios = unsafe { termios.assume_init() };
146
147        termios.c_lflag &= !ECHO;
148        termios.c_lflag &= !ECHOE;
149        termios.c_lflag &= !ECHOK;
150        termios.c_lflag &= !ECHOCTL;
151        termios.c_lflag &= !IEXTEN;
152        /*
153        termios.c_lflag &= !ISIG;
154        termios.c_lflag &= !IXON;
155        termios.c_lflag &= !ICRNL;
156        termios.c_lflag &= !OPOST;
157        */
158
159        unsafe { tcsetattr(0, TCSANOW, &termios) };
160        Ok(())
161    }
162
163    pub fn set_mode_echo() -> Result<(), anyhow::Error> {
164        let mut termios = mem::MaybeUninit::<termios>::uninit();
165        io_result(unsafe { ::libc::tcgetattr(0, termios.as_mut_ptr()) })?;
166        let mut termios = unsafe { termios.assume_init() };
167
168        termios.c_lflag |= ECHO;
169        termios.c_lflag |= ECHOE;
170        termios.c_lflag |= ECHOK;
171        termios.c_lflag |= ECHOCTL;
172        termios.c_lflag |= IEXTEN;
173        /*
174        termios.c_lflag |= ISIG;
175        termios.c_lflag |= IXON;
176        termios.c_lflag |= ICRNL;
177        termios.c_lflag |= OPOST;
178        */
179
180        unsafe { tcsetattr(0, TCSANOW, &termios) };
181        Ok(())
182    }
183
184    pub fn set_mode_no_line_buffered() -> Result<(), anyhow::Error> {
185        let mut termios = mem::MaybeUninit::<termios>::uninit();
186        io_result(unsafe { ::libc::tcgetattr(0, termios.as_mut_ptr()) })?;
187        let mut termios = unsafe { termios.assume_init() };
188
189        set_line_buffering(&mut termios, false);
190
191        unsafe { tcsetattr(0, TCSANOW, &termios) };
192        Ok(())
193    }
194
195    pub fn set_mode_line_buffered() -> Result<(), anyhow::Error> {
196        let mut termios = mem::MaybeUninit::<termios>::uninit();
197        io_result(unsafe { ::libc::tcgetattr(0, termios.as_mut_ptr()) })?;
198        let mut termios = unsafe { termios.assume_init() };
199
200        set_line_buffering(&mut termios, true);
201
202        unsafe { tcsetattr(0, TCSANOW, &termios) };
203        Ok(())
204    }
205
206    fn set_line_buffering(termios: &mut termios, enabled: bool) {
207        if enabled {
208            termios.c_lflag |= ICANON;
209            termios.c_iflag |= ICRNL;
210            termios.c_iflag &= !(INLCR | IGNCR);
211        } else {
212            termios.c_lflag &= !ICANON;
213            // Preserve carriage returns so applications can distinguish Enter (CR)
214            // from line feed, which is commonly used for Shift+Enter.
215            termios.c_iflag &= !(ICRNL | INLCR | IGNCR);
216        }
217    }
218
219    pub fn set_mode_no_line_feeds() -> Result<(), anyhow::Error> {
220        let mut termios = mem::MaybeUninit::<termios>::uninit();
221        io_result(unsafe { ::libc::tcgetattr(0, termios.as_mut_ptr()) })?;
222        let mut termios = unsafe { termios.assume_init() };
223
224        termios.c_lflag &= !ONLCR;
225
226        unsafe { tcsetattr(0, TCSANOW, &termios) };
227        Ok(())
228    }
229
230    pub fn set_mode_line_feeds() -> Result<(), anyhow::Error> {
231        let mut termios = mem::MaybeUninit::<termios>::uninit();
232        io_result(unsafe { ::libc::tcgetattr(0, termios.as_mut_ptr()) })?;
233        let mut termios = unsafe { termios.assume_init() };
234
235        termios.c_lflag |= ONLCR;
236
237        unsafe { tcsetattr(0, TCSANOW, &termios) };
238        Ok(())
239    }
240
241    #[cfg(test)]
242    mod tests {
243        use super::*;
244
245        fn blank_termios() -> termios {
246            // SAFETY: libc::termios is a plain C data structure for which an all-zero
247            // value is valid; the tests only inspect and update its flag fields.
248            unsafe { mem::zeroed() }
249        }
250
251        #[test]
252        fn noncanonical_input_preserves_carriage_returns_and_line_feeds() {
253            let mut state = blank_termios();
254            state.c_lflag = ICANON | ECHO | ISIG;
255            state.c_iflag = ICRNL | INLCR | IGNCR | IXON;
256            state.c_oflag = OPOST;
257
258            set_line_buffering(&mut state, false);
259
260            assert_eq!(state.c_lflag & ICANON, 0);
261            assert_eq!(state.c_iflag & (ICRNL | INLCR | IGNCR), 0);
262            assert_ne!(state.c_lflag & ECHO, 0);
263            assert_ne!(state.c_lflag & ISIG, 0);
264            assert_ne!(state.c_iflag & IXON, 0);
265            assert_ne!(state.c_oflag & OPOST, 0);
266        }
267
268        #[test]
269        fn cooked_input_translates_carriage_returns_to_newlines() {
270            let mut state = blank_termios();
271            state.c_iflag = INLCR | IGNCR | IXON;
272
273            set_line_buffering(&mut state, true);
274
275            assert_ne!(state.c_lflag & ICANON, 0);
276            assert_ne!(state.c_iflag & ICRNL, 0);
277            assert_eq!(state.c_iflag & (INLCR | IGNCR), 0);
278            assert_ne!(state.c_iflag & IXON, 0);
279        }
280
281        fn read_exact_with_timeout(fd: c_int, output: &mut [u8]) {
282            let mut offset = 0;
283            while offset < output.len() {
284                let mut descriptor = libc::pollfd {
285                    fd,
286                    events: libc::POLLIN,
287                    revents: 0,
288                };
289                assert_eq!(
290                    unsafe { libc::poll(&mut descriptor, 1, 1_000) },
291                    1,
292                    "timed out waiting for PTY input"
293                );
294                assert_ne!(descriptor.revents & libc::POLLIN, 0);
295
296                let read = unsafe {
297                    libc::read(
298                        fd,
299                        output[offset..].as_mut_ptr().cast(),
300                        output.len() - offset,
301                    )
302                };
303                assert!(read > 0, "failed to read PTY input");
304                offset += read as usize;
305            }
306        }
307
308        #[test]
309        fn noncanonical_pty_input_distinguishes_enter_from_line_feed() {
310            let mut master = -1;
311            let mut slave = -1;
312            assert_eq!(
313                unsafe {
314                    libc::openpty(
315                        &mut master,
316                        &mut slave,
317                        std::ptr::null_mut(),
318                        std::ptr::null_mut(),
319                        std::ptr::null_mut(),
320                    )
321                },
322                0
323            );
324
325            struct Pty {
326                master: c_int,
327                slave: c_int,
328            }
329
330            impl Drop for Pty {
331                fn drop(&mut self) {
332                    unsafe {
333                        libc::close(self.master);
334                        libc::close(self.slave);
335                    }
336                }
337            }
338
339            let pty = Pty { master, slave };
340            let mut state = blank_termios();
341            assert_eq!(unsafe { libc::tcgetattr(pty.slave, &mut state) }, 0);
342            state.c_iflag |= ICRNL | INLCR | IGNCR;
343            set_line_buffering(&mut state, false);
344            assert_eq!(unsafe { libc::tcsetattr(pty.slave, TCSANOW, &state) }, 0);
345
346            let input = [b'\r', b'\n'];
347            assert_eq!(
348                unsafe { libc::write(pty.master, input.as_ptr().cast(), input.len()) },
349                input.len() as isize
350            );
351
352            let mut output = [0_u8; 2];
353            read_exact_with_timeout(pty.slave, &mut output);
354            assert_eq!(output, input);
355
356            assert_eq!(unsafe { libc::tcgetattr(pty.slave, &mut state) }, 0);
357            set_line_buffering(&mut state, true);
358            assert_eq!(unsafe { libc::tcsetattr(pty.slave, TCSANOW, &state) }, 0);
359            let input = [b'\r'];
360            assert_eq!(
361                unsafe { libc::write(pty.master, input.as_ptr().cast(), input.len()) },
362                1
363            );
364            let mut output = [0_u8];
365            read_exact_with_timeout(pty.slave, &mut output);
366            assert_eq!(output, [b'\n']);
367        }
368    }
369}
370
371#[cfg(any(not(unix), target_os = "ios"))]
372mod sys {
373    pub fn reset() -> Result<(), anyhow::Error> {
374        Ok(())
375    }
376
377    pub fn is_stdin_tty() -> bool {
378        false
379    }
380
381    pub fn is_stdout_tty() -> bool {
382        false
383    }
384
385    pub fn is_stderr_tty() -> bool {
386        false
387    }
388
389    pub fn is_mode_echo() -> bool {
390        true
391    }
392
393    pub fn is_mode_line_buffering() -> bool {
394        true
395    }
396
397    pub fn is_mode_line_feeds() -> bool {
398        true
399    }
400
401    pub fn set_mode_no_echo() -> Result<(), anyhow::Error> {
402        Ok(())
403    }
404
405    pub fn set_mode_echo() -> Result<(), anyhow::Error> {
406        Ok(())
407    }
408
409    pub fn set_mode_no_line_buffered() -> Result<(), anyhow::Error> {
410        Ok(())
411    }
412
413    pub fn set_mode_line_buffered() -> Result<(), anyhow::Error> {
414        Ok(())
415    }
416
417    pub fn set_mode_no_line_feeds() -> Result<(), anyhow::Error> {
418        Ok(())
419    }
420
421    pub fn set_mode_line_feeds() -> Result<(), anyhow::Error> {
422        Ok(())
423    }
424}