wasmer_wasix/os/tty/
mod.rs

1use std::{
2    borrow::Cow,
3    sync::{Arc, Mutex},
4};
5
6use futures::future::BoxFuture;
7use virtual_fs::{AsyncWriteExt, NullFile, VirtualFile};
8use wasmer_wasix_types::wasi::{Signal, Snapshot0Clockid};
9
10use crate::syscalls::platform_clock_time_get;
11
12use super::task::signal::SignalHandlerAbi;
13
14const TTY_MOBILE_PAUSE: u128 = std::time::Duration::from_millis(200).as_nanos();
15
16pub mod tty_sys;
17
18#[derive(Debug)]
19pub enum InputEvent {
20    Key,
21    Data(String),
22    Raw(Vec<u8>),
23}
24
25#[derive(Clone, Debug)]
26pub struct ConsoleRect {
27    pub cols: u32,
28    pub rows: u32,
29}
30
31impl Default for ConsoleRect {
32    fn default() -> Self {
33        Self { cols: 80, rows: 25 }
34    }
35}
36
37#[derive(Clone, Debug)]
38pub struct TtyOptionsInner {
39    echo: bool,
40    line_buffering: bool,
41    line_feeds: bool,
42    rect: ConsoleRect,
43}
44
45#[derive(Debug, Clone)]
46pub struct TtyOptions {
47    inner: Arc<Mutex<TtyOptionsInner>>,
48}
49
50impl Default for TtyOptions {
51    fn default() -> Self {
52        Self {
53            inner: Arc::new(Mutex::new(TtyOptionsInner {
54                echo: true,
55                line_buffering: true,
56                line_feeds: true,
57                rect: ConsoleRect { cols: 80, rows: 25 },
58            })),
59        }
60    }
61}
62
63impl TtyOptions {
64    pub fn cols(&self) -> u32 {
65        let inner = self.inner.lock().unwrap();
66        inner.rect.cols
67    }
68
69    pub fn set_cols(&self, cols: u32) {
70        let mut inner = self.inner.lock().unwrap();
71        inner.rect.cols = cols;
72    }
73
74    pub fn rows(&self) -> u32 {
75        let inner = self.inner.lock().unwrap();
76        inner.rect.rows
77    }
78
79    pub fn set_rows(&self, rows: u32) {
80        let mut inner = self.inner.lock().unwrap();
81        inner.rect.rows = rows;
82    }
83
84    pub fn echo(&self) -> bool {
85        let inner = self.inner.lock().unwrap();
86        inner.echo
87    }
88
89    pub fn set_echo(&self, echo: bool) {
90        let mut inner = self.inner.lock().unwrap();
91        inner.echo = echo;
92    }
93
94    pub fn line_buffering(&self) -> bool {
95        let inner = self.inner.lock().unwrap();
96        inner.line_buffering
97    }
98
99    pub fn set_line_buffering(&self, line_buffering: bool) {
100        let mut inner = self.inner.lock().unwrap();
101        inner.line_buffering = line_buffering;
102    }
103
104    pub fn line_feeds(&self) -> bool {
105        let inner = self.inner.lock().unwrap();
106        inner.line_feeds
107    }
108
109    pub fn set_line_feeds(&self, line_feeds: bool) {
110        let mut inner = self.inner.lock().unwrap();
111        inner.line_feeds = line_feeds;
112    }
113}
114
115#[derive(Debug)]
116pub struct Tty {
117    stdin: Box<dyn VirtualFile + Send + Sync + 'static>,
118    stdout: Box<dyn VirtualFile + Send + Sync + 'static>,
119    signaler: Option<Box<dyn SignalHandlerAbi + Send + Sync + 'static>>,
120    is_mobile: bool,
121    last: Option<(String, u128)>,
122    options: TtyOptions,
123    line: String,
124}
125
126impl Tty {
127    pub fn new(
128        stdin: Box<dyn VirtualFile + Send + Sync + 'static>,
129        stdout: Box<dyn VirtualFile + Send + Sync + 'static>,
130        is_mobile: bool,
131        options: TtyOptions,
132    ) -> Self {
133        Self {
134            stdin,
135            stdout,
136            signaler: None,
137            last: None,
138            options,
139            is_mobile,
140            line: String::new(),
141        }
142    }
143
144    pub fn stdin(&self) -> &(dyn VirtualFile + Send + Sync + 'static) {
145        self.stdin.as_ref()
146    }
147
148    pub fn stdin_mut(&mut self) -> &mut (dyn VirtualFile + Send + Sync + 'static) {
149        self.stdin.as_mut()
150    }
151
152    pub fn stdin_replace(
153        &mut self,
154        mut stdin: Box<dyn VirtualFile + Send + Sync + 'static>,
155    ) -> Box<dyn VirtualFile + Send + Sync + 'static> {
156        std::mem::swap(&mut self.stdin, &mut stdin);
157        stdin
158    }
159
160    pub fn stdin_take(&mut self) -> Box<dyn VirtualFile + Send + Sync + 'static> {
161        let mut stdin: Box<dyn VirtualFile + Send + Sync + 'static> = Box::<NullFile>::default();
162        std::mem::swap(&mut self.stdin, &mut stdin);
163        stdin
164    }
165
166    pub fn options(&self) -> TtyOptions {
167        self.options.clone()
168    }
169
170    pub fn set_signaler(&mut self, signaler: Box<dyn SignalHandlerAbi + Send + Sync + 'static>) {
171        self.signaler.replace(signaler);
172    }
173
174    pub fn on_event(mut self, event: InputEvent) -> BoxFuture<'static, Self> {
175        Box::pin(async move {
176            match event {
177                InputEvent::Key => {
178                    // do nothing
179                    self
180                }
181                InputEvent::Data(data) => {
182                    // Due to a nasty bug in xterm.js on Android mobile it sends the keys you press
183                    // twice in a row with a short interval between - this hack will avoid that bug
184                    if self.is_mobile {
185                        let now = platform_clock_time_get(Snapshot0Clockid::Monotonic, 1_000_000)
186                            .unwrap() as u128;
187                        if let Some((what, when)) = self.last.as_ref()
188                            && what.as_str() == data
189                            && now - *when < TTY_MOBILE_PAUSE
190                        {
191                            self.last = None;
192                            return self;
193                        }
194                        self.last = Some((data.clone(), now))
195                    }
196
197                    self.on_data(data.as_bytes().to_vec().into()).await
198                }
199                InputEvent::Raw(data) => self.on_data(data.into()).await,
200            }
201        })
202    }
203
204    fn on_enter(mut self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
205        Box::pin(async move {
206            // Add a line feed on the end and take the line
207            let mut data = self.line.clone();
208            self.line.clear();
209            data.push('\n');
210
211            // If echo is on then write a new line
212            {
213                let echo = {
214                    let options = self.options.inner.lock().unwrap();
215                    options.echo
216                };
217                if echo {
218                    let _ = self.stdout.write("\n".as_bytes()).await;
219                }
220            }
221
222            // Send the data to the process
223            let _ = self.stdin.write(data.as_bytes()).await;
224            self
225        })
226    }
227
228    fn on_ctrl_c(mut self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
229        Box::pin(async move {
230            if let Some(signaler) = self.signaler.as_ref() {
231                signaler.signal(Signal::Sigint as u8).ok();
232
233                let (echo, _line_buffering) = {
234                    let options = self.options.inner.lock().unwrap();
235                    (options.echo, options.line_buffering)
236                };
237
238                self.line.clear();
239                if echo {
240                    let _ = self.stdout.write("\n".as_bytes()).await;
241                }
242            }
243            self
244        })
245    }
246
247    fn on_backspace(mut self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
248        // Remove a character (if there are none left we are done)
249        if self.line.is_empty() {
250            return Box::pin(async move { self });
251        }
252        let len = self.line.len();
253        self.line = self.line[..len - 1].to_string();
254
255        Box::pin(async move {
256            // If echo is on then write the backspace
257            {
258                let echo = {
259                    let options = self.options.inner.lock().unwrap();
260                    options.echo
261                };
262                if echo {
263                    let _ = self.stdout.write("\u{0008} \u{0008}".as_bytes()).await;
264                }
265            }
266            self
267        })
268    }
269
270    fn on_tab(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
271        Box::pin(async move { self })
272    }
273
274    fn on_cursor_left(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
275        Box::pin(async move { self })
276    }
277
278    fn on_cursor_right(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
279        Box::pin(async move { self })
280    }
281
282    fn on_cursor_up(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
283        Box::pin(async move { self })
284    }
285
286    fn on_cursor_down(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
287        Box::pin(async move { self })
288    }
289
290    fn on_home(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
291        Box::pin(async move { self })
292    }
293
294    fn on_end(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
295        Box::pin(async move { self })
296    }
297
298    fn on_ctrl_l(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
299        Box::pin(async move { self })
300    }
301
302    fn on_page_up(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
303        Box::pin(async move { self })
304    }
305
306    fn on_page_down(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
307        Box::pin(async move { self })
308    }
309
310    fn on_f1(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
311        Box::pin(async move { self })
312    }
313
314    fn on_f2(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
315        Box::pin(async move { self })
316    }
317
318    fn on_f3(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
319        Box::pin(async move { self })
320    }
321
322    fn on_f4(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
323        Box::pin(async move { self })
324    }
325
326    fn on_f5(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
327        Box::pin(async move { self })
328    }
329
330    fn on_f6(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
331        Box::pin(async move { self })
332    }
333
334    fn on_f7(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
335        Box::pin(async move { self })
336    }
337
338    fn on_f8(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
339        Box::pin(async move { self })
340    }
341
342    fn on_f9(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
343        Box::pin(async move { self })
344    }
345
346    fn on_f10(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
347        Box::pin(async move { self })
348    }
349
350    fn on_f11(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
351        Box::pin(async move { self })
352    }
353
354    fn on_f12(self, _data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
355        Box::pin(async move { self })
356    }
357
358    fn on_data(mut self, data: Cow<'static, [u8]>) -> BoxFuture<'static, Self> {
359        // If we are line buffering then we need to check for some special cases
360        let options = { self.options.inner.lock().unwrap().clone() };
361        if options.line_buffering {
362            let echo = options.echo;
363            return match String::from_utf8_lossy(data.as_ref()).as_ref() {
364                "\r" | "\u{000A}" => self.on_enter(data),
365                "\u{0003}" => self.on_ctrl_c(data),
366                "\u{007F}" => self.on_backspace(data),
367                "\u{0009}" => self.on_tab(data),
368                "\u{001B}\u{005B}\u{0044}" => self.on_cursor_left(data),
369                "\u{001B}\u{005B}\u{0043}" => self.on_cursor_right(data),
370                "\u{0001}" | "\u{001B}\u{005B}\u{0048}" => self.on_home(data),
371                "\u{001B}\u{005B}\u{0046}" => self.on_end(data),
372                "\u{001B}\u{005B}\u{0041}" => self.on_cursor_up(data),
373                "\u{001B}\u{005B}\u{0042}" => self.on_cursor_down(data),
374                "\u{000C}" => self.on_ctrl_l(data),
375                "\u{001B}\u{005B}\u{0035}\u{007E}" => self.on_page_up(data),
376                "\u{001B}\u{005B}\u{0036}\u{007E}" => self.on_page_down(data),
377                "\u{001B}\u{004F}\u{0050}" => self.on_f1(data),
378                "\u{001B}\u{004F}\u{0051}" => self.on_f2(data),
379                "\u{001B}\u{004F}\u{0052}" => self.on_f3(data),
380                "\u{001B}\u{004F}\u{0053}" => self.on_f4(data),
381                "\u{001B}\u{005B}\u{0031}\u{0035}\u{007E}" => self.on_f5(data),
382                "\u{001B}\u{005B}\u{0031}\u{0037}\u{007E}" => self.on_f6(data),
383                "\u{001B}\u{005B}\u{0031}\u{0038}\u{007E}" => self.on_f7(data),
384                "\u{001B}\u{005B}\u{0031}\u{0039}\u{007E}" => self.on_f8(data),
385                "\u{001B}\u{005B}\u{0032}\u{0030}\u{007E}" => self.on_f9(data),
386                "\u{001B}\u{005B}\u{0032}\u{0031}\u{007E}" => self.on_f10(data),
387                "\u{001B}\u{005B}\u{0032}\u{0033}\u{007E}" => self.on_f11(data),
388                "\u{001B}\u{005B}\u{0032}\u{0034}\u{007E}" => self.on_f12(data),
389                _ => Box::pin(async move {
390                    if echo {
391                        let _ = self.stdout.write(data.as_ref()).await;
392                    }
393                    self.line
394                        .push_str(String::from_utf8_lossy(data.as_ref()).as_ref());
395                    self
396                }),
397            };
398        };
399
400        Box::pin(async move {
401            // If the echo is enabled then write it to the terminal
402            if options.echo {
403                // TODO: log / propagate error?
404                let _ = self.stdout.write(data.as_ref()).await;
405            }
406
407            // Now send it to the process
408            let _ = self.stdin.write(data.as_ref()).await;
409            self
410        })
411    }
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
415pub struct WasiTtyState {
416    pub cols: u32,
417    pub rows: u32,
418    pub width: u32,
419    pub height: u32,
420    pub stdin_tty: bool,
421    pub stdout_tty: bool,
422    pub stderr_tty: bool,
423    pub echo: bool,
424    pub line_buffered: bool,
425    pub line_feeds: bool,
426}
427
428impl Default for WasiTtyState {
429    fn default() -> Self {
430        Self {
431            rows: 80,
432            cols: 25,
433            width: 800,
434            height: 600,
435            stdin_tty: true,
436            stdout_tty: true,
437            stderr_tty: true,
438            echo: false,
439            line_buffered: false,
440            line_feeds: true,
441        }
442    }
443}
444
445/// Provides access to a TTY.
446pub trait TtyBridge: std::fmt::Debug {
447    /// Resets the values
448    fn reset(&self);
449
450    /// Retrieve the current TTY state.
451    fn tty_get(&self) -> WasiTtyState;
452
453    /// Set the TTY state.
454    fn tty_set(&self, _tty_state: WasiTtyState);
455}