From 2358bfb4c03aac86a6fae8e6a9de95e31e26be17 Mon Sep 17 00:00:00 2001 From: "Kevin (Kun) Kassimo Qian" Date: Sun, 9 Feb 2020 17:48:51 -0800 Subject: [PATCH 1/6] tty: Deno.stdin.setRaw() --- cli/js/dispatch.ts | 1 + cli/js/files.ts | 28 ++++++- cli/js/lib.deno.ns.d.ts | 12 ++- cli/ops/mod.rs | 1 + cli/ops/tty.rs | 170 ++++++++++++++++++++++++++++++++++++++++ cli/worker.rs | 1 + 6 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 cli/ops/tty.rs diff --git a/cli/js/dispatch.ts b/cli/js/dispatch.ts index 1a6b6528db368f..fc5edb4ba3ed60 100644 --- a/cli/js/dispatch.ts +++ b/cli/js/dispatch.ts @@ -74,6 +74,7 @@ export let OP_TRANSPILE: number; export let OP_SIGNAL_BIND: number; export let OP_SIGNAL_UNBIND: number; export let OP_SIGNAL_POLL: number; +export let OP_SET_RAW: number; /** **WARNING:** This is only available during the snapshotting process and is * unavailable at runtime. */ diff --git a/cli/js/files.ts b/cli/js/files.ts index c966b9fa04699b..9ce997737f666e 100644 --- a/cli/js/files.ts +++ b/cli/js/files.ts @@ -14,7 +14,8 @@ import { sendAsyncMinimal, sendSyncMinimal } from "./dispatch_minimal.ts"; import * as dispatch from "./dispatch.ts"; import { sendSync as sendSyncJson, - sendAsync as sendAsyncJson + sendAsync as sendAsyncJson, + sendSync } from "./dispatch_json.ts"; /** Open a file and return an instance of the `File` object @@ -256,8 +257,29 @@ export class File } } -/** An instance of `File` for stdin. */ -export const stdin = new File(0); +export type RestoreModeFunc = () => void; +/** Extended file abstraction with setRaw() */ +export class Stdin extends File { + /** Set input mode to raw (non-canonical). + * Returns a function that when called, restores previous mode. + */ + setRaw(): RestoreModeFunc { + const restoreInfo = sendSync(dispatch.OP_SET_RAW, { + rid: this.rid, + raw: true + }); + return (): void => { + sendSync(dispatch.OP_SET_RAW, { + rid: this.rid, + raw: false, + ...restoreInfo + }); + }; + } +} + +/** An instance of `Stdin` for stdin. */ +export const stdin = new Stdin(0); /** An instance of `File` for stdout. */ export const stdout = new File(1); /** An instance of `File` for stderr. */ diff --git a/cli/js/lib.deno.ns.d.ts b/cli/js/lib.deno.ns.d.ts index 4bbbf632028d4d..7f2e603a2d44c6 100644 --- a/cli/js/lib.deno.ns.d.ts +++ b/cli/js/lib.deno.ns.d.ts @@ -460,8 +460,16 @@ declare namespace Deno { seekSync(offset: number, whence: SeekMode): void; close(): void; } - /** An instance of `File` for stdin. */ - export const stdin: File; + export type RestoreModeFunc = () => void; + /** Extended file abstraction with setRaw() */ + export class Stdin extends File { + /** Set input mode to raw (non-canonical). + * Returns a function that when called, restores previous mode. + */ + setRaw(): RestoreModeFunc; + } + /** An instance of `Stdin` for stdin. */ + export const stdin: Stdin; /** An instance of `File` for stdout. */ export const stdout: File; /** An instance of `File` for stderr. */ diff --git a/cli/ops/mod.rs b/cli/ops/mod.rs index dd772cd9a31970..70c9e1e9a3ec29 100644 --- a/cli/ops/mod.rs +++ b/cli/ops/mod.rs @@ -27,5 +27,6 @@ pub mod runtime_compiler; pub mod signal; pub mod timers; pub mod tls; +pub mod tty; pub mod web_worker; pub mod worker_host; diff --git a/cli/ops/tty.rs b/cli/ops/tty.rs new file mode 100644 index 00000000000000..3cbe172c5d3340 --- /dev/null +++ b/cli/ops/tty.rs @@ -0,0 +1,170 @@ +use super::dispatch_json::JsonOp; +use super::io::StreamResource; +use crate::deno_error::bad_resource; +use crate::deno_error::other_error; +use crate::ops::json_op; +use crate::state::State; +use deno_core::*; +use nix::sys::termios; +use serde_derive::{Deserialize, Serialize}; +use serde_json::Value; + +pub fn init(i: &mut Isolate, s: &State) { + i.register_op("set_raw", s.core_op(json_op(s.stateful_op(op_set_raw)))); +} + +#[cfg(windows)] +macro_rules! wincheck { + ($funcall:expr) => {{ + let rc = unsafe { $funcall }; + if rc == 0 { + Err(std::io::Error::last_os_error())?; + } + rc + }}; +} + +// libc::termios cannot be serialized. +// Create a similar one for our use. +#[derive(Serialize, Deserialize)] +struct SerializedTermios { + iflags: u64, + oflags: u64, + cflags: u64, + lflags: u64, + cc: [u8; libc::NCCS], +} + +impl From for SerializedTermios { + fn from(t: termios::Termios) -> Self { + Self { + iflags: t.input_flags.bits(), + oflags: t.output_flags.bits(), + cflags: t.control_flags.bits(), + lflags: t.local_flags.bits(), + cc: t.control_chars, + } + } +} + +impl Into for SerializedTermios { + fn into(self) -> termios::Termios { + let mut t = unsafe { termios::Termios::default_uninit() }; + t.input_flags = termios::InputFlags::from_bits_truncate(self.iflags); + t.output_flags = termios::OutputFlags::from_bits_truncate(self.oflags); + t.control_flags = termios::ControlFlags::from_bits_truncate(self.cflags); + t.local_flags = termios::LocalFlags::from_bits_truncate(self.lflags); + t.control_chars = self.cc; + t + } +} + +#[derive(Deserialize)] +struct SetRawArgs { + rid: u32, + raw: bool, + #[cfg(not(windows))] + restore: Option, // Only used for *nix + // Saved as string in case of u64 problem in JS +} + +pub fn op_set_raw( + state_: &State, + args: Value, + _zero_copy: Option, +) -> Result { + let args: SetRawArgs = serde_json::from_value(args)?; + let rid = args.rid; + let is_raw = args.raw; + + let state = state_.borrow_mut(); + let resource = state.resource_table.get::(rid); + if resource.is_none() { + return Err(bad_resource()); + } + + // For now, only stdin + match resource.unwrap() { + StreamResource::Stdin(_) => (), + _ => { + return Err(other_error( + "Resource other than stdin is not implemented".to_owned(), + )) + } + } + + // From https://github.com/kkawakam/rustyline/blob/master/src/tty/windows.rs + // and https://github.com/crossterm-rs/crossterm/blob/e35d4d2c1cc4c919e36d242e014af75f6127ab50/src/terminal/sys/windows.rs + #[cfg(windows)] + { + use std::os::windows::io::AsRawHandle; + use winapi::um::{consoleapi, handleapi, winbase, wincon, winuser}; + + let handle = std::io::stdin().as_raw_handle(); + if handle == handleapi::INVALID_HANDLE_VALUE { + return Err(std::io::Error::last_os_error()); + } else if handle.is_null() { + return Err(other_error("null handle".to_owned())); + } + let mut original_mode = 0; + let RAW_MODE_MASK = + ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT; + wincheck!(consoleapi::GetConsoleMode(handle, &mut original_mode)); + let new_mode = if is_raw { + original_mode & !RAW_MODE_MASK; + } else { + original_mode | RAW_MODE_MASK; + }; + wincheck!(consoleapi::SetConsoleMode(handle, new_mode)); + + return Ok(JsonOp::Sync(json!({}))); + } + // From https://github.com/kkawakam/rustyline/blob/master/src/tty/unix.rs + #[cfg(not(windows))] + { + use std::os::unix::io::AsRawFd; + let raw_fd = std::io::stdin().as_raw_fd(); + + if is_raw { + let original_mode = termios::tcgetattr(raw_fd)?; + let mut raw = original_mode.clone(); + + raw.input_flags &= !(termios::InputFlags::BRKINT + | termios::InputFlags::ICRNL + | termios::InputFlags::INPCK + | termios::InputFlags::ISTRIP + | termios::InputFlags::IXON); + + raw.control_flags |= termios::ControlFlags::CS8; + + raw.local_flags &= !(termios::LocalFlags::ECHO + | termios::LocalFlags::ICANON + | termios::LocalFlags::IEXTEN + | termios::LocalFlags::ISIG); + raw.control_chars[termios::SpecialCharacterIndices::VMIN as usize] = 1; + raw.control_chars[termios::SpecialCharacterIndices::VTIME as usize] = 0; + termios::tcsetattr(raw_fd, termios::SetArg::TCSADRAIN, &raw)?; + return Ok(JsonOp::Sync(json!({ + "restore": + serde_json::to_string(&SerializedTermios::from(original_mode)).unwrap(), + }))); + } else { + // Restore old mode. + if args.restore.is_none() { + return Err(other_error("no termios to restore".to_owned())); + } + let old_termios = + serde_json::from_str::(&args.restore.unwrap()); + if old_termios.is_err() { + return Err(other_error("bad termios to restore".to_owned())); + } + let old_termios = old_termios.unwrap(); + termios::tcsetattr( + raw_fd, + termios::SetArg::TCSADRAIN, + &old_termios.into(), + )?; + return Ok(JsonOp::Sync(json!({}))); + } + } +} diff --git a/cli/worker.rs b/cli/worker.rs index 20b8b802102ad1..c73963c9b5f0b4 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -225,6 +225,7 @@ impl MainWorker { ops::timers::init(isolate, &state); ops::worker_host::init(isolate, &state); ops::web_worker::init(isolate, &state); + ops::tty::init(isolate, &state); } Self(worker) } From 52e062f8cb35f3074bfab0a0b5a3f74b5734302d Mon Sep 17 00:00:00 2001 From: "Kevin (Kun) Kassimo Qian" Date: Sun, 9 Feb 2020 18:03:12 -0800 Subject: [PATCH 2/6] Re-call safe --- cli/js/files.ts | 17 ++++++++++++----- cli/ops/tty.rs | 8 ++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/cli/js/files.ts b/cli/js/files.ts index 9ce997737f666e..6a95b94380a7c4 100644 --- a/cli/js/files.ts +++ b/cli/js/files.ts @@ -14,8 +14,7 @@ import { sendAsyncMinimal, sendSyncMinimal } from "./dispatch_minimal.ts"; import * as dispatch from "./dispatch.ts"; import { sendSync as sendSyncJson, - sendAsync as sendAsyncJson, - sendSync + sendAsync as sendAsyncJson } from "./dispatch_json.ts"; /** Open a file and return an instance of the `File` object @@ -260,21 +259,29 @@ export class File export type RestoreModeFunc = () => void; /** Extended file abstraction with setRaw() */ export class Stdin extends File { + private _isRaw = false; + private _restoreFunc?: RestoreModeFunc; /** Set input mode to raw (non-canonical). * Returns a function that when called, restores previous mode. */ setRaw(): RestoreModeFunc { - const restoreInfo = sendSync(dispatch.OP_SET_RAW, { + if (this._isRaw) { + return this._restoreFunc!; + } + const restoreInfo = sendSyncJson(dispatch.OP_SET_RAW, { rid: this.rid, raw: true }); - return (): void => { - sendSync(dispatch.OP_SET_RAW, { + this._isRaw = true; + this._restoreFunc = (): void => { + sendSyncJson(dispatch.OP_SET_RAW, { rid: this.rid, raw: false, ...restoreInfo }); + this._isRaw = false; }; + return this._restoreFunc!; } } diff --git a/cli/ops/tty.rs b/cli/ops/tty.rs index 3cbe172c5d3340..95ccac22538fb0 100644 --- a/cli/ops/tty.rs +++ b/cli/ops/tty.rs @@ -117,7 +117,7 @@ pub fn op_set_raw( }; wincheck!(consoleapi::SetConsoleMode(handle, new_mode)); - return Ok(JsonOp::Sync(json!({}))); + Ok(JsonOp::Sync(json!({}))) } // From https://github.com/kkawakam/rustyline/blob/master/src/tty/unix.rs #[cfg(not(windows))] @@ -144,10 +144,10 @@ pub fn op_set_raw( raw.control_chars[termios::SpecialCharacterIndices::VMIN as usize] = 1; raw.control_chars[termios::SpecialCharacterIndices::VTIME as usize] = 0; termios::tcsetattr(raw_fd, termios::SetArg::TCSADRAIN, &raw)?; - return Ok(JsonOp::Sync(json!({ + Ok(JsonOp::Sync(json!({ "restore": serde_json::to_string(&SerializedTermios::from(original_mode)).unwrap(), - }))); + }))) } else { // Restore old mode. if args.restore.is_none() { @@ -164,7 +164,7 @@ pub fn op_set_raw( termios::SetArg::TCSADRAIN, &old_termios.into(), )?; - return Ok(JsonOp::Sync(json!({}))); + Ok(JsonOp::Sync(json!({}))) } } } From 09274e0fa5805e800c32686c78f6475a66e9a70f Mon Sep 17 00:00:00 2001 From: "Kevin (Kun) Kassimo Qian" Date: Sun, 9 Feb 2020 18:20:30 -0800 Subject: [PATCH 3/6] types --- cli/ops/tty.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cli/ops/tty.rs b/cli/ops/tty.rs index 95ccac22538fb0..ae4d0a8611d36f 100644 --- a/cli/ops/tty.rs +++ b/cli/ops/tty.rs @@ -28,11 +28,11 @@ macro_rules! wincheck { // Create a similar one for our use. #[derive(Serialize, Deserialize)] struct SerializedTermios { - iflags: u64, - oflags: u64, - cflags: u64, - lflags: u64, - cc: [u8; libc::NCCS], + iflags: libc::tcflag_t, + oflags: libc::tcflag_t, + cflags: libc::tcflag_t, + lflags: libc::tcflag_t, + cc: [libc::cc_t; libc::NCCS], } impl From for SerializedTermios { @@ -98,6 +98,7 @@ pub fn op_set_raw( #[cfg(windows)] { use std::os::windows::io::AsRawHandle; + use winapi::shared::minwindef::DWORD; use winapi::um::{consoleapi, handleapi, winbase, wincon, winuser}; let handle = std::io::stdin().as_raw_handle(); @@ -106,7 +107,7 @@ pub fn op_set_raw( } else if handle.is_null() { return Err(other_error("null handle".to_owned())); } - let mut original_mode = 0; + let mut original_mode: DWORD = 0; let RAW_MODE_MASK = ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT; wincheck!(consoleapi::GetConsoleMode(handle, &mut original_mode)); From 5eaeb7b0a5168494a3ddd2fb7171fa41226a8a5e Mon Sep 17 00:00:00 2001 From: "Kevin (Kun) Kassimo Qian" Date: Sun, 9 Feb 2020 19:26:50 -0800 Subject: [PATCH 4/6] windows 1 --- cli/ops/tty.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/cli/ops/tty.rs b/cli/ops/tty.rs index ae4d0a8611d36f..f594ee87e298b1 100644 --- a/cli/ops/tty.rs +++ b/cli/ops/tty.rs @@ -5,6 +5,7 @@ use crate::deno_error::other_error; use crate::ops::json_op; use crate::state::State; use deno_core::*; +#[cfg(unix)] use nix::sys::termios; use serde_derive::{Deserialize, Serialize}; use serde_json::Value; @@ -18,7 +19,7 @@ macro_rules! wincheck { ($funcall:expr) => {{ let rc = unsafe { $funcall }; if rc == 0 { - Err(std::io::Error::last_os_error())?; + Err(ErrBox::from(std::io::Error::last_os_error()))?; } rc }}; @@ -26,6 +27,7 @@ macro_rules! wincheck { // libc::termios cannot be serialized. // Create a similar one for our use. +#[cfg(unix)] #[derive(Serialize, Deserialize)] struct SerializedTermios { iflags: libc::tcflag_t, @@ -35,6 +37,7 @@ struct SerializedTermios { cc: [libc::cc_t; libc::NCCS], } +#[cfg(unix)] impl From for SerializedTermios { fn from(t: termios::Termios) -> Self { Self { @@ -47,6 +50,7 @@ impl From for SerializedTermios { } } +#[cfg(unix)] impl Into for SerializedTermios { fn into(self) -> termios::Termios { let mut t = unsafe { termios::Termios::default_uninit() }; @@ -63,7 +67,7 @@ impl Into for SerializedTermios { struct SetRawArgs { rid: u32, raw: bool, - #[cfg(not(windows))] + #[cfg(unix)] restore: Option, // Only used for *nix // Saved as string in case of u64 problem in JS } @@ -99,17 +103,18 @@ pub fn op_set_raw( { use std::os::windows::io::AsRawHandle; use winapi::shared::minwindef::DWORD; - use winapi::um::{consoleapi, handleapi, winbase, wincon, winuser}; + use winapi::um::{consoleapi, handleapi, wincon}; let handle = std::io::stdin().as_raw_handle(); if handle == handleapi::INVALID_HANDLE_VALUE { - return Err(std::io::Error::last_os_error()); + return Err(ErrBox::from(std::io::Error::last_os_error())); } else if handle.is_null() { - return Err(other_error("null handle".to_owned())); + return Err(ErrBox::from(other_error("null handle".to_owned()))); } let mut original_mode: DWORD = 0; - let RAW_MODE_MASK = - ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT; + let RAW_MODE_MASK = wincon::ENABLE_LINE_INPUT + | wincon::ENABLE_ECHO_INPUT + | wincon::ENABLE_PROCESSED_INPUT; wincheck!(consoleapi::GetConsoleMode(handle, &mut original_mode)); let new_mode = if is_raw { original_mode & !RAW_MODE_MASK; @@ -121,7 +126,7 @@ pub fn op_set_raw( Ok(JsonOp::Sync(json!({}))) } // From https://github.com/kkawakam/rustyline/blob/master/src/tty/unix.rs - #[cfg(not(windows))] + #[cfg(unix)] { use std::os::unix::io::AsRawFd; let raw_fd = std::io::stdin().as_raw_fd(); From f139d214e566379973a2ade0752e18f263bd2a86 Mon Sep 17 00:00:00 2001 From: "Kevin (Kun) Kassimo Qian" Date: Sun, 9 Feb 2020 19:56:16 -0800 Subject: [PATCH 5/6] windows 2 --- cli/ops/tty.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cli/ops/tty.rs b/cli/ops/tty.rs index f594ee87e298b1..2a9c6f123481de 100644 --- a/cli/ops/tty.rs +++ b/cli/ops/tty.rs @@ -7,7 +7,9 @@ use crate::state::State; use deno_core::*; #[cfg(unix)] use nix::sys::termios; -use serde_derive::{Deserialize, Serialize}; +use serde_derive::Deserialize; +#[cfg(unix)] +use serde_derive::Serialize; use serde_json::Value; pub fn init(i: &mut Isolate, s: &State) { @@ -117,9 +119,9 @@ pub fn op_set_raw( | wincon::ENABLE_PROCESSED_INPUT; wincheck!(consoleapi::GetConsoleMode(handle, &mut original_mode)); let new_mode = if is_raw { - original_mode & !RAW_MODE_MASK; + original_mode & !RAW_MODE_MASK } else { - original_mode | RAW_MODE_MASK; + original_mode | RAW_MODE_MASK }; wincheck!(consoleapi::SetConsoleMode(handle, new_mode)); From dede7f117bb5abec1419616ef41d11159da1e148 Mon Sep 17 00:00:00 2001 From: "Kevin (Kun) Kassimo Qian" Date: Sun, 9 Feb 2020 20:31:57 -0800 Subject: [PATCH 6/6] windows 3 --- cli/ops/tty.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/cli/ops/tty.rs b/cli/ops/tty.rs index 2a9c6f123481de..cb51cc59bef82d 100644 --- a/cli/ops/tty.rs +++ b/cli/ops/tty.rs @@ -12,6 +12,15 @@ use serde_derive::Deserialize; use serde_derive::Serialize; use serde_json::Value; +#[cfg(windows)] +use winapi::shared::minwindef::DWORD; +#[cfg(windows)] +use winapi::um::wincon; +#[cfg(windows)] +const RAW_MODE_MASK: DWORD = wincon::ENABLE_LINE_INPUT + | wincon::ENABLE_ECHO_INPUT + | wincon::ENABLE_PROCESSED_INPUT; + pub fn init(i: &mut Isolate, s: &State) { i.register_op("set_raw", s.core_op(json_op(s.stateful_op(op_set_raw)))); } @@ -89,7 +98,7 @@ pub fn op_set_raw( return Err(bad_resource()); } - // For now, only stdin + // For now, only stdin. match resource.unwrap() { StreamResource::Stdin(_) => (), _ => { @@ -104,8 +113,7 @@ pub fn op_set_raw( #[cfg(windows)] { use std::os::windows::io::AsRawHandle; - use winapi::shared::minwindef::DWORD; - use winapi::um::{consoleapi, handleapi, wincon}; + use winapi::um::{consoleapi, handleapi}; let handle = std::io::stdin().as_raw_handle(); if handle == handleapi::INVALID_HANDLE_VALUE { @@ -114,9 +122,6 @@ pub fn op_set_raw( return Err(ErrBox::from(other_error("null handle".to_owned()))); } let mut original_mode: DWORD = 0; - let RAW_MODE_MASK = wincon::ENABLE_LINE_INPUT - | wincon::ENABLE_ECHO_INPUT - | wincon::ENABLE_PROCESSED_INPUT; wincheck!(consoleapi::GetConsoleMode(handle, &mut original_mode)); let new_mode = if is_raw { original_mode & !RAW_MODE_MASK