tty: Deno.setRaw(rid, mode) to turn on/off raw mode (#3958)

This commit is contained in:
Kevin (Kun) "Kassimo" Qian 2020-02-25 22:01:24 -08:00 committed by GitHub
parent e53064c4f2
commit 5946808f66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 429 additions and 49 deletions

22
Cargo.lock generated
View file

@ -454,6 +454,7 @@ dependencies = [
"nix",
"notify",
"os_pipe",
"pty",
"rand 0.7.3",
"regex",
"remove_dir_all",
@ -619,6 +620,17 @@ dependencies = [
"syn 1.0.14",
]
[[package]]
name = "errno"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e2b2decb0484e15560df3210cf0d78654bb0864b2c138977c07e377a1bae0e2"
dependencies = [
"kernel32-sys",
"libc",
"winapi 0.2.8",
]
[[package]]
name = "failure"
version = "0.1.6"
@ -1508,6 +1520,16 @@ dependencies = [
"unicode-xid 0.2.0",
]
[[package]]
name = "pty"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f50f3d255966981eb4e4c5df3e983e6f7d163221f547406d83b6a460ff5c5ee8"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "qadapt-spin"
version = "1.0.1"

View file

@ -72,3 +72,6 @@ nix = "0.14.1"
[dev-dependencies]
os_pipe = "0.9.1"
[target.'cfg(unix)'.dev-dependencies]
pty = "0.2"

View file

@ -90,7 +90,6 @@ export {
dir,
env,
exit,
isTTY,
execPath,
hostname,
loadavg,
@ -124,6 +123,7 @@ export { statSync, lstatSync, stat, lstat } from "./stat.ts";
export { symlinkSync, symlink } from "./symlink.ts";
export { connectTLS, listenTLS } from "./tls.ts";
export { truncateSync, truncate } from "./truncate.ts";
export { isatty, setRaw } from "./tty.ts";
export { utimeSync, utime } from "./utime.ts";
export { version } from "./version.ts";
export { writeFileSync, writeFile, WriteFileOptions } from "./write_file.ts";

View file

@ -30,16 +30,6 @@ declare namespace Deno {
export function runTests(opts?: RunTestsOptions): Promise<void>;
/** Check if running in terminal.
*
* console.log(Deno.isTTY().stdout);
*/
export function isTTY(): {
stdin: boolean;
stdout: boolean;
stderr: boolean;
};
/** Get the loadavg. Requires the `--allow-env` flag.
*
* console.log(Deno.loadavg());
@ -492,6 +482,7 @@ declare namespace Deno {
seekSync(offset: number, whence: SeekMode): void;
close(): void;
}
/** An instance of `File` for stdin. */
export const stdin: File;
/** An instance of `File` for stdout. */
@ -555,6 +546,20 @@ declare namespace Deno {
/** Read-write. Behaves like `x` and allows to read from file. */
| "x+";
// @url js/tty.d.ts
/** UNSTABLE: newly added API
*
* Check if a given resource is TTY
*/
export function isatty(rid: number): boolean;
/** UNSTABLE: newly added API
*
* Set TTY to be under raw mode or not.
*/
export function setRaw(rid: number, mode: boolean): void;
// @url js/buffer.d.ts
/** A Buffer is a variable-sized buffer of bytes with read() and write()

View file

@ -3,13 +3,6 @@ import { sendSync } from "./dispatch_json.ts";
import { errors } from "./errors.ts";
import * as util from "./util.ts";
/** Check if running in terminal.
*
* console.log(Deno.isTTY().stdout);
*/
export function isTTY(): { stdin: boolean; stdout: boolean; stderr: boolean } {
return sendSync("op_is_tty");
}
/** Get the loadavg.
* Requires the `--allow-env` flag.
*

View file

@ -115,10 +115,6 @@ test(function osPid(): void {
assert(Deno.pid > 0);
});
test(function osIsTTYSmoke(): void {
console.log(Deno.isTTY());
});
testPerm({ env: true }, function getDir(): void {
type supportOS = "mac" | "win" | "linux";

14
cli/js/tty.ts Normal file
View file

@ -0,0 +1,14 @@
import { sendSync } from "./dispatch_json.ts";
/** Check if a given resource is TTY. */
export function isatty(rid: number): boolean {
return sendSync("op_isatty", { rid });
}
/** Set TTY to be under raw mode or not. */
export function setRaw(rid: number, mode: boolean): void {
sendSync("op_set_raw", {
rid,
mode
});
}

22
cli/js/tty_test.ts Normal file
View file

@ -0,0 +1,22 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { test, testPerm, assert } from "./test_util.ts";
// Note tests for Deno.setRaw is in integration tests.
testPerm({ read: true }, function isatty(): void {
// CI not under TTY, so cannot test stdin/stdout/stderr.
const f = Deno.openSync("cli/tests/hello.txt");
assert(!Deno.isatty(f.rid));
});
test(function isattyError(): void {
let caught = false;
try {
// Absurdly large rid.
Deno.isatty(0x7fffffff);
} catch (e) {
caught = true;
assert(e instanceof Deno.errors.BadResource);
}
assert(caught);
});

View file

@ -54,6 +54,7 @@ import "./text_encoding_test.ts";
import "./timers_test.ts";
import "./tls_test.ts";
import "./truncate_test.ts";
import "./tty_test.ts";
import "./url_test.ts";
import "./url_search_params_test.ts";
import "./utime_test.ts";

View file

@ -1,6 +1,6 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use super::dispatch_json::{Deserialize, JsonOp, Value};
use super::io::StreamResource;
use super::io::{FileMetadata, StreamResource};
use crate::fs as deno_fs;
use crate::op_error::OpError;
use crate::state::State;
@ -125,9 +125,10 @@ fn op_open(
let fut = async move {
let fs_file = open_options.open(filename).await?;
let mut state = state_.borrow_mut();
let rid = state
.resource_table
.add("fsFile", Box::new(StreamResource::FsFile(fs_file)));
let rid = state.resource_table.add(
"fsFile",
Box::new(StreamResource::FsFile(fs_file, FileMetadata::default())),
);
Ok(json!(rid))
};
@ -197,7 +198,7 @@ fn op_seek(
.ok_or_else(OpError::bad_resource)?;
let tokio_file = match resource {
StreamResource::FsFile(ref file) => file,
StreamResource::FsFile(ref file, _) => file,
_ => return Err(OpError::bad_resource()),
};
let mut file = futures::executor::block_on(tokio_file.try_clone())?;

View file

@ -57,7 +57,7 @@ pub fn init(i: &mut Isolate, s: &State) {
}
pub fn get_stdio() -> (StreamResource, StreamResource, StreamResource) {
let stdin = StreamResource::Stdin(tokio::io::stdin());
let stdin = StreamResource::Stdin(tokio::io::stdin(), TTYMetadata::default());
let stdout = StreamResource::Stdout({
let stdout = STDOUT_HANDLE
.try_clone()
@ -69,11 +69,25 @@ pub fn get_stdio() -> (StreamResource, StreamResource, StreamResource) {
(stdin, stdout, stderr)
}
#[cfg(unix)]
use nix::sys::termios;
#[derive(Default)]
pub struct TTYMetadata {
#[cfg(unix)]
pub mode: Option<termios::Termios>,
}
#[derive(Default)]
pub struct FileMetadata {
pub tty: TTYMetadata,
}
pub enum StreamResource {
Stdin(tokio::io::Stdin),
Stdin(tokio::io::Stdin, TTYMetadata),
Stdout(tokio::fs::File),
Stderr(tokio::io::Stderr),
FsFile(tokio::fs::File),
FsFile(tokio::fs::File, FileMetadata),
TcpStream(tokio::net::TcpStream),
ServerTlsStream(Box<ServerTlsStream<TcpStream>>),
ClientTlsStream(Box<ClientTlsStream<TcpStream>>),
@ -101,8 +115,8 @@ impl DenoAsyncRead for StreamResource {
) -> Poll<Result<usize, OpError>> {
use StreamResource::*;
let mut f: Pin<Box<dyn AsyncRead>> = match self {
FsFile(f) => Box::pin(f),
Stdin(f) => Box::pin(f),
FsFile(f, _) => Box::pin(f),
Stdin(f, _) => Box::pin(f),
TcpStream(f) => Box::pin(f),
ClientTlsStream(f) => Box::pin(f),
ServerTlsStream(f) => Box::pin(f),
@ -203,7 +217,7 @@ impl DenoAsyncWrite for StreamResource {
) -> Poll<Result<usize, OpError>> {
use StreamResource::*;
let mut f: Pin<Box<dyn AsyncWrite>> = match self {
FsFile(f) => Box::pin(f),
FsFile(f, _) => Box::pin(f),
Stdout(f) => Box::pin(f),
Stderr(f) => Box::pin(f),
TcpStream(f) => Box::pin(f),
@ -220,7 +234,7 @@ impl DenoAsyncWrite for StreamResource {
fn poll_flush(&mut self, cx: &mut Context) -> Poll<Result<(), OpError>> {
use StreamResource::*;
let mut f: Pin<Box<dyn AsyncWrite>> = match self {
FsFile(f) => Box::pin(f),
FsFile(f, _) => Box::pin(f),
Stdout(f) => Box::pin(f),
Stderr(f) => Box::pin(f),
TcpStream(f) => Box::pin(f),

View file

@ -28,5 +28,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;

View file

@ -2,7 +2,6 @@
use super::dispatch_json::{Deserialize, JsonOp, Value};
use crate::op_error::OpError;
use crate::state::State;
use atty;
use deno_core::*;
use std::collections::HashMap;
use std::env;
@ -12,7 +11,6 @@ use url::Url;
pub fn init(i: &mut Isolate, s: &State) {
i.register_op("op_exit", s.stateful_json_op(op_exit));
i.register_op("op_is_tty", s.stateful_json_op(op_is_tty));
i.register_op("op_env", s.stateful_json_op(op_env));
i.register_op("op_exec_path", s.stateful_json_op(op_exec_path));
i.register_op("op_set_env", s.stateful_json_op(op_set_env));
@ -151,18 +149,6 @@ fn op_exit(
std::process::exit(args.code)
}
fn op_is_tty(
_s: &State,
_args: Value,
_zero_copy: Option<ZeroCopyBuf>,
) -> Result<JsonOp, OpError> {
Ok(JsonOp::Sync(json!({
"stdin": atty::is(atty::Stream::Stdin),
"stdout": atty::is(atty::Stream::Stdout),
"stderr": atty::is(atty::Stream::Stderr),
})))
}
fn op_loadavg(
state: &State,
_args: Value,

View file

@ -33,7 +33,7 @@ fn clone_file(rid: u32, state: &State) -> Result<std::fs::File, OpError> {
.get_mut::<StreamResource>(rid)
.ok_or_else(OpError::bad_resource)?;
let file = match repr {
StreamResource::FsFile(ref mut file) => file,
StreamResource::FsFile(ref mut file, _) => file,
_ => return Err(OpError::bad_resource()),
};
let tokio_file = futures::executor::block_on(file.try_clone())?;

246
cli/ops/tty.rs Normal file
View file

@ -0,0 +1,246 @@
use super::dispatch_json::JsonOp;
use super::io::StreamResource;
use crate::op_error::OpError;
use crate::ops::json_op;
use crate::state::State;
use atty;
use deno_core::*;
#[cfg(unix)]
use nix::sys::termios;
use serde_derive::Deserialize;
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;
#[cfg(windows)]
fn get_windows_handle(
f: &std::fs::File,
) -> Result<std::os::windows::io::RawHandle, OpError> {
use std::os::windows::io::AsRawHandle;
use winapi::um::handleapi;
let handle = f.as_raw_handle();
if handle == handleapi::INVALID_HANDLE_VALUE {
return Err(OpError::from(std::io::Error::last_os_error()));
} else if handle.is_null() {
return Err(OpError::other("null handle".to_owned()));
}
Ok(handle)
}
pub fn init(i: &mut Isolate, s: &State) {
i.register_op("op_set_raw", s.core_op(json_op(s.stateful_op(op_set_raw))));
i.register_op("op_isatty", s.core_op(json_op(s.stateful_op(op_isatty))));
}
#[cfg(windows)]
macro_rules! wincheck {
($funcall:expr) => {{
let rc = unsafe { $funcall };
if rc == 0 {
Err(OpError::from(std::io::Error::last_os_error()))?;
}
rc
}};
}
#[derive(Deserialize)]
struct SetRawArgs {
rid: u32,
mode: bool,
}
pub fn op_set_raw(
state_: &State,
args: Value,
_zero_copy: Option<ZeroCopyBuf>,
) -> Result<JsonOp, OpError> {
let args: SetRawArgs = serde_json::from_value(args)?;
let rid = args.rid;
let is_raw = args.mode;
// From https://github.com/kkawakam/rustyline/blob/master/src/tty/windows.rs
// and https://github.com/kkawakam/rustyline/blob/master/src/tty/unix.rs
// and https://github.com/crossterm-rs/crossterm/blob/e35d4d2c1cc4c919e36d242e014af75f6127ab50/src/terminal/sys/windows.rs
// Copyright (c) 2015 Katsu Kawakami & Rustyline authors. MIT license.
// Copyright (c) 2019 Timon. MIT license.
#[cfg(windows)]
{
use std::os::windows::io::AsRawHandle;
use winapi::um::{consoleapi, handleapi};
let state = state_.borrow_mut();
let resource = state.resource_table.get::<StreamResource>(rid);
if resource.is_none() {
return Err(OpError::bad_resource());
}
// For now, only stdin.
let handle = match resource.unwrap() {
StreamResource::Stdin(_, _) => std::io::stdin().as_raw_handle(),
StreamResource::FsFile(f, _) => {
let tokio_file = futures::executor::block_on(f.try_clone())?;
let std_file = futures::executor::block_on(tokio_file.into_std());
std_file.as_raw_handle()
}
_ => {
return Err(OpError::other("Not supported".to_owned()));
}
};
if handle == handleapi::INVALID_HANDLE_VALUE {
return Err(OpError::from(std::io::Error::last_os_error()));
} else if handle.is_null() {
return Err(OpError::other("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!({})))
}
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let mut state = state_.borrow_mut();
let resource = state.resource_table.get_mut::<StreamResource>(rid);
if resource.is_none() {
return Err(OpError::bad_resource());
}
if is_raw {
let (raw_fd, maybe_tty_mode) = match resource.unwrap() {
StreamResource::Stdin(_, ref mut metadata) => {
(std::io::stdin().as_raw_fd(), &mut metadata.mode)
}
StreamResource::FsFile(f, ref mut metadata) => {
let tokio_file = futures::executor::block_on(f.try_clone())?;
let std_file = futures::executor::block_on(tokio_file.into_std());
(std_file.as_raw_fd(), &mut metadata.tty.mode)
}
_ => {
return Err(OpError::other("Not supported".to_owned()));
}
};
if maybe_tty_mode.is_some() {
// Already raw. Skip.
return Ok(JsonOp::Sync(json!({})));
}
let original_mode = termios::tcgetattr(raw_fd)?;
let mut raw = original_mode.clone();
// Save original mode.
maybe_tty_mode.replace(original_mode);
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!({})))
} else {
// Try restore saved mode.
let (raw_fd, maybe_tty_mode) = match resource.unwrap() {
StreamResource::Stdin(_, ref mut metadata) => {
(std::io::stdin().as_raw_fd(), &mut metadata.mode)
}
StreamResource::FsFile(f, ref mut metadata) => {
let tokio_file = futures::executor::block_on(f.try_clone())?;
let std_file = futures::executor::block_on(tokio_file.into_std());
(std_file.as_raw_fd(), &mut metadata.tty.mode)
}
_ => {
return Err(OpError::other("Not supported".to_owned()));
}
};
if let Some(mode) = maybe_tty_mode.take() {
termios::tcsetattr(raw_fd, termios::SetArg::TCSADRAIN, &mode)?;
}
Ok(JsonOp::Sync(json!({})))
}
}
}
#[derive(Deserialize)]
struct IsattyArgs {
rid: u32,
}
pub fn op_isatty(
state_: &State,
args: Value,
_zero_copy: Option<ZeroCopyBuf>,
) -> Result<JsonOp, OpError> {
let args: IsattyArgs = serde_json::from_value(args)?;
let rid = args.rid;
let state = state_.borrow_mut();
if !state.resource_table.has(rid) {
return Err(OpError::bad_resource());
}
let resource = state.resource_table.get::<StreamResource>(rid);
if resource.is_none() {
return Ok(JsonOp::Sync(json!(false)));
}
match resource.unwrap() {
StreamResource::Stdin(_, _) => {
Ok(JsonOp::Sync(json!(atty::is(atty::Stream::Stdin))))
}
StreamResource::Stdout(_) => {
Ok(JsonOp::Sync(json!(atty::is(atty::Stream::Stdout))))
}
StreamResource::Stderr(_) => {
Ok(JsonOp::Sync(json!(atty::is(atty::Stream::Stderr))))
}
StreamResource::FsFile(f, _) => {
let tokio_file = futures::executor::block_on(f.try_clone())?;
let std_file = futures::executor::block_on(tokio_file.into_std());
#[cfg(windows)]
{
use winapi::um::consoleapi;
let handle = get_windows_handle(&std_file)?;
let mut test_mode: DWORD = 0;
// If I cannot get mode out of console, it is not a console.
let result =
unsafe { consoleapi::GetConsoleMode(handle, &mut test_mode) != 0 };
Ok(JsonOp::Sync(json!(result)))
}
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let raw_fd = std_file.as_raw_fd();
let result = unsafe { libc::isatty(raw_fd as libc::c_int) == 1 };
Ok(JsonOp::Sync(json!(result)))
}
}
_ => Ok(JsonOp::Sync(json!(false))),
}
}

View file

@ -1,8 +1,61 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
#[macro_use]
extern crate lazy_static;
#[cfg(unix)]
extern crate nix;
#[cfg(unix)]
extern crate pty;
extern crate tempfile;
#[cfg(unix)]
#[test]
pub fn test_raw_tty() {
use pty::fork::*;
use std::io::{Read, Write};
let fork = Fork::from_ptmx().unwrap();
if let Ok(mut master) = fork.is_parent() {
let mut obytes: [u8; 100] = [0; 100];
let mut nread = master.read(&mut obytes).unwrap();
assert_eq!(String::from_utf8_lossy(&obytes[0..nread]), "S");
master.write_all(b"a").unwrap();
nread = master.read(&mut obytes).unwrap();
assert_eq!(String::from_utf8_lossy(&obytes[0..nread]), "A");
master.write_all(b"b").unwrap();
nread = master.read(&mut obytes).unwrap();
assert_eq!(String::from_utf8_lossy(&obytes[0..nread]), "B");
master.write_all(b"c").unwrap();
nread = master.read(&mut obytes).unwrap();
assert_eq!(String::from_utf8_lossy(&obytes[0..nread]), "C");
} else {
use deno::test_util::*;
use nix::sys::termios;
use std::os::unix::io::AsRawFd;
use std::process::*;
use tempfile::TempDir;
// Turn off echo such that parent is reading works properly.
let stdin_fd = std::io::stdin().as_raw_fd();
let mut t = termios::tcgetattr(stdin_fd).unwrap();
t.local_flags.remove(termios::LocalFlags::ECHO);
termios::tcsetattr(stdin_fd, termios::SetArg::TCSANOW, &t).unwrap();
let deno_dir = TempDir::new().expect("tempdir fail");
let mut child = Command::new(deno_exe_path())
.env("DENO_DIR", deno_dir.path())
.current_dir(util::root_path())
.arg("run")
.arg("cli/tests/raw_mode.ts")
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::null())
.spawn()
.expect("Failed to spawn script");
child.wait().unwrap();
}
}
#[test]
fn test_pattern_match() {
assert!(util::pattern_match("foo[BAR]baz", "foobarbaz", "[BAR]"));

18
cli/tests/raw_mode.ts Normal file
View file

@ -0,0 +1,18 @@
Deno.setRaw(0, true);
Deno.setRaw(0, true); // Can be called multiple times
Deno.stdout.writeSync(new TextEncoder().encode("S"));
const buf = new Uint8Array(3);
for (let i = 0; i < 3; i++) {
const nread = await Deno.stdin.read(buf);
if (nread === Deno.EOF) {
break;
} else {
const data = new TextDecoder().decode(buf.subarray(0, nread));
Deno.stdout.writeSync(new TextEncoder().encode(data.toUpperCase()));
}
}
Deno.setRaw(0, false); // restores old mode.
Deno.setRaw(0, false); // Can be safely called multiple times

View file

@ -219,6 +219,7 @@ impl MainWorker {
ops::resources::init(isolate, &state);
ops::signal::init(isolate, &state);
ops::timers::init(isolate, &state);
ops::tty::init(isolate, &state);
ops::worker_host::init(isolate, &state);
ops::web_worker::init(isolate, &state, &worker.internal_channels.sender);
}

View file

@ -26,6 +26,10 @@ pub struct ResourceTable {
}
impl ResourceTable {
pub fn has(&self, rid: ResourceId) -> bool {
self.map.contains_key(&rid)
}
pub fn get<T: Resource>(&self, rid: ResourceId) -> Option<&T> {
if let Some((_name, resource)) = self.map.get(&rid) {
return resource.downcast_ref::<T>();