deno_runtime/ops/
tty.rs

1// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2
3use std::io::Error;
4
5use deno_core::op2;
6use deno_core::OpState;
7use rustyline::config::Configurer;
8use rustyline::error::ReadlineError;
9use rustyline::Cmd;
10use rustyline::Editor;
11use rustyline::KeyCode;
12use rustyline::KeyEvent;
13use rustyline::Modifiers;
14
15#[cfg(windows)]
16use deno_core::parking_lot::Mutex;
17#[cfg(windows)]
18use deno_io::WinTtyState;
19#[cfg(windows)]
20use std::sync::Arc;
21
22#[cfg(unix)]
23use deno_core::ResourceId;
24#[cfg(unix)]
25use nix::sys::termios;
26#[cfg(unix)]
27use std::cell::RefCell;
28#[cfg(unix)]
29use std::collections::HashMap;
30
31#[cfg(unix)]
32#[derive(Default, Clone)]
33struct TtyModeStore(
34  std::rc::Rc<RefCell<HashMap<ResourceId, termios::Termios>>>,
35);
36
37#[cfg(unix)]
38impl TtyModeStore {
39  pub fn get(&self, id: ResourceId) -> Option<termios::Termios> {
40    self.0.borrow().get(&id).map(ToOwned::to_owned)
41  }
42
43  pub fn take(&self, id: ResourceId) -> Option<termios::Termios> {
44    self.0.borrow_mut().remove(&id)
45  }
46
47  pub fn set(&self, id: ResourceId, mode: termios::Termios) {
48    self.0.borrow_mut().insert(id, mode);
49  }
50}
51
52#[cfg(windows)]
53use winapi::shared::minwindef::DWORD;
54#[cfg(windows)]
55use winapi::um::wincon;
56
57deno_core::extension!(
58  deno_tty,
59  ops = [op_set_raw, op_console_size, op_read_line_prompt],
60  state = |state| {
61    #[cfg(unix)]
62    state.put(TtyModeStore::default());
63  },
64);
65
66#[derive(Debug, thiserror::Error)]
67pub enum TtyError {
68  #[error(transparent)]
69  Resource(deno_core::error::AnyError),
70  #[error("{0}")]
71  Io(#[from] std::io::Error),
72  #[cfg(unix)]
73  #[error(transparent)]
74  Nix(nix::Error),
75  #[error(transparent)]
76  Other(deno_core::error::AnyError),
77}
78
79// ref: <https://learn.microsoft.com/en-us/windows/console/setconsolemode>
80#[cfg(windows)]
81const COOKED_MODE: DWORD =
82  // enable line-by-line input (returns input only after CR is read)
83  wincon::ENABLE_LINE_INPUT
84  // enables real-time character echo to console display (requires ENABLE_LINE_INPUT)
85  | wincon::ENABLE_ECHO_INPUT
86  // system handles CTRL-C (with ENABLE_LINE_INPUT, also handles BS, CR, and LF) and other control keys (when using `ReadFile` or `ReadConsole`)
87  | wincon::ENABLE_PROCESSED_INPUT;
88
89#[cfg(windows)]
90fn mode_raw_input_on(original_mode: DWORD) -> DWORD {
91  original_mode & !COOKED_MODE | wincon::ENABLE_VIRTUAL_TERMINAL_INPUT
92}
93
94#[cfg(windows)]
95fn mode_raw_input_off(original_mode: DWORD) -> DWORD {
96  original_mode & !wincon::ENABLE_VIRTUAL_TERMINAL_INPUT | COOKED_MODE
97}
98
99#[op2(fast)]
100fn op_set_raw(
101  state: &mut OpState,
102  rid: u32,
103  is_raw: bool,
104  cbreak: bool,
105) -> Result<(), TtyError> {
106  let handle_or_fd = state
107    .resource_table
108    .get_fd(rid)
109    .map_err(TtyError::Resource)?;
110
111  // From https://github.com/kkawakam/rustyline/blob/master/src/tty/windows.rs
112  // and https://github.com/kkawakam/rustyline/blob/master/src/tty/unix.rs
113  // and https://github.com/crossterm-rs/crossterm/blob/e35d4d2c1cc4c919e36d242e014af75f6127ab50/src/terminal/sys/windows.rs
114  // Copyright (c) 2015 Katsu Kawakami & Rustyline authors. MIT license.
115  // Copyright (c) 2019 Timon. MIT license.
116  #[cfg(windows)]
117  {
118    use winapi::shared::minwindef::FALSE;
119
120    use winapi::um::consoleapi;
121
122    let handle = handle_or_fd;
123
124    if cbreak {
125      return Err(TtyError::Other(deno_core::error::not_supported()));
126    }
127
128    let mut original_mode: DWORD = 0;
129    // SAFETY: winapi call
130    if unsafe { consoleapi::GetConsoleMode(handle, &mut original_mode) }
131      == FALSE
132    {
133      return Err(TtyError::Io(Error::last_os_error()));
134    }
135
136    let new_mode = if is_raw {
137      mode_raw_input_on(original_mode)
138    } else {
139      mode_raw_input_off(original_mode)
140    };
141
142    let stdin_state = state.borrow::<Arc<Mutex<WinTtyState>>>();
143    let mut stdin_state = stdin_state.lock();
144
145    if stdin_state.reading {
146      let cvar = stdin_state.cvar.clone();
147
148      /* Trick to unblock an ongoing line-buffered read operation if not already pending.
149      See https://github.com/libuv/libuv/pull/866 for prior art */
150      if original_mode & COOKED_MODE != 0 && !stdin_state.cancelled {
151        // SAFETY: Write enter key event to force the console wait to return.
152        let record = unsafe {
153          let mut record: wincon::INPUT_RECORD = std::mem::zeroed();
154          record.EventType = wincon::KEY_EVENT;
155          record.Event.KeyEvent_mut().wVirtualKeyCode =
156            winapi::um::winuser::VK_RETURN as u16;
157          record.Event.KeyEvent_mut().bKeyDown = 1;
158          record.Event.KeyEvent_mut().wRepeatCount = 1;
159          *record.Event.KeyEvent_mut().uChar.UnicodeChar_mut() = '\r' as u16;
160          record.Event.KeyEvent_mut().dwControlKeyState = 0;
161          record.Event.KeyEvent_mut().wVirtualScanCode =
162            winapi::um::winuser::MapVirtualKeyW(
163              winapi::um::winuser::VK_RETURN as u32,
164              winapi::um::winuser::MAPVK_VK_TO_VSC,
165            ) as u16;
166          record
167        };
168        stdin_state.cancelled = true;
169
170        // SAFETY: winapi call to open conout$ and save screen state.
171        let active_screen_buffer = unsafe {
172          /* Save screen state before sending the VK_RETURN event */
173          let handle = winapi::um::fileapi::CreateFileW(
174            "conout$"
175              .encode_utf16()
176              .chain(Some(0))
177              .collect::<Vec<_>>()
178              .as_ptr(),
179            winapi::um::winnt::GENERIC_READ | winapi::um::winnt::GENERIC_WRITE,
180            winapi::um::winnt::FILE_SHARE_READ
181              | winapi::um::winnt::FILE_SHARE_WRITE,
182            std::ptr::null_mut(),
183            winapi::um::fileapi::OPEN_EXISTING,
184            0,
185            std::ptr::null_mut(),
186          );
187
188          let mut active_screen_buffer = std::mem::zeroed();
189          winapi::um::wincon::GetConsoleScreenBufferInfo(
190            handle,
191            &mut active_screen_buffer,
192          );
193          winapi::um::handleapi::CloseHandle(handle);
194          active_screen_buffer
195        };
196        stdin_state.screen_buffer_info = Some(active_screen_buffer);
197
198        // SAFETY: winapi call to write the VK_RETURN event.
199        if unsafe {
200          winapi::um::wincon::WriteConsoleInputW(handle, &record, 1, &mut 0)
201        } == FALSE
202        {
203          return Err(TtyError::Io(Error::last_os_error()));
204        }
205
206        /* Wait for read thread to acknowledge the cancellation to ensure that nothing
207        interferes with the screen state.
208        NOTE: `wait_while` automatically unlocks stdin_state */
209        cvar.wait_while(&mut stdin_state, |state: &mut WinTtyState| {
210          state.cancelled
211        });
212      }
213    }
214
215    // SAFETY: winapi call
216    if unsafe { consoleapi::SetConsoleMode(handle, new_mode) } == FALSE {
217      return Err(TtyError::Io(Error::last_os_error()));
218    }
219
220    Ok(())
221  }
222  #[cfg(unix)]
223  {
224    fn prepare_stdio() {
225      // SAFETY: Save current state of stdio and restore it when we exit.
226      unsafe {
227        use libc::atexit;
228        use libc::tcgetattr;
229        use libc::tcsetattr;
230        use libc::termios;
231        use once_cell::sync::OnceCell;
232
233        // Only save original state once.
234        static ORIG_TERMIOS: OnceCell<Option<termios>> = OnceCell::new();
235        ORIG_TERMIOS.get_or_init(|| {
236          let mut termios = std::mem::zeroed::<termios>();
237          if tcgetattr(libc::STDIN_FILENO, &mut termios) == 0 {
238            extern "C" fn reset_stdio() {
239              // SAFETY: Reset the stdio state.
240              unsafe {
241                tcsetattr(
242                  libc::STDIN_FILENO,
243                  0,
244                  &ORIG_TERMIOS.get().unwrap().unwrap(),
245                )
246              };
247            }
248
249            atexit(reset_stdio);
250            return Some(termios);
251          }
252
253          None
254        });
255      }
256    }
257
258    prepare_stdio();
259    let tty_mode_store = state.borrow::<TtyModeStore>().clone();
260    let previous_mode = tty_mode_store.get(rid);
261
262    // SAFETY: Nix crate requires value to implement the AsFd trait
263    let raw_fd = unsafe { std::os::fd::BorrowedFd::borrow_raw(handle_or_fd) };
264
265    if is_raw {
266      let mut raw = match previous_mode {
267        Some(mode) => mode,
268        None => {
269          // Save original mode.
270          let original_mode =
271            termios::tcgetattr(raw_fd).map_err(TtyError::Nix)?;
272          tty_mode_store.set(rid, original_mode.clone());
273          original_mode
274        }
275      };
276
277      raw.input_flags &= !(termios::InputFlags::BRKINT
278        | termios::InputFlags::ICRNL
279        | termios::InputFlags::INPCK
280        | termios::InputFlags::ISTRIP
281        | termios::InputFlags::IXON);
282
283      raw.control_flags |= termios::ControlFlags::CS8;
284
285      raw.local_flags &= !(termios::LocalFlags::ECHO
286        | termios::LocalFlags::ICANON
287        | termios::LocalFlags::IEXTEN);
288      if !cbreak {
289        raw.local_flags &= !(termios::LocalFlags::ISIG);
290      }
291      raw.control_chars[termios::SpecialCharacterIndices::VMIN as usize] = 1;
292      raw.control_chars[termios::SpecialCharacterIndices::VTIME as usize] = 0;
293      termios::tcsetattr(raw_fd, termios::SetArg::TCSADRAIN, &raw)
294        .map_err(TtyError::Nix)?;
295    } else {
296      // Try restore saved mode.
297      if let Some(mode) = tty_mode_store.take(rid) {
298        termios::tcsetattr(raw_fd, termios::SetArg::TCSADRAIN, &mode)
299          .map_err(TtyError::Nix)?;
300      }
301    }
302
303    Ok(())
304  }
305}
306
307#[op2(fast)]
308fn op_console_size(
309  state: &mut OpState,
310  #[buffer] result: &mut [u32],
311) -> Result<(), TtyError> {
312  fn check_console_size(
313    state: &mut OpState,
314    result: &mut [u32],
315    rid: u32,
316  ) -> Result<(), TtyError> {
317    let fd = state
318      .resource_table
319      .get_fd(rid)
320      .map_err(TtyError::Resource)?;
321    let size = console_size_from_fd(fd)?;
322    result[0] = size.cols;
323    result[1] = size.rows;
324    Ok(())
325  }
326
327  let mut last_result = Ok(());
328  // Since stdio might be piped we try to get the size of the console for all
329  // of them and return the first one that succeeds.
330  for rid in [0, 1, 2] {
331    last_result = check_console_size(state, result, rid);
332    if last_result.is_ok() {
333      return last_result;
334    }
335  }
336
337  last_result
338}
339
340#[derive(Debug, PartialEq, Eq, Clone, Copy)]
341pub struct ConsoleSize {
342  pub cols: u32,
343  pub rows: u32,
344}
345
346pub fn console_size(
347  std_file: &std::fs::File,
348) -> Result<ConsoleSize, std::io::Error> {
349  #[cfg(windows)]
350  {
351    use std::os::windows::io::AsRawHandle;
352    let handle = std_file.as_raw_handle();
353    console_size_from_fd(handle)
354  }
355  #[cfg(unix)]
356  {
357    use std::os::unix::io::AsRawFd;
358    let fd = std_file.as_raw_fd();
359    console_size_from_fd(fd)
360  }
361}
362
363#[cfg(windows)]
364fn console_size_from_fd(
365  handle: std::os::windows::io::RawHandle,
366) -> Result<ConsoleSize, std::io::Error> {
367  // SAFETY: winapi calls
368  unsafe {
369    let mut bufinfo: winapi::um::wincon::CONSOLE_SCREEN_BUFFER_INFO =
370      std::mem::zeroed();
371
372    if winapi::um::wincon::GetConsoleScreenBufferInfo(handle, &mut bufinfo) == 0
373    {
374      return Err(Error::last_os_error());
375    }
376
377    // calculate the size of the visible window
378    // * use over/under-flow protections b/c MSDN docs only imply that srWindow components are all non-negative
379    // * ref: <https://docs.microsoft.com/en-us/windows/console/console-screen-buffer-info-str> @@ <https://archive.is/sfjnm>
380    let cols = std::cmp::max(
381      bufinfo.srWindow.Right as i32 - bufinfo.srWindow.Left as i32 + 1,
382      0,
383    ) as u32;
384    let rows = std::cmp::max(
385      bufinfo.srWindow.Bottom as i32 - bufinfo.srWindow.Top as i32 + 1,
386      0,
387    ) as u32;
388
389    Ok(ConsoleSize { cols, rows })
390  }
391}
392
393#[cfg(not(windows))]
394fn console_size_from_fd(
395  fd: std::os::unix::prelude::RawFd,
396) -> Result<ConsoleSize, std::io::Error> {
397  // SAFETY: libc calls
398  unsafe {
399    let mut size: libc::winsize = std::mem::zeroed();
400    if libc::ioctl(fd, libc::TIOCGWINSZ, &mut size as *mut _) != 0 {
401      return Err(Error::last_os_error());
402    }
403    Ok(ConsoleSize {
404      cols: size.ws_col as u32,
405      rows: size.ws_row as u32,
406    })
407  }
408}
409
410#[cfg(all(test, windows))]
411mod tests {
412  #[test]
413  fn test_winos_raw_mode_transitions() {
414    use crate::ops::tty::mode_raw_input_off;
415    use crate::ops::tty::mode_raw_input_on;
416
417    let known_off_modes =
418      [0xf7 /* Win10/CMD */, 0x1f7 /* Win10/WinTerm */];
419    let known_on_modes =
420      [0x2f0 /* Win10/CMD */, 0x3f0 /* Win10/WinTerm */];
421
422    // assert known transitions
423    assert_eq!(known_on_modes[0], mode_raw_input_on(known_off_modes[0]));
424    assert_eq!(known_on_modes[1], mode_raw_input_on(known_off_modes[1]));
425
426    // assert ON-OFF round-trip is neutral
427    assert_eq!(
428      known_off_modes[0],
429      mode_raw_input_off(mode_raw_input_on(known_off_modes[0]))
430    );
431    assert_eq!(
432      known_off_modes[1],
433      mode_raw_input_off(mode_raw_input_on(known_off_modes[1]))
434    );
435  }
436}
437
438#[op2]
439#[string]
440pub fn op_read_line_prompt(
441  #[string] prompt_text: &str,
442  #[string] default_value: &str,
443) -> Result<Option<String>, ReadlineError> {
444  let mut editor = Editor::<(), rustyline::history::DefaultHistory>::new()
445    .expect("Failed to create editor.");
446
447  editor.set_keyseq_timeout(1);
448  editor
449    .bind_sequence(KeyEvent(KeyCode::Esc, Modifiers::empty()), Cmd::Interrupt);
450
451  let read_result =
452    editor.readline_with_initial(prompt_text, (default_value, ""));
453  match read_result {
454    Ok(line) => Ok(Some(line)),
455    Err(ReadlineError::Interrupted) => {
456      // SAFETY: Disable raw mode and raise SIGINT.
457      unsafe {
458        libc::raise(libc::SIGINT);
459      }
460      Ok(None)
461    }
462    Err(ReadlineError::Eof) => Ok(None),
463    Err(err) => Err(err),
464  }
465}