Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] tty: Deno.stdin.setRaw() #3947

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/js/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
33 changes: 31 additions & 2 deletions cli/js/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,37 @@ 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 {
private _isRaw = false;
private _restoreFunc?: RestoreModeFunc;
/** Set input mode to raw (non-canonical).
* Returns a function that when called, restores previous mode.
*/
setRaw(): RestoreModeFunc {
if (this._isRaw) {
return this._restoreFunc!;
}
const restoreInfo = sendSyncJson(dispatch.OP_SET_RAW, {
rid: this.rid,
raw: true
});
this._isRaw = true;
this._restoreFunc = (): void => {
sendSyncJson(dispatch.OP_SET_RAW, {
rid: this.rid,
raw: false,
...restoreInfo
});
this._isRaw = false;
};
return this._restoreFunc!;
}
}

/** 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. */
Expand Down
12 changes: 10 additions & 2 deletions cli/js/lib.deno.ns.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
1 change: 1 addition & 0 deletions cli/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
183 changes: 183 additions & 0 deletions cli/ops/tty.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
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::*;
#[cfg(unix)]
use nix::sys::termios;
use serde_derive::Deserialize;
#[cfg(unix)]
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))));
}

#[cfg(windows)]
macro_rules! wincheck {
($funcall:expr) => {{
let rc = unsafe { $funcall };
if rc == 0 {
Err(ErrBox::from(std::io::Error::last_os_error()))?;
}
rc
}};
}

// libc::termios cannot be serialized.
// Create a similar one for our use.
#[cfg(unix)]
#[derive(Serialize, Deserialize)]
struct SerializedTermios {
iflags: libc::tcflag_t,
oflags: libc::tcflag_t,
cflags: libc::tcflag_t,
lflags: libc::tcflag_t,
cc: [libc::cc_t; libc::NCCS],
}

#[cfg(unix)]
impl From<termios::Termios> 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,
}
}
}

#[cfg(unix)]
impl Into<termios::Termios> 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(unix)]
restore: Option<String>, // 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<ZeroCopyBuf>,
) -> Result<JsonOp, ErrBox> {
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::<StreamResource>(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
Copy link
Member

@ry ry Feb 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add their copyright header to this file. Something like this

// Copyright (c) 2015 Katsu Kawakami & Rustyline authors. MIT license.

#[cfg(windows)]
{
use std::os::windows::io::AsRawHandle;
use winapi::um::{consoleapi, handleapi};

let handle = std::io::stdin().as_raw_handle();
if handle == handleapi::INVALID_HANDLE_VALUE {
return Err(ErrBox::from(std::io::Error::last_os_error()));
} else if handle.is_null() {
return Err(ErrBox::from(other_error("null handle".to_owned())));
}
let mut original_mode: DWORD = 0;
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));

Ok(JsonOp::Sync(json!({})))
}
// From https://github.com/kkawakam/rustyline/blob/master/src/tty/unix.rs
#[cfg(unix)]
{
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)?;
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::<SerializedTermios>(&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(),
)?;
Ok(JsonOp::Sync(json!({})))
}
}
}
1 change: 1 addition & 0 deletions cli/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down