diff --git a/Cargo.lock b/Cargo.lock index 29f8e334cb..b3d5f48145 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 52bc4f2c33..f09f8b6e45 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -72,3 +72,6 @@ nix = "0.14.1" [dev-dependencies] os_pipe = "0.9.1" + +[target.'cfg(unix)'.dev-dependencies] +pty = "0.2" diff --git a/cli/js/deno.ts b/cli/js/deno.ts index e2052f7293..c563d5112c 100644 --- a/cli/js/deno.ts +++ b/cli/js/deno.ts @@ -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"; diff --git a/cli/js/lib.deno.ns.d.ts b/cli/js/lib.deno.ns.d.ts index b22d89ebeb..f94d284072 100644 --- a/cli/js/lib.deno.ns.d.ts +++ b/cli/js/lib.deno.ns.d.ts @@ -30,16 +30,6 @@ declare namespace Deno { export function runTests(opts?: RunTestsOptions): Promise; - /** 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() diff --git a/cli/js/os.ts b/cli/js/os.ts index 2a68ff8d33..309f5e1ff2 100644 --- a/cli/js/os.ts +++ b/cli/js/os.ts @@ -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. * diff --git a/cli/js/os_test.ts b/cli/js/os_test.ts index 6e771fe989..cdf72fdd7e 100644 --- a/cli/js/os_test.ts +++ b/cli/js/os_test.ts @@ -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"; diff --git a/cli/js/tty.ts b/cli/js/tty.ts new file mode 100644 index 0000000000..2ad44d025e --- /dev/null +++ b/cli/js/tty.ts @@ -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 + }); +} diff --git a/cli/js/tty_test.ts b/cli/js/tty_test.ts new file mode 100644 index 0000000000..f58784a7cd --- /dev/null +++ b/cli/js/tty_test.ts @@ -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); +}); diff --git a/cli/js/unit_tests.ts b/cli/js/unit_tests.ts index 1c82374663..2495c938bf 100644 --- a/cli/js/unit_tests.ts +++ b/cli/js/unit_tests.ts @@ -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"; diff --git a/cli/ops/files.rs b/cli/ops/files.rs index 4bf8b16887..916cbdc69a 100644 --- a/cli/ops/files.rs +++ b/cli/ops/files.rs @@ -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())?; diff --git a/cli/ops/io.rs b/cli/ops/io.rs index 8edb1f748e..ad3949a032 100644 --- a/cli/ops/io.rs +++ b/cli/ops/io.rs @@ -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, +} + +#[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>), ClientTlsStream(Box>), @@ -101,8 +115,8 @@ impl DenoAsyncRead for StreamResource { ) -> Poll> { use StreamResource::*; let mut f: Pin> = 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> { use StreamResource::*; let mut f: Pin> = 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> { use StreamResource::*; let mut f: Pin> = 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), diff --git a/cli/ops/mod.rs b/cli/ops/mod.rs index 7746143dbe..32d4e3b965 100644 --- a/cli/ops/mod.rs +++ b/cli/ops/mod.rs @@ -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; diff --git a/cli/ops/os.rs b/cli/ops/os.rs index c0479c656c..2df9470ddb 100644 --- a/cli/ops/os.rs +++ b/cli/ops/os.rs @@ -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, -) -> Result { - 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, diff --git a/cli/ops/process.rs b/cli/ops/process.rs index 9da87bd396..82ac25bbee 100644 --- a/cli/ops/process.rs +++ b/cli/ops/process.rs @@ -33,7 +33,7 @@ fn clone_file(rid: u32, state: &State) -> Result { .get_mut::(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())?; diff --git a/cli/ops/tty.rs b/cli/ops/tty.rs new file mode 100644 index 0000000000..e83d2edfdc --- /dev/null +++ b/cli/ops/tty.rs @@ -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 { + 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, +) -> Result { + 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::(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::(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, +) -> Result { + 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::(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))), + } +} diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 29c4ac1d05..7fd531c298 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -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]")); diff --git a/cli/tests/raw_mode.ts b/cli/tests/raw_mode.ts new file mode 100644 index 0000000000..125601b2b5 --- /dev/null +++ b/cli/tests/raw_mode.ts @@ -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 diff --git a/cli/worker.rs b/cli/worker.rs index a4a35dfaab..b619b2a67b 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -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); } diff --git a/core/resources.rs b/core/resources.rs index 51b66c4bc5..a5563bed35 100644 --- a/core/resources.rs +++ b/core/resources.rs @@ -26,6 +26,10 @@ pub struct ResourceTable { } impl ResourceTable { + pub fn has(&self, rid: ResourceId) -> bool { + self.map.contains_key(&rid) + } + pub fn get(&self, rid: ResourceId) -> Option<&T> { if let Some((_name, resource)) = self.map.get(&rid) { return resource.downcast_ref::();