From 2e74f164b6dcf0ecbf8dd38fba9fae550d784bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Sun, 13 Dec 2020 19:45:53 +0100 Subject: refactor: deno_runtime crate (#8640) This commit moves Deno JS runtime, ops, permissions and inspector implementation to new "deno_runtime" crate located in "runtime/" directory. Details in "runtime/README.md". Co-authored-by: Ryan Dahl --- Cargo.lock | 48 +- Cargo.toml | 1 + cli/Cargo.toml | 11 +- cli/build.rs | 17 +- cli/errors.rs | 195 +--- cli/file_fetcher.rs | 4 +- cli/file_watcher.rs | 13 +- cli/http_util.rs | 20 +- cli/inspector.rs | 952 ------------------- cli/js.rs | 27 - cli/main.rs | 53 +- cli/metrics.rs | 131 --- cli/module_loader.rs | 2 +- cli/ops/crypto.rs | 14 - cli/ops/dispatch_minimal.rs | 205 ---- cli/ops/fetch.rs | 22 - cli/ops/fs.rs | 1702 --------------------------------- cli/ops/fs_events.rs | 133 --- cli/ops/io.rs | 473 ---------- cli/ops/mod.rs | 56 +- cli/ops/net.rs | 566 ----------- cli/ops/net_unix.rs | 151 --- cli/ops/os.rs | 192 ---- cli/ops/permissions.rs | 103 -- cli/ops/plugin.rs | 156 --- cli/ops/process.rs | 236 ----- cli/ops/runtime.rs | 118 --- cli/ops/runtime_compiler.rs | 8 +- cli/ops/signal.rs | 142 --- cli/ops/timers.rs | 193 ---- cli/ops/tls.rs | 431 --------- cli/ops/tty.rs | 334 ------- cli/ops/web_worker.rs | 37 - cli/ops/websocket.rs | 326 ------- cli/ops/worker_host.rs | 318 ------- cli/permissions.rs | 1095 --------------------- cli/program_state.rs | 4 +- cli/resolve_addr.rs | 72 -- cli/rt/00_bootstrap_namespace.js | 9 - cli/rt/01_build.js | 26 - cli/rt/01_colors.js | 92 -- cli/rt/01_errors.js | 162 ---- cli/rt/01_internals.js | 23 - cli/rt/01_version.js | 26 - cli/rt/01_web_util.js | 160 ---- cli/rt/02_console.js | 1732 ---------------------------------- cli/rt/06_util.js | 148 --- cli/rt/10_dispatch_minimal.js | 114 --- cli/rt/11_timers.js | 557 ----------- cli/rt/11_workers.js | 206 ---- cli/rt/12_io.js | 135 --- cli/rt/13_buffer.js | 241 ----- cli/rt/27_websocket.js | 316 ------- cli/rt/30_files.js | 209 ---- cli/rt/30_fs.js | 425 --------- cli/rt/30_metrics.js | 13 - cli/rt/30_net.js | 245 ----- cli/rt/30_os.js | 66 -- cli/rt/40_compiler_api.js | 97 -- cli/rt/40_diagnostics.js | 23 - cli/rt/40_error_stack.js | 23 - cli/rt/40_fs_events.js | 52 - cli/rt/40_net_unstable.js | 48 - cli/rt/40_performance.js | 341 ------- cli/rt/40_permissions.js | 65 -- cli/rt/40_plugins.js | 13 - cli/rt/40_process.js | 122 --- cli/rt/40_read_file.js | 43 - cli/rt/40_signals.js | 256 ----- cli/rt/40_testing.js | 350 ------- cli/rt/40_tls.js | 82 -- cli/rt/40_tty.js | 28 - cli/rt/40_write_file.js | 92 -- cli/rt/41_prompt.js | 80 -- cli/rt/90_deno_ns.js | 137 --- cli/rt/99_main.js | 395 -------- cli/rt/README.md | 59 -- cli/signal.rs | 62 -- cli/specifier_handler.rs | 2 +- cli/standalone.rs | 7 +- cli/tools/coverage.rs | 2 +- cli/tools/repl.rs | 4 +- cli/tools/upgrade.rs | 4 +- cli/web_worker.rs | 588 ------------ cli/worker.rs | 342 ------- core/error.rs | 5 - runtime/Cargo.toml | 74 ++ runtime/README.md | 44 + runtime/build.rs | 81 ++ runtime/colors.rs | 130 +++ runtime/errors.rs | 209 ++++ runtime/examples/hello_runtime.js | 2 + runtime/examples/hello_runtime.rs | 55 ++ runtime/fs_util.rs | 80 ++ runtime/http_util.rs | 46 + runtime/inspector.rs | 952 +++++++++++++++++++ runtime/js.rs | 31 + runtime/lib.rs | 26 + runtime/metrics.rs | 131 +++ runtime/ops/crypto.rs | 14 + runtime/ops/dispatch_minimal.rs | 205 ++++ runtime/ops/fetch.rs | 25 + runtime/ops/fs.rs | 1702 +++++++++++++++++++++++++++++++++ runtime/ops/fs_events.rs | 133 +++ runtime/ops/io.rs | 473 ++++++++++ runtime/ops/mod.rs | 89 ++ runtime/ops/net.rs | 566 +++++++++++ runtime/ops/net_unix.rs | 151 +++ runtime/ops/os.rs | 192 ++++ runtime/ops/permissions.rs | 103 ++ runtime/ops/plugin.rs | 156 +++ runtime/ops/process.rs | 290 ++++++ runtime/ops/runtime.rs | 118 +++ runtime/ops/signal.rs | 142 +++ runtime/ops/timers.rs | 193 ++++ runtime/ops/tls.rs | 431 +++++++++ runtime/ops/tty.rs | 334 +++++++ runtime/ops/web_worker.rs | 37 + runtime/ops/websocket.rs | 326 +++++++ runtime/ops/worker_host.rs | 318 +++++++ runtime/permissions.rs | 1108 ++++++++++++++++++++++ runtime/resolve_addr.rs | 72 ++ runtime/rt/00_bootstrap_namespace.js | 9 + runtime/rt/01_build.js | 26 + runtime/rt/01_colors.js | 92 ++ runtime/rt/01_errors.js | 162 ++++ runtime/rt/01_internals.js | 23 + runtime/rt/01_version.js | 26 + runtime/rt/01_web_util.js | 160 ++++ runtime/rt/02_console.js | 1732 ++++++++++++++++++++++++++++++++++ runtime/rt/06_util.js | 148 +++ runtime/rt/10_dispatch_minimal.js | 114 +++ runtime/rt/11_timers.js | 557 +++++++++++ runtime/rt/11_workers.js | 206 ++++ runtime/rt/12_io.js | 135 +++ runtime/rt/13_buffer.js | 241 +++++ runtime/rt/27_websocket.js | 316 +++++++ runtime/rt/30_files.js | 209 ++++ runtime/rt/30_fs.js | 425 +++++++++ runtime/rt/30_metrics.js | 13 + runtime/rt/30_net.js | 245 +++++ runtime/rt/30_os.js | 66 ++ runtime/rt/40_compiler_api.js | 97 ++ runtime/rt/40_diagnostics.js | 23 + runtime/rt/40_error_stack.js | 23 + runtime/rt/40_fs_events.js | 52 + runtime/rt/40_net_unstable.js | 48 + runtime/rt/40_performance.js | 341 +++++++ runtime/rt/40_permissions.js | 65 ++ runtime/rt/40_plugins.js | 13 + runtime/rt/40_process.js | 122 +++ runtime/rt/40_read_file.js | 43 + runtime/rt/40_signals.js | 256 +++++ runtime/rt/40_testing.js | 350 +++++++ runtime/rt/40_tls.js | 82 ++ runtime/rt/40_tty.js | 28 + runtime/rt/40_write_file.js | 92 ++ runtime/rt/41_prompt.js | 80 ++ runtime/rt/90_deno_ns.js | 137 +++ runtime/rt/99_main.js | 395 ++++++++ runtime/rt/README.md | 59 ++ runtime/tokio_util.rs | 25 + runtime/web_worker.rs | 595 ++++++++++++ runtime/worker.rs | 349 +++++++ 164 files changed, 17338 insertions(+), 16669 deletions(-) delete mode 100644 cli/inspector.rs delete mode 100644 cli/metrics.rs delete mode 100644 cli/ops/crypto.rs delete mode 100644 cli/ops/dispatch_minimal.rs delete mode 100644 cli/ops/fetch.rs delete mode 100644 cli/ops/fs.rs delete mode 100644 cli/ops/fs_events.rs delete mode 100644 cli/ops/io.rs delete mode 100644 cli/ops/net.rs delete mode 100644 cli/ops/net_unix.rs delete mode 100644 cli/ops/os.rs delete mode 100644 cli/ops/permissions.rs delete mode 100644 cli/ops/plugin.rs delete mode 100644 cli/ops/process.rs delete mode 100644 cli/ops/runtime.rs delete mode 100644 cli/ops/signal.rs delete mode 100644 cli/ops/timers.rs delete mode 100644 cli/ops/tls.rs delete mode 100644 cli/ops/tty.rs delete mode 100644 cli/ops/web_worker.rs delete mode 100644 cli/ops/websocket.rs delete mode 100644 cli/ops/worker_host.rs delete mode 100644 cli/permissions.rs delete mode 100644 cli/resolve_addr.rs delete mode 100644 cli/rt/00_bootstrap_namespace.js delete mode 100644 cli/rt/01_build.js delete mode 100644 cli/rt/01_colors.js delete mode 100644 cli/rt/01_errors.js delete mode 100644 cli/rt/01_internals.js delete mode 100644 cli/rt/01_version.js delete mode 100644 cli/rt/01_web_util.js delete mode 100644 cli/rt/02_console.js delete mode 100644 cli/rt/06_util.js delete mode 100644 cli/rt/10_dispatch_minimal.js delete mode 100644 cli/rt/11_timers.js delete mode 100644 cli/rt/11_workers.js delete mode 100644 cli/rt/12_io.js delete mode 100644 cli/rt/13_buffer.js delete mode 100644 cli/rt/27_websocket.js delete mode 100644 cli/rt/30_files.js delete mode 100644 cli/rt/30_fs.js delete mode 100644 cli/rt/30_metrics.js delete mode 100644 cli/rt/30_net.js delete mode 100644 cli/rt/30_os.js delete mode 100644 cli/rt/40_compiler_api.js delete mode 100644 cli/rt/40_diagnostics.js delete mode 100644 cli/rt/40_error_stack.js delete mode 100644 cli/rt/40_fs_events.js delete mode 100644 cli/rt/40_net_unstable.js delete mode 100644 cli/rt/40_performance.js delete mode 100644 cli/rt/40_permissions.js delete mode 100644 cli/rt/40_plugins.js delete mode 100644 cli/rt/40_process.js delete mode 100644 cli/rt/40_read_file.js delete mode 100644 cli/rt/40_signals.js delete mode 100644 cli/rt/40_testing.js delete mode 100644 cli/rt/40_tls.js delete mode 100644 cli/rt/40_tty.js delete mode 100644 cli/rt/40_write_file.js delete mode 100644 cli/rt/41_prompt.js delete mode 100644 cli/rt/90_deno_ns.js delete mode 100644 cli/rt/99_main.js delete mode 100644 cli/rt/README.md delete mode 100644 cli/signal.rs delete mode 100644 cli/web_worker.rs delete mode 100644 cli/worker.rs create mode 100644 runtime/Cargo.toml create mode 100644 runtime/README.md create mode 100644 runtime/build.rs create mode 100644 runtime/colors.rs create mode 100644 runtime/errors.rs create mode 100644 runtime/examples/hello_runtime.js create mode 100644 runtime/examples/hello_runtime.rs create mode 100644 runtime/fs_util.rs create mode 100644 runtime/http_util.rs create mode 100644 runtime/inspector.rs create mode 100644 runtime/js.rs create mode 100644 runtime/lib.rs create mode 100644 runtime/metrics.rs create mode 100644 runtime/ops/crypto.rs create mode 100644 runtime/ops/dispatch_minimal.rs create mode 100644 runtime/ops/fetch.rs create mode 100644 runtime/ops/fs.rs create mode 100644 runtime/ops/fs_events.rs create mode 100644 runtime/ops/io.rs create mode 100644 runtime/ops/mod.rs create mode 100644 runtime/ops/net.rs create mode 100644 runtime/ops/net_unix.rs create mode 100644 runtime/ops/os.rs create mode 100644 runtime/ops/permissions.rs create mode 100644 runtime/ops/plugin.rs create mode 100644 runtime/ops/process.rs create mode 100644 runtime/ops/runtime.rs create mode 100644 runtime/ops/signal.rs create mode 100644 runtime/ops/timers.rs create mode 100644 runtime/ops/tls.rs create mode 100644 runtime/ops/tty.rs create mode 100644 runtime/ops/web_worker.rs create mode 100644 runtime/ops/websocket.rs create mode 100644 runtime/ops/worker_host.rs create mode 100644 runtime/permissions.rs create mode 100644 runtime/resolve_addr.rs create mode 100644 runtime/rt/00_bootstrap_namespace.js create mode 100644 runtime/rt/01_build.js create mode 100644 runtime/rt/01_colors.js create mode 100644 runtime/rt/01_errors.js create mode 100644 runtime/rt/01_internals.js create mode 100644 runtime/rt/01_version.js create mode 100644 runtime/rt/01_web_util.js create mode 100644 runtime/rt/02_console.js create mode 100644 runtime/rt/06_util.js create mode 100644 runtime/rt/10_dispatch_minimal.js create mode 100644 runtime/rt/11_timers.js create mode 100644 runtime/rt/11_workers.js create mode 100644 runtime/rt/12_io.js create mode 100644 runtime/rt/13_buffer.js create mode 100644 runtime/rt/27_websocket.js create mode 100644 runtime/rt/30_files.js create mode 100644 runtime/rt/30_fs.js create mode 100644 runtime/rt/30_metrics.js create mode 100644 runtime/rt/30_net.js create mode 100644 runtime/rt/30_os.js create mode 100644 runtime/rt/40_compiler_api.js create mode 100644 runtime/rt/40_diagnostics.js create mode 100644 runtime/rt/40_error_stack.js create mode 100644 runtime/rt/40_fs_events.js create mode 100644 runtime/rt/40_net_unstable.js create mode 100644 runtime/rt/40_performance.js create mode 100644 runtime/rt/40_permissions.js create mode 100644 runtime/rt/40_plugins.js create mode 100644 runtime/rt/40_process.js create mode 100644 runtime/rt/40_read_file.js create mode 100644 runtime/rt/40_signals.js create mode 100644 runtime/rt/40_testing.js create mode 100644 runtime/rt/40_tls.js create mode 100644 runtime/rt/40_tty.js create mode 100644 runtime/rt/40_write_file.js create mode 100644 runtime/rt/41_prompt.js create mode 100644 runtime/rt/90_deno_ns.js create mode 100644 runtime/rt/99_main.js create mode 100644 runtime/rt/README.md create mode 100644 runtime/tokio_util.rs create mode 100644 runtime/web_worker.rs create mode 100644 runtime/worker.rs diff --git a/Cargo.lock b/Cargo.lock index 852c48289..1ed126df5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -460,13 +460,12 @@ dependencies = [ "clap", "crossbeam-channel 0.5.0", "deno_core", - "deno_crypto", "deno_doc", "deno_fetch", "deno_lint", + "deno_runtime", "deno_web", "dissimilar", - "dlopen", "dprint-plugin-typescript", "encoding_rs", "env_logger", @@ -496,7 +495,6 @@ dependencies = [ "swc_bundler", "swc_common", "swc_ecmascript", - "sys-info", "tempfile", "termcolor", "test_util", @@ -506,8 +504,6 @@ dependencies = [ "uuid", "walkdir", "warp", - "webpki", - "webpki-roots", "winapi 0.3.9", "winres", ] @@ -582,6 +578,48 @@ dependencies = [ "swc_ecmascript", ] +[[package]] +name = "deno_runtime" +version = "0.1.0" +dependencies = [ + "atty", + "deno_core", + "deno_crypto", + "deno_fetch", + "deno_web", + "dlopen", + "encoding_rs", + "env_logger", + "filetime", + "fwdansi", + "http", + "indexmap", + "lazy_static", + "libc", + "log", + "nix", + "notify", + "percent-encoding", + "regex", + "ring", + "rustyline", + "rustyline-derive", + "serde", + "shell-escape", + "sys-info", + "termcolor", + "test_util", + "tokio 0.2.22", + "tokio-rustls", + "tokio-tungstenite", + "uuid", + "warp", + "webpki", + "webpki-roots", + "winapi 0.3.9", + "winres", +] + [[package]] name = "deno_web" version = "0.21.0" diff --git a/Cargo.toml b/Cargo.toml index 3281be1af..b7b8adae5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "cli", "core", + "runtime", "test_plugin", "test_util", "op_crates/fetch", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 7bc6221dd..7ad0cc5ca 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -20,10 +20,9 @@ harness = false path = "./bench/main.rs" [build-dependencies] -deno_crypto = { path = "../op_crates/crypto", version = "0.4.0" } deno_core = { path = "../core", version = "0.70.0" } -deno_web = { path = "../op_crates/web", version = "0.21.0" } deno_fetch = { path = "../op_crates/fetch", version = "0.13.0" } +deno_web = { path = "../op_crates/web", version = "0.21.0" } regex = "1.3.9" serde = { version = "1.0.116", features = ["derive"] } @@ -33,11 +32,9 @@ winapi = "0.3.9" [dependencies] deno_core = { path = "../core", version = "0.70.0" } -deno_crypto = { path = "../op_crates/crypto", version = "0.4.0" } deno_doc = "0.1.18" -deno_fetch = { path = "../op_crates/fetch", version = "0.13.0" } deno_lint = "0.2.13" -deno_web = { path = "../op_crates/web", version = "0.21.0" } +deno_runtime = { path = "../runtime", version = "0.1.0" } atty = "0.2.14" base64 = "0.12.3" @@ -46,7 +43,6 @@ byteorder = "1.3.4" clap = "2.33.3" crossbeam-channel = "0.5.0" dissimilar = "1.0.2" -dlopen = "0.1.8" dprint-plugin-typescript = "0.35.1" encoding_rs = "0.8.24" env_logger = "0.7.1" @@ -72,7 +68,6 @@ sourcemap = "6.0.1" swc_bundler = "0.17.6" swc_common = { version = "0.10.7", features = ["sourcemap"] } swc_ecmascript = { version = "0.15.0", features = ["codegen", "dep_graph", "parser", "react", "transforms", "visit"] } -sys-info = "0.7.0" tempfile = "3.1.0" termcolor = "1.1.0" tokio = { version = "0.2.22", features = ["full"] } @@ -82,8 +77,6 @@ tokio-tungstenite = "0.11.0" uuid = { version = "0.8.1", features = ["v4"] } walkdir = "2.3.1" warp = { version = "0.2.5", features = ["tls"] } -webpki = "0.21.3" -webpki-roots = "=0.19.0" # Pinned to v0.19.0 to match 'reqwest'. [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = ["knownfolders", "mswsock", "objbase", "shlobj", "tlhelp32", "winbase", "winerror", "winsock2"] } diff --git a/cli/build.rs b/cli/build.rs index b0088a9d8..4be71bb9e 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -13,14 +13,13 @@ use std::env; use std::path::Path; use std::path::PathBuf; +// TODO(bartlomieju): this module contains a lot of duplicated +// logic with `runtime/build.rs`, factor out to `deno_core`. fn create_snapshot( mut js_runtime: JsRuntime, snapshot_path: &Path, files: Vec, ) { - deno_web::init(&mut js_runtime); - deno_fetch::init(&mut js_runtime); - deno_crypto::init(&mut js_runtime); // TODO(nayeemrmn): https://github.com/rust-lang/cargo/issues/3946 to get the // workspace root. let display_root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(); @@ -43,14 +42,6 @@ fn create_snapshot( println!("Snapshot written to: {} ", snapshot_path.display()); } -fn create_runtime_snapshot(snapshot_path: &Path, files: Vec) { - let js_runtime = JsRuntime::new(RuntimeOptions { - will_snapshot: true, - ..Default::default() - }); - create_snapshot(js_runtime, snapshot_path, files); -} - #[derive(Debug, Deserialize)] struct LoadArgs { /// The fully qualified specifier that should be loaded. @@ -265,12 +256,8 @@ fn main() { let o = PathBuf::from(env::var_os("OUT_DIR").unwrap()); // Main snapshot - let runtime_snapshot_path = o.join("CLI_SNAPSHOT.bin"); let compiler_snapshot_path = o.join("COMPILER_SNAPSHOT.bin"); - let js_files = get_js_files("rt"); - create_runtime_snapshot(&runtime_snapshot_path, js_files); - let js_files = get_js_files("tsc"); create_compiler_snapshot(&compiler_snapshot_path, js_files, &c); diff --git a/cli/errors.rs b/cli/errors.rs index 869cdee2c..29fd428c8 100644 --- a/cli/errors.rs +++ b/cli/errors.rs @@ -12,218 +12,25 @@ use crate::ast::DiagnosticBuffer; use crate::import_map::ImportMapError; use deno_core::error::AnyError; -use deno_core::serde_json; -use deno_core::url; -use deno_core::ModuleResolutionError; -use deno_fetch::reqwest; -use rustyline::error::ReadlineError; -use std::env; -use std::error::Error; -use std::io; - -fn get_dlopen_error_class(error: &dlopen::Error) -> &'static str { - use dlopen::Error::*; - match error { - NullCharacter(_) => "InvalidData", - OpeningLibraryError(ref e) => get_io_error_class(e), - SymbolGettingError(ref e) => get_io_error_class(e), - AddrNotMatchingDll(ref e) => get_io_error_class(e), - NullSymbol => "NotFound", - } -} - -fn get_env_var_error_class(error: &env::VarError) -> &'static str { - use env::VarError::*; - match error { - NotPresent => "NotFound", - NotUnicode(..) => "InvalidData", - } -} fn get_import_map_error_class(_: &ImportMapError) -> &'static str { "URIError" } -fn get_io_error_class(error: &io::Error) -> &'static str { - use io::ErrorKind::*; - match error.kind() { - NotFound => "NotFound", - PermissionDenied => "PermissionDenied", - ConnectionRefused => "ConnectionRefused", - ConnectionReset => "ConnectionReset", - ConnectionAborted => "ConnectionAborted", - NotConnected => "NotConnected", - AddrInUse => "AddrInUse", - AddrNotAvailable => "AddrNotAvailable", - BrokenPipe => "BrokenPipe", - AlreadyExists => "AlreadyExists", - InvalidInput => "TypeError", - InvalidData => "InvalidData", - TimedOut => "TimedOut", - Interrupted => "Interrupted", - WriteZero => "WriteZero", - UnexpectedEof => "UnexpectedEof", - Other => "Error", - WouldBlock => unreachable!(), - // Non-exhaustive enum - might add new variants - // in the future - _ => unreachable!(), - } -} - -fn get_module_resolution_error_class( - _: &ModuleResolutionError, -) -> &'static str { - "URIError" -} - -fn get_notify_error_class(error: ¬ify::Error) -> &'static str { - use notify::ErrorKind::*; - match error.kind { - Generic(_) => "Error", - Io(ref e) => get_io_error_class(e), - PathNotFound => "NotFound", - WatchNotFound => "NotFound", - InvalidConfig(_) => "InvalidData", - } -} - -fn get_readline_error_class(error: &ReadlineError) -> &'static str { - use ReadlineError::*; - match error { - Io(err) => get_io_error_class(err), - Eof => "UnexpectedEof", - Interrupted => "Interrupted", - #[cfg(unix)] - Errno(err) => get_nix_error_class(err), - _ => unimplemented!(), - } -} - -fn get_regex_error_class(error: ®ex::Error) -> &'static str { - use regex::Error::*; - match error { - Syntax(_) => "SyntaxError", - CompiledTooBig(_) => "RangeError", - _ => "Error", - } -} - -fn get_request_error_class(error: &reqwest::Error) -> &'static str { - error - .source() - .and_then(|inner_err| { - (inner_err - .downcast_ref::() - .map(get_io_error_class)) - .or_else(|| { - inner_err - .downcast_ref::() - .map(get_serde_json_error_class) - }) - .or_else(|| { - inner_err - .downcast_ref::() - .map(get_url_parse_error_class) - }) - }) - .unwrap_or("Http") -} - -fn get_serde_json_error_class( - error: &serde_json::error::Error, -) -> &'static str { - use deno_core::serde_json::error::*; - match error.classify() { - Category::Io => error - .source() - .and_then(|e| e.downcast_ref::()) - .map(get_io_error_class) - .unwrap(), - Category::Syntax => "SyntaxError", - Category::Data => "InvalidData", - Category::Eof => "UnexpectedEof", - } -} - fn get_diagnostic_class(_: &DiagnosticBuffer) -> &'static str { "SyntaxError" } -fn get_url_parse_error_class(_error: &url::ParseError) -> &'static str { - "URIError" -} - -#[cfg(unix)] -fn get_nix_error_class(error: &nix::Error) -> &'static str { - use nix::errno::Errno::*; - match error { - nix::Error::Sys(ECHILD) => "NotFound", - nix::Error::Sys(EINVAL) => "TypeError", - nix::Error::Sys(ENOENT) => "NotFound", - nix::Error::Sys(ENOTTY) => "BadResource", - nix::Error::Sys(EPERM) => "PermissionDenied", - nix::Error::Sys(ESRCH) => "NotFound", - nix::Error::Sys(UnknownErrno) => "Error", - nix::Error::Sys(_) => "Error", - nix::Error::InvalidPath => "TypeError", - nix::Error::InvalidUtf8 => "InvalidData", - nix::Error::UnsupportedOperation => unreachable!(), - } -} - pub(crate) fn get_error_class_name(e: &AnyError) -> &'static str { - deno_core::error::get_custom_error_class(e) - .or_else(|| { - e.downcast_ref::() - .map(get_dlopen_error_class) - }) - .or_else(|| { - e.downcast_ref::() - .map(get_env_var_error_class) - }) + deno_runtime::errors::get_error_class_name(e) .or_else(|| { e.downcast_ref::() .map(get_import_map_error_class) }) - .or_else(|| e.downcast_ref::().map(get_io_error_class)) - .or_else(|| { - e.downcast_ref::() - .map(get_module_resolution_error_class) - }) - .or_else(|| { - e.downcast_ref::() - .map(get_notify_error_class) - }) - .or_else(|| { - e.downcast_ref::() - .map(get_readline_error_class) - }) - .or_else(|| { - e.downcast_ref::() - .map(get_request_error_class) - }) - .or_else(|| e.downcast_ref::().map(get_regex_error_class)) - .or_else(|| { - e.downcast_ref::() - .map(get_serde_json_error_class) - }) .or_else(|| { e.downcast_ref::() .map(get_diagnostic_class) }) - .or_else(|| { - e.downcast_ref::() - .map(get_url_parse_error_class) - }) - .or_else(|| { - #[cfg(unix)] - let maybe_get_nix_error_class = - || e.downcast_ref::().map(get_nix_error_class); - #[cfg(not(unix))] - let maybe_get_nix_error_class = || Option::<&'static str>::None; - (maybe_get_nix_error_class)() - }) .unwrap_or_else(|| { panic!("Error '{}' contains boxed error of unknown type", e); }) diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs index 7b730e455..86c0ac966 100644 --- a/cli/file_fetcher.rs +++ b/cli/file_fetcher.rs @@ -7,8 +7,8 @@ use crate::http_util::fetch_once; use crate::http_util::get_user_agent; use crate::http_util::FetchOnceResult; use crate::media_type::MediaType; -use crate::permissions::Permissions; use crate::text_encoding; +use deno_runtime::permissions::Permissions; use deno_core::error::custom_error; use deno_core::error::generic_error; @@ -17,7 +17,7 @@ use deno_core::error::AnyError; use deno_core::futures; use deno_core::futures::future::FutureExt; use deno_core::ModuleSpecifier; -use deno_fetch::reqwest; +use deno_runtime::deno_fetch::reqwest; use std::collections::HashMap; use std::fs; use std::future::Future; diff --git a/cli/file_watcher.rs b/cli/file_watcher.rs index 4aa93c581..ef7aae603 100644 --- a/cli/file_watcher.rs +++ b/cli/file_watcher.rs @@ -240,16 +240,17 @@ fn new_watcher( ) -> Result { let event_detected = Arc::clone(&debounce.event_detected); - let mut watcher: RecommendedWatcher = Watcher::new_immediate( - move |res: Result| { + let mut watcher: RecommendedWatcher = + Watcher::new_immediate(move |res: Result| { if let Ok(event) = res { - if matches!(event.kind, EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)) - { + if matches!( + event.kind, + EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) + ) { event_detected.store(true, Ordering::Relaxed); } } - }, - )?; + })?; watcher.configure(Config::PreciseEvents(true)).unwrap(); diff --git a/cli/http_util.rs b/cli/http_util.rs index 4bd59e32b..97e3453ec 100644 --- a/cli/http_util.rs +++ b/cli/http_util.rs @@ -6,16 +6,16 @@ use deno_core::error::generic_error; use deno_core::error::AnyError; use deno_core::futures; use deno_core::url::Url; -use deno_fetch::reqwest; -use deno_fetch::reqwest::header::HeaderMap; -use deno_fetch::reqwest::header::HeaderValue; -use deno_fetch::reqwest::header::IF_NONE_MATCH; -use deno_fetch::reqwest::header::LOCATION; -use deno_fetch::reqwest::header::USER_AGENT; -use deno_fetch::reqwest::redirect::Policy; -use deno_fetch::reqwest::Client; -use deno_fetch::reqwest::Response; -use deno_fetch::reqwest::StatusCode; +use deno_runtime::deno_fetch::reqwest; +use deno_runtime::deno_fetch::reqwest::header::HeaderMap; +use deno_runtime::deno_fetch::reqwest::header::HeaderValue; +use deno_runtime::deno_fetch::reqwest::header::IF_NONE_MATCH; +use deno_runtime::deno_fetch::reqwest::header::LOCATION; +use deno_runtime::deno_fetch::reqwest::header::USER_AGENT; +use deno_runtime::deno_fetch::reqwest::redirect::Policy; +use deno_runtime::deno_fetch::reqwest::Client; +use deno_runtime::deno_fetch::reqwest::Response; +use deno_runtime::deno_fetch::reqwest::StatusCode; use std::cmp::min; use std::collections::HashMap; use std::fs::File; diff --git a/cli/inspector.rs b/cli/inspector.rs deleted file mode 100644 index 89fd5bf57..000000000 --- a/cli/inspector.rs +++ /dev/null @@ -1,952 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -//! The documentation for the inspector API is sparse, but these are helpful: -//! https://chromedevtools.github.io/devtools-protocol/ -//! https://hyperandroid.com/2020/02/12/v8-inspector-from-an-embedder-standpoint/ - -use core::convert::Infallible as Never; // Alias for the future `!` type. -use deno_core::error::generic_error; -use deno_core::error::AnyError; -use deno_core::futures::channel::mpsc; -use deno_core::futures::channel::mpsc::UnboundedReceiver; -use deno_core::futures::channel::mpsc::UnboundedSender; -use deno_core::futures::channel::oneshot; -use deno_core::futures::future::Future; -use deno_core::futures::pin_mut; -use deno_core::futures::prelude::*; -use deno_core::futures::select; -use deno_core::futures::stream::FuturesUnordered; -use deno_core::futures::task; -use deno_core::futures::task::Context; -use deno_core::futures::task::Poll; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use deno_core::v8; -use std::cell::BorrowMutError; -use std::cell::RefCell; -use std::collections::HashMap; -use std::ffi::c_void; -use std::mem::replace; -use std::mem::take; -use std::mem::MaybeUninit; -use std::net::SocketAddr; -use std::ops::Deref; -use std::ops::DerefMut; -use std::pin::Pin; -use std::process; -use std::ptr; -use std::ptr::NonNull; -use std::sync::Arc; -use std::sync::Mutex; -use std::thread; -use uuid::Uuid; -use warp::filters::ws; -use warp::Filter; - -pub struct InspectorServer { - pub host: SocketAddr, - register_inspector_tx: UnboundedSender, - shutdown_server_tx: Option>, - thread_handle: Option>, -} - -impl InspectorServer { - pub fn new(host: SocketAddr, name: String) -> Self { - let (register_inspector_tx, register_inspector_rx) = - mpsc::unbounded::(); - - let (shutdown_server_tx, shutdown_server_rx) = oneshot::channel(); - - let thread_handle = thread::spawn(move || { - crate::tokio_util::run_basic(server( - host, - register_inspector_rx, - shutdown_server_rx, - name, - )) - }); - - Self { - host, - register_inspector_tx, - shutdown_server_tx: Some(shutdown_server_tx), - thread_handle: Some(thread_handle), - } - } - - fn register_inspector(&self, info: InspectorInfo) { - self.register_inspector_tx.unbounded_send(info).unwrap(); - } -} - -impl Drop for InspectorServer { - fn drop(&mut self) { - if let Some(shutdown_server_tx) = self.shutdown_server_tx.take() { - shutdown_server_tx - .send(()) - .expect("unable to send shutdown signal"); - } - - if let Some(thread_handle) = self.thread_handle.take() { - thread_handle.join().expect("unable to join thread"); - } - } -} - -/// Inspector information that is sent from the isolate thread to the server -/// thread when a new inspector is created. -struct InspectorInfo { - host: SocketAddr, - uuid: Uuid, - thread_name: Option, - new_websocket_tx: UnboundedSender, - canary_rx: oneshot::Receiver, -} - -impl InspectorInfo { - fn get_json_metadata(&self) -> Value { - json!({ - "description": "deno", - "devtoolsFrontendUrl": self.get_frontend_url(), - "faviconUrl": "https://deno.land/favicon.ico", - "id": self.uuid.to_string(), - "title": self.get_title(), - "type": "deno", - // TODO(ry): "url": "file://", - "webSocketDebuggerUrl": self.get_websocket_debugger_url(), - }) - } - - fn get_websocket_debugger_url(&self) -> String { - format!("ws://{}/ws/{}", &self.host, &self.uuid) - } - - fn get_frontend_url(&self) -> String { - format!( - "devtools://devtools/bundled/inspector.html?v8only=true&ws={}/ws/{}", - &self.host, &self.uuid - ) - } - - fn get_title(&self) -> String { - format!( - "[{}] deno{}", - process::id(), - self - .thread_name - .as_ref() - .map(|n| format!(" - {}", n)) - .unwrap_or_default() - ) - } -} - -async fn server( - host: SocketAddr, - register_inspector_rx: UnboundedReceiver, - shutdown_server_rx: oneshot::Receiver<()>, - name: String, -) { - // TODO: put the `inspector_map` in an `Rc>` instead. This is - // currently not possible because warp requires all filters to implement - // `Send`, which should not be necessary because we are using the - // single-threaded Tokio runtime. - let inspector_map = HashMap::::new(); - let inspector_map = Arc::new(Mutex::new(inspector_map)); - - let inspector_map_ = inspector_map.clone(); - let register_inspector_handler = register_inspector_rx - .map(|info| { - eprintln!( - "Debugger listening on {}", - info.get_websocket_debugger_url() - ); - let mut g = inspector_map_.lock().unwrap(); - if g.insert(info.uuid, info).is_some() { - panic!("Inspector UUID already in map"); - } - }) - .collect::<()>(); - - let inspector_map_ = inspector_map_.clone(); - let deregister_inspector_handler = future::poll_fn(|cx| { - let mut g = inspector_map_.lock().unwrap(); - g.retain(|_, info| info.canary_rx.poll_unpin(cx) == Poll::Pending); - Poll::::Pending - }) - .fuse(); - - let inspector_map_ = inspector_map.clone(); - let websocket_route = warp::path("ws") - .and(warp::path::param()) - .and(warp::ws()) - .and_then(move |uuid: String, ws: warp::ws::Ws| { - future::ready( - Uuid::parse_str(&uuid) - .ok() - .and_then(|uuid| { - let g = inspector_map_.lock().unwrap(); - g.get(&uuid).map(|info| info.new_websocket_tx.clone()).map( - |new_websocket_tx| { - ws.on_upgrade(move |websocket| async move { - let (proxy, pump) = create_websocket_proxy(websocket); - let _ = new_websocket_tx.unbounded_send(proxy); - pump.await; - }) - }, - ) - }) - .ok_or_else(warp::reject::not_found), - ) - }); - - let json_version_response = json!({ - "Browser": name, - "Protocol-Version": "1.3", - "V8-Version": deno_core::v8_version(), - }); - let json_version_route = warp::path!("json" / "version") - .map(move || warp::reply::json(&json_version_response)); - - let inspector_map_ = inspector_map.clone(); - let json_list_route = warp::path("json").map(move || { - let g = inspector_map_.lock().unwrap(); - let json_values = g - .values() - .map(|info| info.get_json_metadata()) - .collect::>(); - warp::reply::json(&json!(json_values)) - }); - - let server_routes = - websocket_route.or(json_version_route).or(json_list_route); - let server_handler = warp::serve(server_routes) - .try_bind_with_graceful_shutdown(host, async { - shutdown_server_rx.await.ok(); - }) - .map(|(_, fut)| fut) - .unwrap_or_else(|err| { - eprintln!("Cannot start inspector server: {}.", err); - process::exit(1); - }) - .fuse(); - - pin_mut!(register_inspector_handler); - pin_mut!(deregister_inspector_handler); - pin_mut!(server_handler); - - select! { - _ = register_inspector_handler => {}, - _ = deregister_inspector_handler => unreachable!(), - _ = server_handler => {}, - } -} - -type WebSocketProxySender = UnboundedSender; -type WebSocketProxyReceiver = - UnboundedReceiver>; - -/// Encapsulates an UnboundedSender/UnboundedReceiver pair that together form -/// a duplex channel for sending/receiving websocket messages. -struct WebSocketProxy { - tx: WebSocketProxySender, - rx: WebSocketProxyReceiver, -} - -impl WebSocketProxy { - pub fn split(self) -> (WebSocketProxySender, WebSocketProxyReceiver) { - (self.tx, self.rx) - } -} - -/// Creates a future that proxies messages sent and received on a warp WebSocket -/// to a UnboundedSender/UnboundedReceiver pair. We need this to sidestep -/// Tokio's task budget, which causes issues when DenoInspector::poll_sessions() -/// needs to block the thread because JavaScript execution is paused. -/// -/// This works because UnboundedSender/UnboundedReceiver are implemented in the -/// 'futures' crate, therefore they can't participate in Tokio's cooperative -/// task yielding. -/// -/// A tuple is returned, where the first element is a duplex channel that can -/// be used to send/receive messages on the websocket, and the second element -/// is a future that does the forwarding. -fn create_websocket_proxy( - websocket: ws::WebSocket, -) -> (WebSocketProxy, impl Future + Send) { - // The 'outbound' channel carries messages sent to the websocket. - let (outbound_tx, outbound_rx) = mpsc::unbounded(); - - // The 'inbound' channel carries messages received from the websocket. - let (inbound_tx, inbound_rx) = mpsc::unbounded(); - - let proxy = WebSocketProxy { - tx: outbound_tx, - rx: inbound_rx, - }; - - // The pump future takes care of forwarding messages between the websocket - // and channels. It resolves to () when either side disconnects, ignoring any - // errors. - let pump = async move { - let (websocket_tx, websocket_rx) = websocket.split(); - - let outbound_pump = - outbound_rx.map(Ok).forward(websocket_tx).map_err(|_| ()); - - let inbound_pump = websocket_rx - .map(|msg| inbound_tx.unbounded_send(msg)) - .map_err(|_| ()) - .try_collect::<()>(); - - let _ = future::try_join(outbound_pump, inbound_pump).await; - }; - - (proxy, pump) -} - -#[derive(Clone, Copy)] -enum PollState { - Idle, - Woken, - Polling, - Parked, - Dropped, -} - -pub struct DenoInspector { - v8_inspector_client: v8::inspector::V8InspectorClientBase, - v8_inspector: v8::UniqueRef, - sessions: RefCell, - flags: RefCell, - waker: Arc, - _canary_tx: oneshot::Sender, - pub server: Option>, - pub debugger_url: Option, -} - -impl Deref for DenoInspector { - type Target = v8::inspector::V8Inspector; - fn deref(&self) -> &Self::Target { - &self.v8_inspector - } -} - -impl DerefMut for DenoInspector { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.v8_inspector - } -} - -impl Drop for DenoInspector { - fn drop(&mut self) { - // Since the waker is cloneable, it might outlive the inspector itself. - // Set the poll state to 'dropped' so it doesn't attempt to request an - // interrupt from the isolate. - self.waker.update(|w| w.poll_state = PollState::Dropped); - // V8 automatically deletes all sessions when an Inspector instance is - // deleted, however InspectorSession also has a drop handler that cleans - // up after itself. To avoid a double free, make sure the inspector is - // dropped last. - take(&mut *self.sessions.borrow_mut()); - } -} - -impl v8::inspector::V8InspectorClientImpl for DenoInspector { - fn base(&self) -> &v8::inspector::V8InspectorClientBase { - &self.v8_inspector_client - } - - fn base_mut(&mut self) -> &mut v8::inspector::V8InspectorClientBase { - &mut self.v8_inspector_client - } - - fn run_message_loop_on_pause(&mut self, context_group_id: i32) { - assert_eq!(context_group_id, DenoInspectorSession::CONTEXT_GROUP_ID); - self.flags.borrow_mut().on_pause = true; - let _ = self.poll_sessions(None); - } - - fn quit_message_loop_on_pause(&mut self) { - self.flags.borrow_mut().on_pause = false; - } - - fn run_if_waiting_for_debugger(&mut self, context_group_id: i32) { - assert_eq!(context_group_id, DenoInspectorSession::CONTEXT_GROUP_ID); - self.flags.borrow_mut().session_handshake_done = true; - } -} - -/// DenoInspector implements a Future so that it can poll for new incoming -/// connections and messages from the WebSocket server. The Worker that owns -/// this DenoInspector will call our poll function from Worker::poll(). -impl Future for DenoInspector { - type Output = (); - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> { - self.poll_sessions(Some(cx)).unwrap() - } -} - -impl DenoInspector { - const CONTEXT_GROUP_ID: i32 = 1; - - pub fn new( - js_runtime: &mut deno_core::JsRuntime, - server: Option>, - ) -> Box { - let context = js_runtime.global_context(); - let scope = &mut v8::HandleScope::new(js_runtime.v8_isolate()); - - let (new_websocket_tx, new_websocket_rx) = - mpsc::unbounded::(); - let (canary_tx, canary_rx) = oneshot::channel::(); - - // Create DenoInspector instance. - let mut self_ = new_box_with(|self_ptr| { - let v8_inspector_client = - v8::inspector::V8InspectorClientBase::new::(); - let v8_inspector = - v8::inspector::V8Inspector::create(scope, unsafe { &mut *self_ptr }); - - let sessions = InspectorSessions::new(self_ptr, new_websocket_rx); - let flags = InspectorFlags::new(); - let waker = InspectorWaker::new(scope.thread_safe_handle()); - - let debugger_url = if let Some(server) = server.clone() { - let info = InspectorInfo { - host: server.host, - uuid: Uuid::new_v4(), - thread_name: thread::current().name().map(|n| n.to_owned()), - new_websocket_tx, - canary_rx, - }; - - let debugger_url = info.get_websocket_debugger_url(); - server.register_inspector(info); - Some(debugger_url) - } else { - None - }; - - Self { - v8_inspector_client, - v8_inspector, - sessions, - flags, - waker, - _canary_tx: canary_tx, - server, - debugger_url, - } - }); - - // Tell the inspector about the global context. - let context = v8::Local::new(scope, context); - let context_name = v8::inspector::StringView::from(&b"global context"[..]); - self_.context_created(context, Self::CONTEXT_GROUP_ID, context_name); - - // Poll the session handler so we will get notified whenever there is - // new_incoming debugger activity. - let _ = self_.poll_sessions(None).unwrap(); - - self_ - } - - fn poll_sessions( - &self, - mut invoker_cx: Option<&mut Context>, - ) -> Result, BorrowMutError> { - // Short-circuit if there is no server - if self.server.is_none() { - return Ok(Poll::Ready(())); - } - - // The futures this function uses do not have re-entrant poll() functions. - // However it is can happpen that poll_sessions() gets re-entered, e.g. - // when an interrupt request is honored while the inspector future is polled - // by the task executor. We let the caller know by returning some error. - let mut sessions = self.sessions.try_borrow_mut()?; - - self.waker.update(|w| { - match w.poll_state { - PollState::Idle | PollState::Woken => w.poll_state = PollState::Polling, - _ => unreachable!(), - }; - }); - - // Create a new Context object that will make downstream futures - // use the InspectorWaker when they are ready to be polled again. - let waker_ref = task::waker_ref(&self.waker); - let cx = &mut Context::from_waker(&waker_ref); - - loop { - loop { - // Do one "handshake" with a newly connected session at a time. - if let Some(session) = &mut sessions.handshake { - let poll_result = session.poll_unpin(cx); - let handshake_done = - replace(&mut self.flags.borrow_mut().session_handshake_done, false); - match poll_result { - Poll::Pending if handshake_done => { - let session = sessions.handshake.take().unwrap(); - sessions.established.push(session); - take(&mut self.flags.borrow_mut().waiting_for_session); - } - Poll::Ready(_) => sessions.handshake = None, - Poll::Pending => break, - }; - } - - // Accept new connections. - match sessions.new_incoming.poll_next_unpin(cx) { - Poll::Ready(Some(session)) => { - let prev = sessions.handshake.replace(session); - assert!(prev.is_none()); - continue; - } - Poll::Ready(None) => {} - Poll::Pending => {} - } - - // Poll established sessions. - match sessions.established.poll_next_unpin(cx) { - Poll::Ready(Some(_)) => continue, - Poll::Ready(None) => break, - Poll::Pending => break, - }; - } - - let should_block = sessions.handshake.is_some() - || self.flags.borrow().on_pause - || self.flags.borrow().waiting_for_session; - - let new_state = self.waker.update(|w| { - match w.poll_state { - PollState::Woken => { - // The inspector was woken while the session handler was being - // polled, so we poll it another time. - w.poll_state = PollState::Polling; - } - PollState::Polling if !should_block => { - // The session handler doesn't need to be polled any longer, and - // there's no reason to block (execution is not paused), so this - // function is about to return. - w.poll_state = PollState::Idle; - // Register the task waker that can be used to wake the parent - // task that will poll the inspector future. - if let Some(cx) = invoker_cx.take() { - w.task_waker.replace(cx.waker().clone()); - } - // Register the address of the inspector, which allows the waker - // to request an interrupt from the isolate. - w.inspector_ptr = NonNull::new(self as *const _ as *mut Self); - } - PollState::Polling if should_block => { - // Isolate execution has been paused but there are no more - // events to process, so this thread will be parked. Therefore, - // store the current thread handle in the waker so it knows - // which thread to unpark when new events arrive. - w.poll_state = PollState::Parked; - w.parked_thread.replace(thread::current()); - } - _ => unreachable!(), - }; - w.poll_state - }); - match new_state { - PollState::Idle => break Ok(Poll::Pending), // Yield to task. - PollState::Polling => {} // Poll the session handler again. - PollState::Parked => thread::park(), // Park the thread. - _ => unreachable!(), - }; - } - } - - /// This function blocks the thread until at least one inspector client has - /// established a websocket connection and successfully completed the - /// handshake. After that, it instructs V8 to pause at the next statement. - pub fn wait_for_session_and_break_on_next_statement(&mut self) { - loop { - match self.sessions.get_mut().established.iter_mut().next() { - Some(session) => break session.break_on_next_statement(), - None => { - self.flags.get_mut().waiting_for_session = true; - let _ = self.poll_sessions(None).unwrap(); - } - }; - } - } -} - -#[derive(Default)] -struct InspectorFlags { - waiting_for_session: bool, - session_handshake_done: bool, - on_pause: bool, -} - -impl InspectorFlags { - fn new() -> RefCell { - let self_ = Self::default(); - RefCell::new(self_) - } -} - -struct InspectorSessions { - new_incoming: - Pin> + 'static>>, - handshake: Option>, - established: FuturesUnordered>, -} - -impl InspectorSessions { - fn new( - inspector_ptr: *mut DenoInspector, - new_websocket_rx: UnboundedReceiver, - ) -> RefCell { - let new_incoming = new_websocket_rx - .map(move |websocket| DenoInspectorSession::new(inspector_ptr, websocket)) - .boxed_local(); - let self_ = Self { - new_incoming, - ..Default::default() - }; - RefCell::new(self_) - } -} - -impl Default for InspectorSessions { - fn default() -> Self { - Self { - new_incoming: stream::empty().boxed_local(), - handshake: None, - established: FuturesUnordered::new(), - } - } -} - -struct InspectorWakerInner { - poll_state: PollState, - task_waker: Option, - parked_thread: Option, - inspector_ptr: Option>, - isolate_handle: v8::IsolateHandle, -} - -unsafe impl Send for InspectorWakerInner {} - -struct InspectorWaker(Mutex); - -impl InspectorWaker { - fn new(isolate_handle: v8::IsolateHandle) -> Arc { - let inner = InspectorWakerInner { - poll_state: PollState::Idle, - task_waker: None, - parked_thread: None, - inspector_ptr: None, - isolate_handle, - }; - Arc::new(Self(Mutex::new(inner))) - } - - fn update(&self, update_fn: F) -> R - where - F: FnOnce(&mut InspectorWakerInner) -> R, - { - let mut g = self.0.lock().unwrap(); - update_fn(&mut g) - } -} - -impl task::ArcWake for InspectorWaker { - fn wake_by_ref(arc_self: &Arc) { - arc_self.update(|w| { - match w.poll_state { - PollState::Idle => { - // Wake the task, if any, that has polled the Inspector future last. - if let Some(waker) = w.task_waker.take() { - waker.wake() - } - // Request an interrupt from the isolate if it's running and there's - // not unhandled interrupt request in flight. - if let Some(arg) = w - .inspector_ptr - .take() - .map(|ptr| ptr.as_ptr() as *mut c_void) - { - w.isolate_handle.request_interrupt(handle_interrupt, arg); - } - extern "C" fn handle_interrupt( - _isolate: &mut v8::Isolate, - arg: *mut c_void, - ) { - let inspector = unsafe { &*(arg as *mut DenoInspector) }; - let _ = inspector.poll_sessions(None); - } - } - PollState::Parked => { - // Unpark the isolate thread. - let parked_thread = w.parked_thread.take().unwrap(); - assert_ne!(parked_thread.id(), thread::current().id()); - parked_thread.unpark(); - } - _ => {} - }; - w.poll_state = PollState::Woken; - }); - } -} - -struct DenoInspectorSession { - v8_channel: v8::inspector::ChannelBase, - v8_session: v8::UniqueRef, - websocket_tx: WebSocketProxySender, - websocket_rx_handler: Pin + 'static>>, -} - -impl Deref for DenoInspectorSession { - type Target = v8::inspector::V8InspectorSession; - fn deref(&self) -> &Self::Target { - &self.v8_session - } -} - -impl DerefMut for DenoInspectorSession { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.v8_session - } -} - -impl DenoInspectorSession { - const CONTEXT_GROUP_ID: i32 = 1; - - pub fn new( - inspector_ptr: *mut DenoInspector, - websocket: WebSocketProxy, - ) -> Box { - new_box_with(move |self_ptr| { - let v8_channel = v8::inspector::ChannelBase::new::(); - let v8_session = unsafe { &mut *inspector_ptr }.connect( - Self::CONTEXT_GROUP_ID, - // Todo(piscisaureus): V8Inspector::connect() should require that - // the 'v8_channel' argument cannot move. - unsafe { &mut *self_ptr }, - v8::inspector::StringView::empty(), - ); - - let (websocket_tx, websocket_rx) = websocket.split(); - let websocket_rx_handler = - Self::receive_from_websocket(self_ptr, websocket_rx); - - Self { - v8_channel, - v8_session, - websocket_tx, - websocket_rx_handler, - } - }) - } - - /// Returns a future that receives messages from the websocket and dispatches - /// them to the V8 session. - fn receive_from_websocket( - self_ptr: *mut Self, - websocket_rx: WebSocketProxyReceiver, - ) -> Pin + 'static>> { - async move { - eprintln!("Debugger session started."); - - let result = websocket_rx - .map_ok(move |msg| { - let msg = msg.as_bytes(); - let msg = v8::inspector::StringView::from(msg); - unsafe { &mut *self_ptr }.dispatch_protocol_message(msg); - }) - .try_collect::<()>() - .await; - - match result { - Ok(_) => eprintln!("Debugger session ended."), - Err(err) => eprintln!("Debugger session ended: {}.", err), - }; - } - .boxed_local() - } - - fn send_to_websocket(&self, msg: v8::UniquePtr) { - let msg = msg.unwrap().string().to_string(); - let msg = ws::Message::text(msg); - let _ = self.websocket_tx.unbounded_send(msg); - } - - pub fn break_on_next_statement(&mut self) { - let reason = v8::inspector::StringView::from(&b"debugCommand"[..]); - let detail = v8::inspector::StringView::empty(); - self.schedule_pause_on_next_statement(reason, detail); - } -} - -impl v8::inspector::ChannelImpl for DenoInspectorSession { - fn base(&self) -> &v8::inspector::ChannelBase { - &self.v8_channel - } - - fn base_mut(&mut self) -> &mut v8::inspector::ChannelBase { - &mut self.v8_channel - } - - fn send_response( - &mut self, - _call_id: i32, - message: v8::UniquePtr, - ) { - self.send_to_websocket(message); - } - - fn send_notification( - &mut self, - message: v8::UniquePtr, - ) { - self.send_to_websocket(message); - } - - fn flush_protocol_notifications(&mut self) {} -} - -impl Future for DenoInspectorSession { - type Output = (); - fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { - self.websocket_rx_handler.poll_unpin(cx) - } -} - -/// A local inspector session that can be used to send and receive protocol messages directly on -/// the same thread as an isolate. -pub struct InspectorSession { - v8_channel: v8::inspector::ChannelBase, - v8_session: v8::UniqueRef, - response_tx_map: HashMap>, - next_message_id: i32, - notification_queue: Vec, -} - -impl Deref for InspectorSession { - type Target = v8::inspector::V8InspectorSession; - fn deref(&self) -> &Self::Target { - &self.v8_session - } -} - -impl DerefMut for InspectorSession { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.v8_session - } -} - -impl v8::inspector::ChannelImpl for InspectorSession { - fn base(&self) -> &v8::inspector::ChannelBase { - &self.v8_channel - } - - fn base_mut(&mut self) -> &mut v8::inspector::ChannelBase { - &mut self.v8_channel - } - - fn send_response( - &mut self, - call_id: i32, - message: v8::UniquePtr, - ) { - let raw_message = message.unwrap().string().to_string(); - let message = serde_json::from_str(&raw_message).unwrap(); - - self - .response_tx_map - .remove(&call_id) - .unwrap() - .send(message) - .unwrap(); - } - - fn send_notification( - &mut self, - message: v8::UniquePtr, - ) { - let raw_message = message.unwrap().string().to_string(); - let message = serde_json::from_str(&raw_message).unwrap(); - - self.notification_queue.push(message); - } - - fn flush_protocol_notifications(&mut self) {} -} - -impl InspectorSession { - const CONTEXT_GROUP_ID: i32 = 1; - - pub fn new(inspector_ptr: *mut DenoInspector) -> Box { - new_box_with(move |self_ptr| { - let v8_channel = v8::inspector::ChannelBase::new::(); - let v8_session = unsafe { &mut *inspector_ptr }.connect( - Self::CONTEXT_GROUP_ID, - unsafe { &mut *self_ptr }, - v8::inspector::StringView::empty(), - ); - - let response_tx_map = HashMap::new(); - let next_message_id = 0; - - let notification_queue = Vec::new(); - - Self { - v8_channel, - v8_session, - response_tx_map, - next_message_id, - notification_queue, - } - }) - } - - pub fn notifications(&mut self) -> Vec { - self.notification_queue.split_off(0) - } - - pub async fn post_message( - &mut self, - method: &str, - params: Option, - ) -> Result { - let id = self.next_message_id; - self.next_message_id += 1; - - let (response_tx, response_rx) = oneshot::channel::(); - self.response_tx_map.insert(id, response_tx); - - let message = json!({ - "id": id, - "method": method, - "params": params, - }); - - let raw_message = serde_json::to_string(&message).unwrap(); - let raw_message = v8::inspector::StringView::from(raw_message.as_bytes()); - self.v8_session.dispatch_protocol_message(raw_message); - - let response = response_rx.await.unwrap(); - if let Some(error) = response.get("error") { - return Err(generic_error(error.to_string())); - } - - let result = response.get("result").unwrap().clone(); - Ok(result) - } -} - -fn new_box_with(new_fn: impl FnOnce(*mut T) -> T) -> Box { - let b = Box::new(MaybeUninit::::uninit()); - let p = Box::into_raw(b) as *mut T; - unsafe { ptr::write(p, new_fn(p)) }; - unsafe { Box::from_raw(p) } -} diff --git a/cli/js.rs b/cli/js.rs index 3d2a17f36..7cfa961a1 100644 --- a/cli/js.rs +++ b/cli/js.rs @@ -4,8 +4,6 @@ use deno_core::Snapshot; pub const TS_VERSION: &str = env!("TS_VERSION"); -pub static CLI_SNAPSHOT: &[u8] = - include_bytes!(concat!(env!("OUT_DIR"), "/CLI_SNAPSHOT.bin")); pub static COMPILER_SNAPSHOT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/COMPILER_SNAPSHOT.bin")); pub static DENO_NS_LIB: &str = include_str!("dts/lib.deno.ns.d.ts"); @@ -16,37 +14,12 @@ pub static SHARED_GLOBALS_LIB: &str = pub static WINDOW_LIB: &str = include_str!("dts/lib.deno.window.d.ts"); pub static UNSTABLE_NS_LIB: &str = include_str!("dts/lib.deno.unstable.d.ts"); -pub fn deno_isolate_init() -> Snapshot { - debug!("Deno isolate init with snapshots."); - let data = CLI_SNAPSHOT; - Snapshot::Static(data) -} - pub fn compiler_isolate_init() -> Snapshot { debug!("Deno compiler isolate init with snapshots."); let data = COMPILER_SNAPSHOT; Snapshot::Static(data) } -#[test] -fn cli_snapshot() { - let mut js_runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions { - startup_snapshot: Some(deno_isolate_init()), - ..Default::default() - }); - js_runtime - .execute( - "", - r#" - if (!(bootstrap.mainRuntime && bootstrap.workerRuntime)) { - throw Error("bad"); - } - console.log("we have console.log!!!"); - "#, - ) - .unwrap(); -} - #[test] fn compiler_snapshot() { let mut js_runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions { diff --git a/cli/main.rs b/cli/main.rs index 38deec5bb..b6b6b295b 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -25,19 +25,14 @@ mod http_cache; mod http_util; mod import_map; mod info; -mod inspector; mod js; mod lockfile; mod lsp; mod media_type; -mod metrics; mod module_graph; mod module_loader; mod ops; -mod permissions; mod program_state; -mod resolve_addr; -mod signal; mod source_maps; mod specifier_handler; mod standalone; @@ -47,8 +42,6 @@ mod tools; mod tsc; mod tsc_config; mod version; -mod web_worker; -mod worker; use crate::file_fetcher::File; use crate::file_fetcher::FileFetcher; @@ -59,18 +52,12 @@ use crate::fmt_errors::PrettyJsError; use crate::import_map::ImportMap; use crate::media_type::MediaType; use crate::module_loader::CliModuleLoader; -use crate::ops::worker_host::CreateWebWorkerCb; -use crate::permissions::Permissions; use crate::program_state::exit_unstable; use crate::program_state::ProgramState; use crate::source_maps::apply_source_map; use crate::specifier_handler::FetchHandler; use crate::standalone::create_standalone_binary; use crate::tools::installer::infer_name_from_url; -use crate::web_worker::WebWorker; -use crate::web_worker::WebWorkerOptions; -use crate::worker::MainWorker; -use crate::worker::WorkerOptions; use deno_core::error::generic_error; use deno_core::error::AnyError; use deno_core::futures::future::FutureExt; @@ -81,6 +68,13 @@ use deno_core::v8_set_flags; use deno_core::ModuleSpecifier; use deno_doc as doc; use deno_doc::parser::DocFileLoader; +use deno_runtime::ops::worker_host::CreateWebWorkerCb; +use deno_runtime::permissions::Permissions; +use deno_runtime::permissions::PermissionsOptions; +use deno_runtime::web_worker::WebWorker; +use deno_runtime::web_worker::WebWorkerOptions; +use deno_runtime::worker::MainWorker; +use deno_runtime::worker::WorkerOptions; use log::Level; use log::LevelFilter; use std::cell::RefCell; @@ -93,6 +87,23 @@ use std::pin::Pin; use std::rc::Rc; use std::sync::Arc; +impl From for PermissionsOptions { + fn from(flags: Flags) -> Self { + Self { + allow_env: flags.allow_env, + allow_hrtime: flags.allow_hrtime, + allow_net: flags.allow_net, + allow_plugin: flags.allow_plugin, + allow_read: flags.allow_read, + allow_run: flags.allow_run, + allow_write: flags.allow_write, + net_allowlist: flags.net_allowlist, + read_allowlist: flags.read_allowlist, + write_allowlist: flags.write_allowlist, + } + } +} + fn create_web_worker_callback( program_state: Arc, ) -> Arc { @@ -132,6 +143,7 @@ fn create_web_worker_callback( runtime_version: version::deno(), ts_version: version::TYPESCRIPT.to_string(), no_color: !colors::use_color(), + get_error_class_fn: Some(&crate::errors::get_error_class_name), }; let mut worker = WebWorker::from_options( @@ -207,6 +219,7 @@ pub fn create_main_worker( runtime_version: version::deno(), ts_version: version::TYPESCRIPT.to_string(), no_color: !colors::use_color(), + get_error_class_fn: Some(&crate::errors::get_error_class_name), }; let mut worker = MainWorker::from_options(main_module, permissions, &options); @@ -392,7 +405,7 @@ async fn install_command( let mut preload_flags = flags.clone(); preload_flags.inspect = None; preload_flags.inspect_brk = None; - let permissions = Permissions::from_flags(&preload_flags); + let permissions = Permissions::from_options(&preload_flags.clone().into()); let program_state = ProgramState::new(preload_flags)?; let main_module = ModuleSpecifier::resolve_url_or_path(&module_url)?; let mut worker = @@ -461,7 +474,7 @@ async fn eval_command( // Force TypeScript compile. let main_module = ModuleSpecifier::resolve_url_or_path("./$deno$eval.ts").unwrap(); - let permissions = Permissions::from_flags(&flags); + let permissions = Permissions::from_options(&flags.clone().into()); let program_state = ProgramState::new(flags)?; let mut worker = create_main_worker(&program_state, main_module.clone(), permissions); @@ -804,7 +817,7 @@ async fn format_command( async fn run_repl(flags: Flags) -> Result<(), AnyError> { let main_module = ModuleSpecifier::resolve_url_or_path("./$deno$repl.ts").unwrap(); - let permissions = Permissions::from_flags(&flags); + let permissions = Permissions::from_options(&flags.clone().into()); let program_state = ProgramState::new(flags)?; let mut worker = create_main_worker(&program_state, main_module.clone(), permissions); @@ -815,7 +828,7 @@ async fn run_repl(flags: Flags) -> Result<(), AnyError> { async fn run_from_stdin(flags: Flags) -> Result<(), AnyError> { let program_state = ProgramState::new(flags.clone())?; - let permissions = Permissions::from_flags(&flags); + let permissions = Permissions::from_options(&flags.clone().into()); let main_module = ModuleSpecifier::resolve_url_or_path("./$deno$stdin.ts").unwrap(); let mut worker = create_main_worker( @@ -896,7 +909,7 @@ async fn run_with_watch(flags: Flags, script: String) -> Result<(), AnyError> { let operation = |main_module: ModuleSpecifier| { let flags = flags.clone(); - let permissions = Permissions::from_flags(&flags); + let permissions = Permissions::from_options(&flags.clone().into()); async move { let main_module = main_module.clone(); let program_state = ProgramState::new(flags)?; @@ -932,7 +945,7 @@ async fn run_command(flags: Flags, script: String) -> Result<(), AnyError> { let main_module = ModuleSpecifier::resolve_url_or_path(&script)?; let program_state = ProgramState::new(flags.clone())?; - let permissions = Permissions::from_flags(&flags); + let permissions = Permissions::from_options(&flags.clone().into()); let mut worker = create_main_worker(&program_state, main_module.clone(), permissions); debug!("main_module {}", main_module); @@ -953,7 +966,7 @@ async fn test_command( filter: Option, ) -> Result<(), AnyError> { let program_state = ProgramState::new(flags.clone())?; - let permissions = Permissions::from_flags(&flags); + let permissions = Permissions::from_options(&flags.clone().into()); let cwd = std::env::current_dir().expect("No current directory"); let include = include.unwrap_or_else(|| vec![".".to_string()]); let test_modules = diff --git a/cli/metrics.rs b/cli/metrics.rs deleted file mode 100644 index c70e0dab9..000000000 --- a/cli/metrics.rs +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -#[derive(Default, Debug)] -pub struct Metrics { - pub ops_dispatched: u64, - pub ops_dispatched_sync: u64, - pub ops_dispatched_async: u64, - pub ops_dispatched_async_unref: u64, - pub ops_completed: u64, - pub ops_completed_sync: u64, - pub ops_completed_async: u64, - pub ops_completed_async_unref: u64, - pub bytes_sent_control: u64, - pub bytes_sent_data: u64, - pub bytes_received: u64, -} - -impl Metrics { - fn op_dispatched( - &mut self, - bytes_sent_control: usize, - bytes_sent_data: usize, - ) { - self.ops_dispatched += 1; - self.bytes_sent_control += bytes_sent_control as u64; - self.bytes_sent_data += bytes_sent_data as u64; - } - - fn op_completed(&mut self, bytes_received: usize) { - self.ops_completed += 1; - self.bytes_received += bytes_received as u64; - } - - pub fn op_sync( - &mut self, - bytes_sent_control: usize, - bytes_sent_data: usize, - bytes_received: usize, - ) { - self.ops_dispatched_sync += 1; - self.op_dispatched(bytes_sent_control, bytes_sent_data); - self.ops_completed_sync += 1; - self.op_completed(bytes_received); - } - - pub fn op_dispatched_async( - &mut self, - bytes_sent_control: usize, - bytes_sent_data: usize, - ) { - self.ops_dispatched_async += 1; - self.op_dispatched(bytes_sent_control, bytes_sent_data) - } - - pub fn op_dispatched_async_unref( - &mut self, - bytes_sent_control: usize, - bytes_sent_data: usize, - ) { - self.ops_dispatched_async_unref += 1; - self.op_dispatched(bytes_sent_control, bytes_sent_data) - } - - pub fn op_completed_async(&mut self, bytes_received: usize) { - self.ops_completed_async += 1; - self.op_completed(bytes_received); - } - - pub fn op_completed_async_unref(&mut self, bytes_received: usize) { - self.ops_completed_async_unref += 1; - self.op_completed(bytes_received); - } -} - -use deno_core::BufVec; -use deno_core::Op; -use deno_core::OpFn; -use deno_core::OpState; -use std::cell::RefCell; -use std::rc::Rc; - -pub fn metrics_op(op_fn: Box) -> Box { - Box::new(move |op_state: Rc>, bufs: BufVec| -> Op { - // TODOs: - // * The 'bytes' metrics seem pretty useless, especially now that the - // distinction between 'control' and 'data' buffers has become blurry. - // * Tracking completion of async ops currently makes us put the boxed - // future into _another_ box. Keeping some counters may not be expensive - // in itself, but adding a heap allocation for every metric seems bad. - let mut buf_len_iter = bufs.iter().map(|buf| buf.len()); - let bytes_sent_control = buf_len_iter.next().unwrap_or(0); - let bytes_sent_data = buf_len_iter.sum(); - - let op = (op_fn)(op_state.clone(), bufs); - - let op_state_ = op_state.clone(); - let mut s = op_state.borrow_mut(); - let metrics = s.borrow_mut::(); - - use deno_core::futures::future::FutureExt; - - match op { - Op::Sync(buf) => { - metrics.op_sync(bytes_sent_control, bytes_sent_data, buf.len()); - Op::Sync(buf) - } - Op::Async(fut) => { - metrics.op_dispatched_async(bytes_sent_control, bytes_sent_data); - let fut = fut - .inspect(move |buf| { - let mut s = op_state_.borrow_mut(); - let metrics = s.borrow_mut::(); - metrics.op_completed_async(buf.len()); - }) - .boxed_local(); - Op::Async(fut) - } - Op::AsyncUnref(fut) => { - metrics.op_dispatched_async_unref(bytes_sent_control, bytes_sent_data); - let fut = fut - .inspect(move |buf| { - let mut s = op_state_.borrow_mut(); - let metrics = s.borrow_mut::(); - metrics.op_completed_async_unref(buf.len()); - }) - .boxed_local(); - Op::AsyncUnref(fut) - } - other => other, - } - }) -} diff --git a/cli/module_loader.rs b/cli/module_loader.rs index 9dda2c24a..da75b8510 100644 --- a/cli/module_loader.rs +++ b/cli/module_loader.rs @@ -2,7 +2,6 @@ use crate::import_map::ImportMap; use crate::module_graph::TypeLib; -use crate::permissions::Permissions; use crate::program_state::ProgramState; use deno_core::error::AnyError; use deno_core::futures::future::FutureExt; @@ -11,6 +10,7 @@ use deno_core::ModuleLoadId; use deno_core::ModuleLoader; use deno_core::ModuleSpecifier; use deno_core::OpState; +use deno_runtime::permissions::Permissions; use std::cell::RefCell; use std::pin::Pin; use std::rc::Rc; diff --git a/cli/ops/crypto.rs b/cli/ops/crypto.rs deleted file mode 100644 index a73843a33..000000000 --- a/cli/ops/crypto.rs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -use deno_crypto::op_get_random_values; -use deno_crypto::rand::rngs::StdRng; -use deno_crypto::rand::SeedableRng; - -pub fn init(rt: &mut deno_core::JsRuntime, maybe_seed: Option) { - if let Some(seed) = maybe_seed { - let rng = StdRng::seed_from_u64(seed); - let op_state = rt.op_state(); - let mut state = op_state.borrow_mut(); - state.put::(rng); - } - super::reg_json_sync(rt, "op_get_random_values", op_get_random_values); -} diff --git a/cli/ops/dispatch_minimal.rs b/cli/ops/dispatch_minimal.rs deleted file mode 100644 index ae8fa819d..000000000 --- a/cli/ops/dispatch_minimal.rs +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use deno_core::error::AnyError; -use deno_core::futures::future::FutureExt; -use deno_core::BufVec; -use deno_core::Op; -use deno_core::OpFn; -use deno_core::OpState; -use std::cell::RefCell; -use std::future::Future; -use std::iter::repeat; -use std::mem::size_of_val; -use std::pin::Pin; -use std::rc::Rc; -use std::slice; - -pub enum MinimalOp { - Sync(Result), - Async(Pin>>>), -} - -#[derive(Copy, Clone, Debug, PartialEq)] -// This corresponds to RecordMinimal on the TS side. -pub struct Record { - pub promise_id: i32, - pub arg: i32, - pub result: i32, -} - -impl Into> for Record { - fn into(self) -> Box<[u8]> { - let vec = vec![self.promise_id, self.arg, self.result]; - let buf32 = vec.into_boxed_slice(); - let ptr = Box::into_raw(buf32) as *mut [u8; 3 * 4]; - unsafe { Box::from_raw(ptr) } - } -} - -pub struct ErrorRecord { - pub promise_id: i32, - pub arg: i32, - pub error_len: i32, - pub error_class: &'static [u8], - pub error_message: Vec, -} - -impl Into> for ErrorRecord { - fn into(self) -> Box<[u8]> { - let Self { - promise_id, - arg, - error_len, - error_class, - error_message, - .. - } = self; - let header_i32 = [promise_id, arg, error_len]; - let header_u8 = unsafe { - slice::from_raw_parts( - &header_i32 as *const _ as *const u8, - size_of_val(&header_i32), - ) - }; - let padded_len = - (header_u8.len() + error_class.len() + error_message.len() + 3usize) - & !3usize; - header_u8 - .iter() - .cloned() - .chain(error_class.iter().cloned()) - .chain(error_message.into_iter()) - .chain(repeat(b' ')) - .take(padded_len) - .collect() - } -} - -#[test] -fn test_error_record() { - let expected = vec![ - 1, 0, 0, 0, 255, 255, 255, 255, 11, 0, 0, 0, 66, 97, 100, 82, 101, 115, - 111, 117, 114, 99, 101, 69, 114, 114, 111, 114, - ]; - let err_record = ErrorRecord { - promise_id: 1, - arg: -1, - error_len: 11, - error_class: b"BadResource", - error_message: b"Error".to_vec(), - }; - let buf: Box<[u8]> = err_record.into(); - assert_eq!(buf, expected.into_boxed_slice()); -} - -pub fn parse_min_record(bytes: &[u8]) -> Option { - if bytes.len() % std::mem::size_of::() != 0 { - return None; - } - let p = bytes.as_ptr(); - #[allow(clippy::cast_ptr_alignment)] - let p32 = p as *const i32; - let s = unsafe { std::slice::from_raw_parts(p32, bytes.len() / 4) }; - - if s.len() != 3 { - return None; - } - let ptr = s.as_ptr(); - let ints = unsafe { std::slice::from_raw_parts(ptr, 3) }; - Some(Record { - promise_id: ints[0], - arg: ints[1], - result: ints[2], - }) -} - -#[test] -fn test_parse_min_record() { - let buf = vec![1, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0]; - assert_eq!( - parse_min_record(&buf), - Some(Record { - promise_id: 1, - arg: 3, - result: 4 - }) - ); - - let buf = vec![]; - assert_eq!(parse_min_record(&buf), None); - - let buf = vec![5]; - assert_eq!(parse_min_record(&buf), None); -} - -pub fn minimal_op(op_fn: F) -> Box -where - F: Fn(Rc>, bool, i32, BufVec) -> MinimalOp + 'static, -{ - Box::new(move |state: Rc>, bufs: BufVec| { - let mut bufs_iter = bufs.into_iter(); - let record_buf = bufs_iter.next().expect("Expected record at position 0"); - let zero_copy = bufs_iter.collect::(); - - let mut record = match parse_min_record(&record_buf) { - Some(r) => r, - None => { - let error_class = b"TypeError"; - let error_message = b"Unparsable control buffer"; - let error_record = ErrorRecord { - promise_id: 0, - arg: -1, - error_len: error_class.len() as i32, - error_class, - error_message: error_message[..].to_owned(), - }; - return Op::Sync(error_record.into()); - } - }; - let is_sync = record.promise_id == 0; - let rid = record.arg; - let min_op = op_fn(state.clone(), is_sync, rid, zero_copy); - - match min_op { - MinimalOp::Sync(sync_result) => Op::Sync(match sync_result { - Ok(r) => { - record.result = r; - record.into() - } - Err(err) => { - let error_class = (state.borrow().get_error_class_fn)(&err); - let error_record = ErrorRecord { - promise_id: record.promise_id, - arg: -1, - error_len: error_class.len() as i32, - error_class: error_class.as_bytes(), - error_message: err.to_string().as_bytes().to_owned(), - }; - error_record.into() - } - }), - MinimalOp::Async(min_fut) => { - let fut = async move { - match min_fut.await { - Ok(r) => { - record.result = r; - record.into() - } - Err(err) => { - let error_class = (state.borrow().get_error_class_fn)(&err); - let error_record = ErrorRecord { - promise_id: record.promise_id, - arg: -1, - error_len: error_class.len() as i32, - error_class: error_class.as_bytes(), - error_message: err.to_string().as_bytes().to_owned(), - }; - error_record.into() - } - } - }; - Op::Async(fut.boxed_local()) - } - } - }) -} diff --git a/cli/ops/fetch.rs b/cli/ops/fetch.rs deleted file mode 100644 index 18e9e9c9f..000000000 --- a/cli/ops/fetch.rs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -use crate::http_util; -use crate::permissions::Permissions; -use deno_fetch::reqwest; - -pub fn init(rt: &mut deno_core::JsRuntime, maybe_ca_file: Option<&str>) { - { - let op_state = rt.op_state(); - let mut state = op_state.borrow_mut(); - state.put::({ - http_util::create_http_client(http_util::get_user_agent(), maybe_ca_file) - .unwrap() - }); - } - super::reg_json_async(rt, "op_fetch", deno_fetch::op_fetch::); - super::reg_json_async(rt, "op_fetch_read", deno_fetch::op_fetch_read); - super::reg_json_sync( - rt, - "op_create_http_client", - deno_fetch::op_create_http_client::, - ); -} diff --git a/cli/ops/fs.rs b/cli/ops/fs.rs deleted file mode 100644 index 865c5bcca..000000000 --- a/cli/ops/fs.rs +++ /dev/null @@ -1,1702 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -// Some deserializer fields are only used on Unix and Windows build fails without it -use super::io::std_file_resource; -use super::io::{FileMetadata, StreamResource, StreamResourceHolder}; -use crate::fs_util::canonicalize_path; -use crate::permissions::Permissions; -use deno_core::error::custom_error; -use deno_core::error::type_error; -use deno_core::error::AnyError; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use deno_core::BufVec; -use deno_core::OpState; -use deno_core::ZeroCopyBuf; -use deno_crypto::rand::thread_rng; -use deno_crypto::rand::Rng; -use serde::Deserialize; -use std::cell::RefCell; -use std::convert::From; -use std::env::{current_dir, set_current_dir, temp_dir}; -use std::io; -use std::io::{Seek, SeekFrom}; -use std::path::{Path, PathBuf}; -use std::rc::Rc; -use std::time::SystemTime; -use std::time::UNIX_EPOCH; - -#[cfg(not(unix))] -use deno_core::error::generic_error; -#[cfg(not(unix))] -use deno_core::error::not_supported; - -pub fn init(rt: &mut deno_core::JsRuntime) { - super::reg_json_sync(rt, "op_open_sync", op_open_sync); - super::reg_json_async(rt, "op_open_async", op_open_async); - - super::reg_json_sync(rt, "op_seek_sync", op_seek_sync); - super::reg_json_async(rt, "op_seek_async", op_seek_async); - - super::reg_json_sync(rt, "op_fdatasync_sync", op_fdatasync_sync); - super::reg_json_async(rt, "op_fdatasync_async", op_fdatasync_async); - - super::reg_json_sync(rt, "op_fsync_sync", op_fsync_sync); - super::reg_json_async(rt, "op_fsync_async", op_fsync_async); - - super::reg_json_sync(rt, "op_fstat_sync", op_fstat_sync); - super::reg_json_async(rt, "op_fstat_async", op_fstat_async); - - super::reg_json_sync(rt, "op_umask", op_umask); - super::reg_json_sync(rt, "op_chdir", op_chdir); - - super::reg_json_sync(rt, "op_mkdir_sync", op_mkdir_sync); - super::reg_json_async(rt, "op_mkdir_async", op_mkdir_async); - - super::reg_json_sync(rt, "op_chmod_sync", op_chmod_sync); - super::reg_json_async(rt, "op_chmod_async", op_chmod_async); - - super::reg_json_sync(rt, "op_chown_sync", op_chown_sync); - super::reg_json_async(rt, "op_chown_async", op_chown_async); - - super::reg_json_sync(rt, "op_remove_sync", op_remove_sync); - super::reg_json_async(rt, "op_remove_async", op_remove_async); - - super::reg_json_sync(rt, "op_copy_file_sync", op_copy_file_sync); - super::reg_json_async(rt, "op_copy_file_async", op_copy_file_async); - - super::reg_json_sync(rt, "op_stat_sync", op_stat_sync); - super::reg_json_async(rt, "op_stat_async", op_stat_async); - - super::reg_json_sync(rt, "op_realpath_sync", op_realpath_sync); - super::reg_json_async(rt, "op_realpath_async", op_realpath_async); - - super::reg_json_sync(rt, "op_read_dir_sync", op_read_dir_sync); - super::reg_json_async(rt, "op_read_dir_async", op_read_dir_async); - - super::reg_json_sync(rt, "op_rename_sync", op_rename_sync); - super::reg_json_async(rt, "op_rename_async", op_rename_async); - - super::reg_json_sync(rt, "op_link_sync", op_link_sync); - super::reg_json_async(rt, "op_link_async", op_link_async); - - super::reg_json_sync(rt, "op_symlink_sync", op_symlink_sync); - super::reg_json_async(rt, "op_symlink_async", op_symlink_async); - - super::reg_json_sync(rt, "op_read_link_sync", op_read_link_sync); - super::reg_json_async(rt, "op_read_link_async", op_read_link_async); - - super::reg_json_sync(rt, "op_ftruncate_sync", op_ftruncate_sync); - super::reg_json_async(rt, "op_ftruncate_async", op_ftruncate_async); - - super::reg_json_sync(rt, "op_truncate_sync", op_truncate_sync); - super::reg_json_async(rt, "op_truncate_async", op_truncate_async); - - super::reg_json_sync(rt, "op_make_temp_dir_sync", op_make_temp_dir_sync); - super::reg_json_async(rt, "op_make_temp_dir_async", op_make_temp_dir_async); - - super::reg_json_sync(rt, "op_make_temp_file_sync", op_make_temp_file_sync); - super::reg_json_async(rt, "op_make_temp_file_async", op_make_temp_file_async); - - super::reg_json_sync(rt, "op_cwd", op_cwd); - - super::reg_json_sync(rt, "op_futime_sync", op_futime_sync); - super::reg_json_async(rt, "op_futime_async", op_futime_async); - - super::reg_json_sync(rt, "op_utime_sync", op_utime_sync); - super::reg_json_async(rt, "op_utime_async", op_utime_async); -} - -fn into_string(s: std::ffi::OsString) -> Result { - s.into_string().map_err(|s| { - let message = format!("File name or path {:?} is not valid UTF-8", s); - custom_error("InvalidData", message) - }) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct OpenArgs { - path: String, - mode: Option, - options: OpenOptions, -} - -#[derive(Deserialize, Default, Debug)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -struct OpenOptions { - read: bool, - write: bool, - create: bool, - truncate: bool, - append: bool, - create_new: bool, -} - -fn open_helper( - state: &mut OpState, - args: Value, -) -> Result<(PathBuf, std::fs::OpenOptions), AnyError> { - let args: OpenArgs = serde_json::from_value(args)?; - let path = Path::new(&args.path).to_path_buf(); - - let mut open_options = std::fs::OpenOptions::new(); - - if let Some(mode) = args.mode { - // mode only used if creating the file on Unix - // if not specified, defaults to 0o666 - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - open_options.mode(mode & 0o777); - } - #[cfg(not(unix))] - let _ = mode; // avoid unused warning - } - - let permissions = state.borrow::(); - let options = args.options; - - if options.read { - permissions.check_read(&path)?; - } - - if options.write || options.append { - permissions.check_write(&path)?; - } - - open_options - .read(options.read) - .create(options.create) - .write(options.write) - .truncate(options.truncate) - .append(options.append) - .create_new(options.create_new); - - Ok((path, open_options)) -} - -fn op_open_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let (path, open_options) = open_helper(state, args)?; - let std_file = open_options.open(path)?; - let tokio_file = tokio::fs::File::from_std(std_file); - let rid = state.resource_table.add( - "fsFile", - Box::new(StreamResourceHolder::new(StreamResource::FsFile(Some(( - tokio_file, - FileMetadata::default(), - ))))), - ); - Ok(json!(rid)) -} - -async fn op_open_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let (path, open_options) = open_helper(&mut state.borrow_mut(), args)?; - let tokio_file = tokio::fs::OpenOptions::from(open_options) - .open(path) - .await?; - let rid = state.borrow_mut().resource_table.add( - "fsFile", - Box::new(StreamResourceHolder::new(StreamResource::FsFile(Some(( - tokio_file, - FileMetadata::default(), - ))))), - ); - Ok(json!(rid)) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct SeekArgs { - rid: i32, - offset: i64, - whence: i32, -} - -fn seek_helper(args: Value) -> Result<(u32, SeekFrom), AnyError> { - let args: SeekArgs = serde_json::from_value(args)?; - let rid = args.rid as u32; - let offset = args.offset; - let whence = args.whence as u32; - // Translate seek mode to Rust repr. - let seek_from = match whence { - 0 => SeekFrom::Start(offset as u64), - 1 => SeekFrom::Current(offset), - 2 => SeekFrom::End(offset), - _ => { - return Err(type_error(format!("Invalid seek mode: {}", whence))); - } - }; - - Ok((rid, seek_from)) -} - -fn op_seek_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let (rid, seek_from) = seek_helper(args)?; - let pos = std_file_resource(state, rid, |r| match r { - Ok(std_file) => std_file.seek(seek_from).map_err(AnyError::from), - Err(_) => Err(type_error( - "cannot seek on this type of resource".to_string(), - )), - })?; - Ok(json!(pos)) -} - -async fn op_seek_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let (rid, seek_from) = seek_helper(args)?; - // TODO(ry) This is a fake async op. We need to use poll_fn, - // tokio::fs::File::start_seek and tokio::fs::File::poll_complete - let pos = std_file_resource(&mut state.borrow_mut(), rid, |r| match r { - Ok(std_file) => std_file.seek(seek_from).map_err(AnyError::from), - Err(_) => Err(type_error( - "cannot seek on this type of resource".to_string(), - )), - })?; - Ok(json!(pos)) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct FdatasyncArgs { - rid: i32, -} - -fn op_fdatasync_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: FdatasyncArgs = serde_json::from_value(args)?; - let rid = args.rid as u32; - std_file_resource(state, rid, |r| match r { - Ok(std_file) => std_file.sync_data().map_err(AnyError::from), - Err(_) => Err(type_error("cannot sync this type of resource".to_string())), - })?; - Ok(json!({})) -} - -async fn op_fdatasync_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: FdatasyncArgs = serde_json::from_value(args)?; - let rid = args.rid as u32; - std_file_resource(&mut state.borrow_mut(), rid, |r| match r { - Ok(std_file) => std_file.sync_data().map_err(AnyError::from), - Err(_) => Err(type_error("cannot sync this type of resource".to_string())), - })?; - Ok(json!({})) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct FsyncArgs { - rid: i32, -} - -fn op_fsync_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: FsyncArgs = serde_json::from_value(args)?; - let rid = args.rid as u32; - std_file_resource(state, rid, |r| match r { - Ok(std_file) => std_file.sync_all().map_err(AnyError::from), - Err(_) => Err(type_error("cannot sync this type of resource".to_string())), - })?; - Ok(json!({})) -} - -async fn op_fsync_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: FsyncArgs = serde_json::from_value(args)?; - let rid = args.rid as u32; - std_file_resource(&mut state.borrow_mut(), rid, |r| match r { - Ok(std_file) => std_file.sync_all().map_err(AnyError::from), - Err(_) => Err(type_error("cannot sync this type of resource".to_string())), - })?; - Ok(json!({})) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct FstatArgs { - rid: i32, -} - -fn op_fstat_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.fstat"); - let args: FstatArgs = serde_json::from_value(args)?; - let rid = args.rid as u32; - let metadata = std_file_resource(state, rid, |r| match r { - Ok(std_file) => std_file.metadata().map_err(AnyError::from), - Err(_) => Err(type_error("cannot stat this type of resource".to_string())), - })?; - Ok(get_stat_json(metadata)) -} - -async fn op_fstat_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - super::check_unstable2(&state, "Deno.fstat"); - - let args: FstatArgs = serde_json::from_value(args)?; - let rid = args.rid as u32; - let metadata = - std_file_resource(&mut state.borrow_mut(), rid, |r| match r { - Ok(std_file) => std_file.metadata().map_err(AnyError::from), - Err(_) => { - Err(type_error("cannot stat this type of resource".to_string())) - } - })?; - Ok(get_stat_json(metadata)) -} - -#[derive(Deserialize)] -struct UmaskArgs { - mask: Option, -} - -fn op_umask( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.umask"); - let args: UmaskArgs = serde_json::from_value(args)?; - // TODO implement umask for Windows - // see https://github.com/nodejs/node/blob/master/src/node_process_methods.cc - // and https://docs.microsoft.com/fr-fr/cpp/c-runtime-library/reference/umask?view=vs-2019 - #[cfg(not(unix))] - { - let _ = args.mask; // avoid unused warning. - Err(not_supported()) - } - #[cfg(unix)] - { - use nix::sys::stat::mode_t; - use nix::sys::stat::umask; - use nix::sys::stat::Mode; - let r = if let Some(mask) = args.mask { - // If mask provided, return previous. - umask(Mode::from_bits_truncate(mask as mode_t)) - } else { - // If no mask provided, we query the current. Requires two syscalls. - let prev = umask(Mode::from_bits_truncate(0o777)); - let _ = umask(prev); - prev - }; - Ok(json!(r.bits() as u32)) - } -} - -#[derive(Deserialize)] -struct ChdirArgs { - directory: String, -} - -fn op_chdir( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: ChdirArgs = serde_json::from_value(args)?; - let d = PathBuf::from(&args.directory); - state.borrow::().check_read(&d)?; - set_current_dir(&d)?; - Ok(json!({})) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct MkdirArgs { - path: String, - recursive: bool, - mode: Option, -} - -fn op_mkdir_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: MkdirArgs = serde_json::from_value(args)?; - let path = Path::new(&args.path).to_path_buf(); - let mode = args.mode.unwrap_or(0o777) & 0o777; - state.borrow::().check_write(&path)?; - debug!("op_mkdir {} {:o} {}", path.display(), mode, args.recursive); - let mut builder = std::fs::DirBuilder::new(); - builder.recursive(args.recursive); - #[cfg(unix)] - { - use std::os::unix::fs::DirBuilderExt; - builder.mode(mode); - } - builder.create(path)?; - Ok(json!({})) -} - -async fn op_mkdir_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: MkdirArgs = serde_json::from_value(args)?; - let path = Path::new(&args.path).to_path_buf(); - let mode = args.mode.unwrap_or(0o777) & 0o777; - - { - let state = state.borrow(); - state.borrow::().check_write(&path)?; - } - - tokio::task::spawn_blocking(move || { - debug!("op_mkdir {} {:o} {}", path.display(), mode, args.recursive); - let mut builder = std::fs::DirBuilder::new(); - builder.recursive(args.recursive); - #[cfg(unix)] - { - use std::os::unix::fs::DirBuilderExt; - builder.mode(mode); - } - builder.create(path)?; - Ok(json!({})) - }) - .await - .unwrap() -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct ChmodArgs { - path: String, - mode: u32, -} - -fn op_chmod_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: ChmodArgs = serde_json::from_value(args)?; - let path = Path::new(&args.path).to_path_buf(); - let mode = args.mode & 0o777; - - state.borrow::().check_write(&path)?; - debug!("op_chmod_sync {} {:o}", path.display(), mode); - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let permissions = PermissionsExt::from_mode(mode); - std::fs::set_permissions(&path, permissions)?; - Ok(json!({})) - } - // TODO Implement chmod for Windows (#4357) - #[cfg(not(unix))] - { - // Still check file/dir exists on Windows - let _metadata = std::fs::metadata(&path)?; - Err(generic_error("Not implemented")) - } -} - -async fn op_chmod_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: ChmodArgs = serde_json::from_value(args)?; - let path = Path::new(&args.path).to_path_buf(); - let mode = args.mode & 0o777; - - { - let state = state.borrow(); - state.borrow::().check_write(&path)?; - } - - tokio::task::spawn_blocking(move || { - debug!("op_chmod_async {} {:o}", path.display(), mode); - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let permissions = PermissionsExt::from_mode(mode); - std::fs::set_permissions(&path, permissions)?; - Ok(json!({})) - } - // TODO Implement chmod for Windows (#4357) - #[cfg(not(unix))] - { - // Still check file/dir exists on Windows - let _metadata = std::fs::metadata(&path)?; - Err(not_supported()) - } - }) - .await - .unwrap() -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct ChownArgs { - path: String, - uid: Option, - gid: Option, -} - -fn op_chown_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: ChownArgs = serde_json::from_value(args)?; - let path = Path::new(&args.path).to_path_buf(); - state.borrow::().check_write(&path)?; - debug!( - "op_chown_sync {} {:?} {:?}", - path.display(), - args.uid, - args.gid, - ); - #[cfg(unix)] - { - use nix::unistd::{chown, Gid, Uid}; - let nix_uid = args.uid.map(Uid::from_raw); - let nix_gid = args.gid.map(Gid::from_raw); - chown(&path, nix_uid, nix_gid)?; - Ok(json!({})) - } - // TODO Implement chown for Windows - #[cfg(not(unix))] - { - Err(generic_error("Not implemented")) - } -} - -async fn op_chown_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: ChownArgs = serde_json::from_value(args)?; - let path = Path::new(&args.path).to_path_buf(); - - { - let state = state.borrow(); - state.borrow::().check_write(&path)?; - } - - tokio::task::spawn_blocking(move || { - debug!( - "op_chown_async {} {:?} {:?}", - path.display(), - args.uid, - args.gid, - ); - #[cfg(unix)] - { - use nix::unistd::{chown, Gid, Uid}; - let nix_uid = args.uid.map(Uid::from_raw); - let nix_gid = args.gid.map(Gid::from_raw); - chown(&path, nix_uid, nix_gid)?; - Ok(json!({})) - } - // TODO Implement chown for Windows - #[cfg(not(unix))] - Err(not_supported()) - }) - .await - .unwrap() -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct RemoveArgs { - path: String, - recursive: bool, -} - -fn op_remove_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: RemoveArgs = serde_json::from_value(args)?; - let path = PathBuf::from(&args.path); - let recursive = args.recursive; - - state.borrow::().check_write(&path)?; - - #[cfg(not(unix))] - use std::os::windows::prelude::MetadataExt; - - let metadata = std::fs::symlink_metadata(&path)?; - - debug!("op_remove_sync {} {}", path.display(), recursive); - let file_type = metadata.file_type(); - if file_type.is_file() { - std::fs::remove_file(&path)?; - } else if recursive { - std::fs::remove_dir_all(&path)?; - } else if file_type.is_symlink() { - #[cfg(unix)] - std::fs::remove_file(&path)?; - #[cfg(not(unix))] - { - use winapi::um::winnt::FILE_ATTRIBUTE_DIRECTORY; - if metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 { - std::fs::remove_dir(&path)?; - } else { - std::fs::remove_file(&path)?; - } - } - } else if file_type.is_dir() { - std::fs::remove_dir(&path)?; - } else { - // pipes, sockets, etc... - std::fs::remove_file(&path)?; - } - Ok(json!({})) -} - -async fn op_remove_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: RemoveArgs = serde_json::from_value(args)?; - let path = PathBuf::from(&args.path); - let recursive = args.recursive; - - { - let state = state.borrow(); - state.borrow::().check_write(&path)?; - } - - tokio::task::spawn_blocking(move || { - #[cfg(not(unix))] - use std::os::windows::prelude::MetadataExt; - - let metadata = std::fs::symlink_metadata(&path)?; - - debug!("op_remove_async {} {}", path.display(), recursive); - let file_type = metadata.file_type(); - if file_type.is_file() { - std::fs::remove_file(&path)?; - } else if recursive { - std::fs::remove_dir_all(&path)?; - } else if file_type.is_symlink() { - #[cfg(unix)] - std::fs::remove_file(&path)?; - #[cfg(not(unix))] - { - use winapi::um::winnt::FILE_ATTRIBUTE_DIRECTORY; - if metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 { - std::fs::remove_dir(&path)?; - } else { - std::fs::remove_file(&path)?; - } - } - } else if file_type.is_dir() { - std::fs::remove_dir(&path)?; - } else { - // pipes, sockets, etc... - std::fs::remove_file(&path)?; - } - Ok(json!({})) - }) - .await - .unwrap() -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct CopyFileArgs { - from: String, - to: String, -} - -fn op_copy_file_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: CopyFileArgs = serde_json::from_value(args)?; - let from = PathBuf::from(&args.from); - let to = PathBuf::from(&args.to); - - let permissions = state.borrow::(); - permissions.check_read(&from)?; - permissions.check_write(&to)?; - - debug!("op_copy_file_sync {} {}", from.display(), to.display()); - // On *nix, Rust reports non-existent `from` as ErrorKind::InvalidInput - // See https://github.com/rust-lang/rust/issues/54800 - // Once the issue is resolved, we should remove this workaround. - if cfg!(unix) && !from.is_file() { - return Err(custom_error("NotFound", "File not found")); - } - - // returns size of from as u64 (we ignore) - std::fs::copy(&from, &to)?; - Ok(json!({})) -} - -async fn op_copy_file_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: CopyFileArgs = serde_json::from_value(args)?; - let from = PathBuf::from(&args.from); - let to = PathBuf::from(&args.to); - - { - let state = state.borrow(); - let permissions = state.borrow::(); - permissions.check_read(&from)?; - permissions.check_write(&to)?; - } - - debug!("op_copy_file_async {} {}", from.display(), to.display()); - tokio::task::spawn_blocking(move || { - // On *nix, Rust reports non-existent `from` as ErrorKind::InvalidInput - // See https://github.com/rust-lang/rust/issues/54800 - // Once the issue is resolved, we should remove this workaround. - if cfg!(unix) && !from.is_file() { - return Err(custom_error("NotFound", "File not found")); - } - - // returns size of from as u64 (we ignore) - std::fs::copy(&from, &to)?; - Ok(json!({})) - }) - .await - .unwrap() -} - -fn to_msec(maybe_time: Result) -> Value { - match maybe_time { - Ok(time) => { - let msec = time - .duration_since(UNIX_EPOCH) - .map(|t| t.as_secs_f64() * 1000f64) - .unwrap_or_else(|err| err.duration().as_secs_f64() * -1000f64); - serde_json::Number::from_f64(msec) - .map(Value::Number) - .unwrap_or(Value::Null) - } - Err(_) => Value::Null, - } -} - -#[inline(always)] -fn get_stat_json(metadata: std::fs::Metadata) -> Value { - // Unix stat member (number types only). 0 if not on unix. - macro_rules! usm { - ($member:ident) => {{ - #[cfg(unix)] - { - metadata.$member() - } - #[cfg(not(unix))] - { - 0 - } - }}; - } - - #[cfg(unix)] - use std::os::unix::fs::MetadataExt; - let json_val = json!({ - "isFile": metadata.is_file(), - "isDirectory": metadata.is_dir(), - "isSymlink": metadata.file_type().is_symlink(), - "size": metadata.len(), - // In milliseconds, like JavaScript. Available on both Unix or Windows. - "mtime": to_msec(metadata.modified()), - "atime": to_msec(metadata.accessed()), - "birthtime": to_msec(metadata.created()), - // Following are only valid under Unix. - "dev": usm!(dev), - "ino": usm!(ino), - "mode": usm!(mode), - "nlink": usm!(nlink), - "uid": usm!(uid), - "gid": usm!(gid), - "rdev": usm!(rdev), - // TODO(kevinkassimo): *time_nsec requires BigInt. - // Probably should be treated as String if we need to add them. - "blksize": usm!(blksize), - "blocks": usm!(blocks), - }); - json_val -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct StatArgs { - path: String, - lstat: bool, -} - -fn op_stat_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: StatArgs = serde_json::from_value(args)?; - let path = PathBuf::from(&args.path); - let lstat = args.lstat; - state.borrow::().check_read(&path)?; - debug!("op_stat_sync {} {}", path.display(), lstat); - let metadata = if lstat { - std::fs::symlink_metadata(&path)? - } else { - std::fs::metadata(&path)? - }; - Ok(get_stat_json(metadata)) -} - -async fn op_stat_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: StatArgs = serde_json::from_value(args)?; - let path = PathBuf::from(&args.path); - let lstat = args.lstat; - - { - let state = state.borrow(); - state.borrow::().check_read(&path)?; - } - - tokio::task::spawn_blocking(move || { - debug!("op_stat_async {} {}", path.display(), lstat); - let metadata = if lstat { - std::fs::symlink_metadata(&path)? - } else { - std::fs::metadata(&path)? - }; - Ok(get_stat_json(metadata)) - }) - .await - .unwrap() -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct RealpathArgs { - path: String, -} - -fn op_realpath_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: RealpathArgs = serde_json::from_value(args)?; - let path = PathBuf::from(&args.path); - - let permissions = state.borrow::(); - permissions.check_read(&path)?; - if path.is_relative() { - permissions.check_read_blind(¤t_dir()?, "CWD")?; - } - - debug!("op_realpath_sync {}", path.display()); - // corresponds to the realpath on Unix and - // CreateFile and GetFinalPathNameByHandle on Windows - let realpath = canonicalize_path(&path)?; - let realpath_str = into_string(realpath.into_os_string())?; - Ok(json!(realpath_str)) -} - -async fn op_realpath_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: RealpathArgs = serde_json::from_value(args)?; - let path = PathBuf::from(&args.path); - - { - let state = state.borrow(); - let permissions = state.borrow::(); - permissions.check_read(&path)?; - if path.is_relative() { - permissions.check_read_blind(¤t_dir()?, "CWD")?; - } - } - - tokio::task::spawn_blocking(move || { - debug!("op_realpath_async {}", path.display()); - // corresponds to the realpath on Unix and - // CreateFile and GetFinalPathNameByHandle on Windows - let realpath = canonicalize_path(&path)?; - let realpath_str = into_string(realpath.into_os_string())?; - Ok(json!(realpath_str)) - }) - .await - .unwrap() -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct ReadDirArgs { - path: String, -} - -fn op_read_dir_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: ReadDirArgs = serde_json::from_value(args)?; - let path = PathBuf::from(&args.path); - - state.borrow::().check_read(&path)?; - - debug!("op_read_dir_sync {}", path.display()); - let entries: Vec<_> = std::fs::read_dir(path)? - .filter_map(|entry| { - let entry = entry.unwrap(); - let file_type = entry.file_type().unwrap(); - // Not all filenames can be encoded as UTF-8. Skip those for now. - if let Ok(name) = into_string(entry.file_name()) { - Some(json!({ - "name": name, - "isFile": file_type.is_file(), - "isDirectory": file_type.is_dir(), - "isSymlink": file_type.is_symlink() - })) - } else { - None - } - }) - .collect(); - - Ok(json!({ "entries": entries })) -} - -async fn op_read_dir_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: ReadDirArgs = serde_json::from_value(args)?; - let path = PathBuf::from(&args.path); - { - let state = state.borrow(); - state.borrow::().check_read(&path)?; - } - tokio::task::spawn_blocking(move || { - debug!("op_read_dir_async {}", path.display()); - let entries: Vec<_> = std::fs::read_dir(path)? - .filter_map(|entry| { - let entry = entry.unwrap(); - let file_type = entry.file_type().unwrap(); - // Not all filenames can be encoded as UTF-8. Skip those for now. - if let Ok(name) = into_string(entry.file_name()) { - Some(json!({ - "name": name, - "isFile": file_type.is_file(), - "isDirectory": file_type.is_dir(), - "isSymlink": file_type.is_symlink() - })) - } else { - None - } - }) - .collect(); - - Ok(json!({ "entries": entries })) - }) - .await - .unwrap() -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct RenameArgs { - oldpath: String, - newpath: String, -} - -fn op_rename_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: RenameArgs = serde_json::from_value(args)?; - let oldpath = PathBuf::from(&args.oldpath); - let newpath = PathBuf::from(&args.newpath); - - let permissions = state.borrow::(); - permissions.check_read(&oldpath)?; - permissions.check_write(&oldpath)?; - permissions.check_write(&newpath)?; - debug!("op_rename_sync {} {}", oldpath.display(), newpath.display()); - std::fs::rename(&oldpath, &newpath)?; - Ok(json!({})) -} - -async fn op_rename_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: RenameArgs = serde_json::from_value(args)?; - let oldpath = PathBuf::from(&args.oldpath); - let newpath = PathBuf::from(&args.newpath); - { - let state = state.borrow(); - let permissions = state.borrow::(); - permissions.check_read(&oldpath)?; - permissions.check_write(&oldpath)?; - permissions.check_write(&newpath)?; - } - tokio::task::spawn_blocking(move || { - debug!( - "op_rename_async {} {}", - oldpath.display(), - newpath.display() - ); - std::fs::rename(&oldpath, &newpath)?; - Ok(json!({})) - }) - .await - .unwrap() -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct LinkArgs { - oldpath: String, - newpath: String, -} - -fn op_link_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.link"); - let args: LinkArgs = serde_json::from_value(args)?; - let oldpath = PathBuf::from(&args.oldpath); - let newpath = PathBuf::from(&args.newpath); - - let permissions = state.borrow::(); - permissions.check_read(&oldpath)?; - permissions.check_write(&newpath)?; - - debug!("op_link_sync {} {}", oldpath.display(), newpath.display()); - std::fs::hard_link(&oldpath, &newpath)?; - Ok(json!({})) -} - -async fn op_link_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - super::check_unstable2(&state, "Deno.link"); - - let args: LinkArgs = serde_json::from_value(args)?; - let oldpath = PathBuf::from(&args.oldpath); - let newpath = PathBuf::from(&args.newpath); - - { - let state = state.borrow(); - let permissions = state.borrow::(); - permissions.check_read(&oldpath)?; - permissions.check_write(&newpath)?; - } - - tokio::task::spawn_blocking(move || { - debug!("op_link_async {} {}", oldpath.display(), newpath.display()); - std::fs::hard_link(&oldpath, &newpath)?; - Ok(json!({})) - }) - .await - .unwrap() -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct SymlinkArgs { - oldpath: String, - newpath: String, - #[cfg(not(unix))] - options: Option, -} - -#[cfg(not(unix))] -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct SymlinkOptions { - _type: String, -} - -fn op_symlink_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.symlink"); - let args: SymlinkArgs = serde_json::from_value(args)?; - let oldpath = PathBuf::from(&args.oldpath); - let newpath = PathBuf::from(&args.newpath); - - state.borrow::().check_write(&newpath)?; - - debug!( - "op_symlink_sync {} {}", - oldpath.display(), - newpath.display() - ); - #[cfg(unix)] - { - use std::os::unix::fs::symlink; - symlink(&oldpath, &newpath)?; - Ok(json!({})) - } - #[cfg(not(unix))] - { - use std::os::windows::fs::{symlink_dir, symlink_file}; - - match args.options { - Some(options) => match options._type.as_ref() { - "file" => symlink_file(&oldpath, &newpath)?, - "dir" => symlink_dir(&oldpath, &newpath)?, - _ => return Err(type_error("unsupported type")), - }, - None => { - let old_meta = std::fs::metadata(&oldpath); - match old_meta { - Ok(metadata) => { - if metadata.is_file() { - symlink_file(&oldpath, &newpath)? - } else if metadata.is_dir() { - symlink_dir(&oldpath, &newpath)? - } - } - Err(_) => return Err(type_error("you must pass a `options` argument for non-existent target path in windows".to_string())), - } - } - }; - Ok(json!({})) - } -} - -async fn op_symlink_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - super::check_unstable2(&state, "Deno.symlink"); - - let args: SymlinkArgs = serde_json::from_value(args)?; - let oldpath = PathBuf::from(&args.oldpath); - let newpath = PathBuf::from(&args.newpath); - - { - let state = state.borrow(); - state.borrow::().check_write(&newpath)?; - } - - tokio::task::spawn_blocking(move || { - debug!("op_symlink_async {} {}", oldpath.display(), newpath.display()); - #[cfg(unix)] - { - use std::os::unix::fs::symlink; - symlink(&oldpath, &newpath)?; - Ok(json!({})) - } - #[cfg(not(unix))] - { - use std::os::windows::fs::{symlink_dir, symlink_file}; - - match args.options { - Some(options) => match options._type.as_ref() { - "file" => symlink_file(&oldpath, &newpath)?, - "dir" => symlink_dir(&oldpath, &newpath)?, - _ => return Err(type_error("unsupported type")), - }, - None => { - let old_meta = std::fs::metadata(&oldpath); - match old_meta { - Ok(metadata) => { - if metadata.is_file() { - symlink_file(&oldpath, &newpath)? - } else if metadata.is_dir() { - symlink_dir(&oldpath, &newpath)? - } - } - Err(_) => return Err(type_error("you must pass a `options` argument for non-existent target path in windows".to_string())), - } - } - }; - Ok(json!({})) - } - }) - .await - .unwrap() -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct ReadLinkArgs { - path: String, -} - -fn op_read_link_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: ReadLinkArgs = serde_json::from_value(args)?; - let path = PathBuf::from(&args.path); - - state.borrow::().check_read(&path)?; - - debug!("op_read_link_value {}", path.display()); - let target = std::fs::read_link(&path)?.into_os_string(); - let targetstr = into_string(target)?; - Ok(json!(targetstr)) -} - -async fn op_read_link_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: ReadLinkArgs = serde_json::from_value(args)?; - let path = PathBuf::from(&args.path); - { - let state = state.borrow(); - state.borrow::().check_read(&path)?; - } - tokio::task::spawn_blocking(move || { - debug!("op_read_link_async {}", path.display()); - let target = std::fs::read_link(&path)?.into_os_string(); - let targetstr = into_string(target)?; - Ok(json!(targetstr)) - }) - .await - .unwrap() -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct FtruncateArgs { - rid: i32, - len: i32, -} - -fn op_ftruncate_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.ftruncate"); - let args: FtruncateArgs = serde_json::from_value(args)?; - let rid = args.rid as u32; - let len = args.len as u64; - std_file_resource(state, rid, |r| match r { - Ok(std_file) => std_file.set_len(len).map_err(AnyError::from), - Err(_) => Err(type_error("cannot truncate this type of resource")), - })?; - Ok(json!({})) -} - -async fn op_ftruncate_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - super::check_unstable2(&state, "Deno.ftruncate"); - let args: FtruncateArgs = serde_json::from_value(args)?; - let rid = args.rid as u32; - let len = args.len as u64; - std_file_resource(&mut state.borrow_mut(), rid, |r| match r { - Ok(std_file) => std_file.set_len(len).map_err(AnyError::from), - Err(_) => Err(type_error("cannot truncate this type of resource")), - })?; - Ok(json!({})) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct TruncateArgs { - path: String, - len: u64, -} - -fn op_truncate_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: TruncateArgs = serde_json::from_value(args)?; - let path = PathBuf::from(&args.path); - let len = args.len; - - state.borrow::().check_write(&path)?; - - debug!("op_truncate_sync {} {}", path.display(), len); - let f = std::fs::OpenOptions::new().write(true).open(&path)?; - f.set_len(len)?; - Ok(json!({})) -} - -async fn op_truncate_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: TruncateArgs = serde_json::from_value(args)?; - let path = PathBuf::from(&args.path); - let len = args.len; - { - let state = state.borrow(); - state.borrow::().check_write(&path)?; - } - tokio::task::spawn_blocking(move || { - debug!("op_truncate_async {} {}", path.display(), len); - let f = std::fs::OpenOptions::new().write(true).open(&path)?; - f.set_len(len)?; - Ok(json!({})) - }) - .await - .unwrap() -} - -fn make_temp( - dir: Option<&Path>, - prefix: Option<&str>, - suffix: Option<&str>, - is_dir: bool, -) -> std::io::Result { - let prefix_ = prefix.unwrap_or(""); - let suffix_ = suffix.unwrap_or(""); - let mut buf: PathBuf = match dir { - Some(ref p) => p.to_path_buf(), - None => temp_dir(), - } - .join("_"); - let mut rng = thread_rng(); - loop { - let unique = rng.gen::(); - buf.set_file_name(format!("{}{:08x}{}", prefix_, unique, suffix_)); - let r = if is_dir { - #[allow(unused_mut)] - let mut builder = std::fs::DirBuilder::new(); - #[cfg(unix)] - { - use std::os::unix::fs::DirBuilderExt; - builder.mode(0o700); - } - builder.create(buf.as_path()) - } else { - let mut open_options = std::fs::OpenOptions::new(); - open_options.write(true).create_new(true); - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - open_options.mode(0o600); - } - open_options.open(buf.as_path())?; - Ok(()) - }; - match r { - Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue, - Ok(_) => return Ok(buf), - Err(e) => return Err(e), - } - } -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct MakeTempArgs { - dir: Option, - prefix: Option, - suffix: Option, -} - -fn op_make_temp_dir_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: MakeTempArgs = serde_json::from_value(args)?; - - let dir = args.dir.map(|s| PathBuf::from(&s)); - let prefix = args.prefix.map(String::from); - let suffix = args.suffix.map(String::from); - - state - .borrow::() - .check_write(dir.clone().unwrap_or_else(temp_dir).as_path())?; - - // TODO(piscisaureus): use byte vector for paths, not a string. - // See https://github.com/denoland/deno/issues/627. - // We can't assume that paths are always valid utf8 strings. - let path = make_temp( - // Converting Option to Option<&str> - dir.as_deref(), - prefix.as_deref(), - suffix.as_deref(), - true, - )?; - let path_str = into_string(path.into_os_string())?; - - Ok(json!(path_str)) -} - -async fn op_make_temp_dir_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: MakeTempArgs = serde_json::from_value(args)?; - - let dir = args.dir.map(|s| PathBuf::from(&s)); - let prefix = args.prefix.map(String::from); - let suffix = args.suffix.map(String::from); - { - let state = state.borrow(); - state - .borrow::() - .check_write(dir.clone().unwrap_or_else(temp_dir).as_path())?; - } - tokio::task::spawn_blocking(move || { - // TODO(piscisaureus): use byte vector for paths, not a string. - // See https://github.com/denoland/deno/issues/627. - // We can't assume that paths are always valid utf8 strings. - let path = make_temp( - // Converting Option to Option<&str> - dir.as_deref(), - prefix.as_deref(), - suffix.as_deref(), - true, - )?; - let path_str = into_string(path.into_os_string())?; - - Ok(json!(path_str)) - }) - .await - .unwrap() -} - -fn op_make_temp_file_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: MakeTempArgs = serde_json::from_value(args)?; - - let dir = args.dir.map(|s| PathBuf::from(&s)); - let prefix = args.prefix.map(String::from); - let suffix = args.suffix.map(String::from); - - state - .borrow::() - .check_write(dir.clone().unwrap_or_else(temp_dir).as_path())?; - - // TODO(piscisaureus): use byte vector for paths, not a string. - // See https://github.com/denoland/deno/issues/627. - // We can't assume that paths are always valid utf8 strings. - let path = make_temp( - // Converting Option to Option<&str> - dir.as_deref(), - prefix.as_deref(), - suffix.as_deref(), - false, - )?; - let path_str = into_string(path.into_os_string())?; - - Ok(json!(path_str)) -} - -async fn op_make_temp_file_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: MakeTempArgs = serde_json::from_value(args)?; - - let dir = args.dir.map(|s| PathBuf::from(&s)); - let prefix = args.prefix.map(String::from); - let suffix = args.suffix.map(String::from); - { - let state = state.borrow(); - state - .borrow::() - .check_write(dir.clone().unwrap_or_else(temp_dir).as_path())?; - } - tokio::task::spawn_blocking(move || { - // TODO(piscisaureus): use byte vector for paths, not a string. - // See https://github.com/denoland/deno/issues/627. - // We can't assume that paths are always valid utf8 strings. - let path = make_temp( - // Converting Option to Option<&str> - dir.as_deref(), - prefix.as_deref(), - suffix.as_deref(), - false, - )?; - let path_str = into_string(path.into_os_string())?; - - Ok(json!(path_str)) - }) - .await - .unwrap() -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct FutimeArgs { - rid: i32, - atime: (i64, u32), - mtime: (i64, u32), -} - -fn op_futime_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.futimeSync"); - let args: FutimeArgs = serde_json::from_value(args)?; - let rid = args.rid as u32; - let atime = filetime::FileTime::from_unix_time(args.atime.0, args.atime.1); - let mtime = filetime::FileTime::from_unix_time(args.mtime.0, args.mtime.1); - - std_file_resource(state, rid, |r| match r { - Ok(std_file) => { - filetime::set_file_handle_times(std_file, Some(atime), Some(mtime)) - .map_err(AnyError::from) - } - Err(_) => Err(type_error( - "cannot futime on this type of resource".to_string(), - )), - })?; - - Ok(json!({})) -} - -async fn op_futime_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let mut state = state.borrow_mut(); - super::check_unstable(&state, "Deno.futime"); - let args: FutimeArgs = serde_json::from_value(args)?; - let rid = args.rid as u32; - let atime = filetime::FileTime::from_unix_time(args.atime.0, args.atime.1); - let mtime = filetime::FileTime::from_unix_time(args.mtime.0, args.mtime.1); - // TODO Not actually async! https://github.com/denoland/deno/issues/7400 - std_file_resource(&mut state, rid, |r| match r { - Ok(std_file) => { - filetime::set_file_handle_times(std_file, Some(atime), Some(mtime)) - .map_err(AnyError::from) - } - Err(_) => Err(type_error( - "cannot futime on this type of resource".to_string(), - )), - })?; - - Ok(json!({})) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct UtimeArgs { - path: String, - atime: (i64, u32), - mtime: (i64, u32), -} - -fn op_utime_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.utime"); - - let args: UtimeArgs = serde_json::from_value(args)?; - let path = PathBuf::from(&args.path); - let atime = filetime::FileTime::from_unix_time(args.atime.0, args.atime.1); - let mtime = filetime::FileTime::from_unix_time(args.mtime.0, args.mtime.1); - - state.borrow::().check_write(&path)?; - filetime::set_file_times(path, atime, mtime)?; - Ok(json!({})) -} - -async fn op_utime_async( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let state = state.borrow(); - super::check_unstable(&state, "Deno.utime"); - - let args: UtimeArgs = serde_json::from_value(args)?; - let path = PathBuf::from(&args.path); - let atime = filetime::FileTime::from_unix_time(args.atime.0, args.atime.1); - let mtime = filetime::FileTime::from_unix_time(args.mtime.0, args.mtime.1); - - state.borrow::().check_write(&path)?; - - tokio::task::spawn_blocking(move || { - filetime::set_file_times(path, atime, mtime)?; - Ok(json!({})) - }) - .await - .unwrap() -} - -fn op_cwd( - state: &mut OpState, - _args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let path = current_dir()?; - state - .borrow::() - .check_read_blind(&path, "CWD")?; - let path_str = into_string(path.into_os_string())?; - Ok(json!(path_str)) -} diff --git a/cli/ops/fs_events.rs b/cli/ops/fs_events.rs deleted file mode 100644 index 4832c915c..000000000 --- a/cli/ops/fs_events.rs +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::permissions::Permissions; -use deno_core::error::bad_resource_id; -use deno_core::error::AnyError; -use deno_core::futures::future::poll_fn; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use deno_core::BufVec; -use deno_core::OpState; -use deno_core::ZeroCopyBuf; -use notify::event::Event as NotifyEvent; -use notify::Error as NotifyError; -use notify::EventKind; -use notify::RecommendedWatcher; -use notify::RecursiveMode; -use notify::Watcher; -use serde::Deserialize; -use serde::Serialize; -use std::cell::RefCell; -use std::convert::From; -use std::path::PathBuf; -use std::rc::Rc; -use tokio::sync::mpsc; - -pub fn init(rt: &mut deno_core::JsRuntime) { - super::reg_json_sync(rt, "op_fs_events_open", op_fs_events_open); - super::reg_json_async(rt, "op_fs_events_poll", op_fs_events_poll); -} - -struct FsEventsResource { - #[allow(unused)] - watcher: RecommendedWatcher, - receiver: mpsc::Receiver>, -} - -/// Represents a file system event. -/// -/// We do not use the event directly from the notify crate. We flatten -/// the structure into this simpler structure. We want to only make it more -/// complex as needed. -/// -/// Feel free to expand this struct as long as you can add tests to demonstrate -/// the complexity. -#[derive(Serialize, Debug)] -struct FsEvent { - kind: String, - paths: Vec, -} - -impl From for FsEvent { - fn from(e: NotifyEvent) -> Self { - let kind = match e.kind { - EventKind::Any => "any", - EventKind::Access(_) => "access", - EventKind::Create(_) => "create", - EventKind::Modify(_) => "modify", - EventKind::Remove(_) => "remove", - EventKind::Other => todo!(), // What's this for? Leaving it out for now. - } - .to_string(); - FsEvent { - kind, - paths: e.paths, - } - } -} - -fn op_fs_events_open( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - #[derive(Deserialize)] - struct OpenArgs { - recursive: bool, - paths: Vec, - } - let args: OpenArgs = serde_json::from_value(args)?; - let (sender, receiver) = mpsc::channel::>(16); - let sender = std::sync::Mutex::new(sender); - let mut watcher: RecommendedWatcher = - Watcher::new_immediate(move |res: Result| { - let res2 = res.map(FsEvent::from).map_err(AnyError::from); - let mut sender = sender.lock().unwrap(); - // Ignore result, if send failed it means that watcher was already closed, - // but not all messages have been flushed. - let _ = sender.try_send(res2); - })?; - let recursive_mode = if args.recursive { - RecursiveMode::Recursive - } else { - RecursiveMode::NonRecursive - }; - for path in &args.paths { - state - .borrow::() - .check_read(&PathBuf::from(path))?; - watcher.watch(path, recursive_mode)?; - } - let resource = FsEventsResource { watcher, receiver }; - let rid = state.resource_table.add("fsEvents", Box::new(resource)); - Ok(json!(rid)) -} - -async fn op_fs_events_poll( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - #[derive(Deserialize)] - struct PollArgs { - rid: u32, - } - let PollArgs { rid } = serde_json::from_value(args)?; - poll_fn(move |cx| { - let mut state = state.borrow_mut(); - let watcher = state - .resource_table - .get_mut::(rid) - .ok_or_else(bad_resource_id)?; - watcher - .receiver - .poll_recv(cx) - .map(|maybe_result| match maybe_result { - Some(Ok(value)) => Ok(json!({ "value": value, "done": false })), - Some(Err(err)) => Err(err), - None => Ok(json!({ "done": true })), - }) - }) - .await -} diff --git a/cli/ops/io.rs b/cli/ops/io.rs deleted file mode 100644 index 0f8af905a..000000000 --- a/cli/ops/io.rs +++ /dev/null @@ -1,473 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use super::dispatch_minimal::minimal_op; -use super::dispatch_minimal::MinimalOp; -use crate::metrics::metrics_op; -use deno_core::error::bad_resource_id; -use deno_core::error::resource_unavailable; -use deno_core::error::type_error; -use deno_core::error::AnyError; -use deno_core::futures; -use deno_core::futures::future::poll_fn; -use deno_core::futures::future::FutureExt; -use deno_core::futures::ready; -use deno_core::BufVec; -use deno_core::JsRuntime; -use deno_core::OpState; -use std::cell::RefCell; -use std::collections::HashMap; -use std::pin::Pin; -use std::rc::Rc; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::task::Context; -use std::task::Poll; -use tokio::io::{AsyncRead, AsyncWrite}; -use tokio::net::TcpStream; -use tokio_rustls::client::TlsStream as ClientTlsStream; -use tokio_rustls::server::TlsStream as ServerTlsStream; - -#[cfg(not(windows))] -use std::os::unix::io::FromRawFd; - -#[cfg(windows)] -use std::os::windows::io::FromRawHandle; - -lazy_static! { - /// Due to portability issues on Windows handle to stdout is created from raw - /// file descriptor. The caveat of that approach is fact that when this - /// handle is dropped underlying file descriptor is closed - that is highly - /// not desirable in case of stdout. That's why we store this global handle - /// that is then cloned when obtaining stdio for process. In turn when - /// resource table is dropped storing reference to that handle, the handle - /// itself won't be closed (so Deno.core.print) will still work. - // TODO(ry) It should be possible to close stdout. - static ref STDIN_HANDLE: Option = { - #[cfg(not(windows))] - let stdin = unsafe { Some(std::fs::File::from_raw_fd(0)) }; - #[cfg(windows)] - let stdin = unsafe { - let handle = winapi::um::processenv::GetStdHandle( - winapi::um::winbase::STD_INPUT_HANDLE, - ); - if handle.is_null() { - return None; - } - Some(std::fs::File::from_raw_handle(handle)) - }; - stdin - }; - static ref STDOUT_HANDLE: Option = { - #[cfg(not(windows))] - let stdout = unsafe { Some(std::fs::File::from_raw_fd(1)) }; - #[cfg(windows)] - let stdout = unsafe { - let handle = winapi::um::processenv::GetStdHandle( - winapi::um::winbase::STD_OUTPUT_HANDLE, - ); - if handle.is_null() { - return None; - } - Some(std::fs::File::from_raw_handle(handle)) - }; - stdout - }; - static ref STDERR_HANDLE: Option = { - #[cfg(not(windows))] - let stderr = unsafe { Some(std::fs::File::from_raw_fd(2)) }; - #[cfg(windows)] - let stderr = unsafe { - let handle = winapi::um::processenv::GetStdHandle( - winapi::um::winbase::STD_ERROR_HANDLE, - ); - if handle.is_null() { - return None; - } - Some(std::fs::File::from_raw_handle(handle)) - }; - stderr - }; -} - -pub fn init(rt: &mut JsRuntime) { - rt.register_op("op_read", metrics_op(minimal_op(op_read))); - rt.register_op("op_write", metrics_op(minimal_op(op_write))); -} - -pub fn get_stdio() -> ( - Option, - Option, - Option, -) { - let stdin = get_stdio_stream(&STDIN_HANDLE); - let stdout = get_stdio_stream(&STDOUT_HANDLE); - let stderr = get_stdio_stream(&STDERR_HANDLE); - - (stdin, stdout, stderr) -} - -fn get_stdio_stream( - handle: &Option, -) -> Option { - match handle { - None => None, - Some(file_handle) => match file_handle.try_clone() { - Ok(clone) => Some(StreamResourceHolder::new(StreamResource::FsFile( - Some((tokio::fs::File::from_std(clone), FileMetadata::default())), - ))), - Err(_e) => None, - }, - } -} - -fn no_buffer_specified() -> AnyError { - type_error("no buffer specified") -} - -#[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 struct StreamResourceHolder { - pub resource: StreamResource, - waker: HashMap, - waker_counter: AtomicUsize, -} - -impl StreamResourceHolder { - pub fn new(resource: StreamResource) -> StreamResourceHolder { - StreamResourceHolder { - resource, - // Atleast one task is expecter for the resource - waker: HashMap::with_capacity(1), - // Tracks wakers Ids - waker_counter: AtomicUsize::new(0), - } - } -} - -impl Drop for StreamResourceHolder { - fn drop(&mut self) { - self.wake_tasks(); - } -} - -impl StreamResourceHolder { - pub fn track_task(&mut self, cx: &Context) -> Result { - let waker = futures::task::AtomicWaker::new(); - waker.register(cx.waker()); - // Its OK if it overflows - let task_waker_id = self.waker_counter.fetch_add(1, Ordering::Relaxed); - self.waker.insert(task_waker_id, waker); - Ok(task_waker_id) - } - - pub fn wake_tasks(&mut self) { - for waker in self.waker.values() { - waker.wake(); - } - } - - pub fn untrack_task(&mut self, task_waker_id: usize) { - self.waker.remove(&task_waker_id); - } -} - -pub enum StreamResource { - FsFile(Option<(tokio::fs::File, FileMetadata)>), - TcpStream(Option), - #[cfg(not(windows))] - UnixStream(tokio::net::UnixStream), - ServerTlsStream(Box>), - ClientTlsStream(Box>), - ChildStdin(tokio::process::ChildStdin), - ChildStdout(tokio::process::ChildStdout), - ChildStderr(tokio::process::ChildStderr), -} - -trait UnpinAsyncRead: AsyncRead + Unpin {} -trait UnpinAsyncWrite: AsyncWrite + Unpin {} - -impl UnpinAsyncRead for T {} -impl UnpinAsyncWrite for T {} - -/// `DenoAsyncRead` is the same as the `tokio_io::AsyncRead` trait -/// but uses an `AnyError` error instead of `std::io:Error` -pub trait DenoAsyncRead { - fn poll_read( - &mut self, - cx: &mut Context, - buf: &mut [u8], - ) -> Poll>; -} - -impl DenoAsyncRead for StreamResource { - fn poll_read( - &mut self, - cx: &mut Context, - buf: &mut [u8], - ) -> Poll> { - use StreamResource::*; - let f: &mut dyn UnpinAsyncRead = match self { - FsFile(Some((f, _))) => f, - FsFile(None) => return Poll::Ready(Err(resource_unavailable())), - TcpStream(Some(f)) => f, - #[cfg(not(windows))] - UnixStream(f) => f, - ClientTlsStream(f) => f, - ServerTlsStream(f) => f, - ChildStdout(f) => f, - ChildStderr(f) => f, - _ => return Err(bad_resource_id()).into(), - }; - let v = ready!(Pin::new(f).poll_read(cx, buf))?; - Ok(v).into() - } -} - -pub fn op_read( - state: Rc>, - is_sync: bool, - rid: i32, - mut zero_copy: BufVec, -) -> MinimalOp { - debug!("read rid={}", rid); - match zero_copy.len() { - 0 => return MinimalOp::Sync(Err(no_buffer_specified())), - 1 => {} - _ => panic!("Invalid number of arguments"), - } - - if is_sync { - MinimalOp::Sync({ - // First we look up the rid in the resource table. - std_file_resource(&mut state.borrow_mut(), rid as u32, move |r| match r { - Ok(std_file) => { - use std::io::Read; - std_file - .read(&mut zero_copy[0]) - .map(|n: usize| n as i32) - .map_err(AnyError::from) - } - Err(_) => Err(type_error("sync read not allowed on this resource")), - }) - }) - } else { - let mut zero_copy = zero_copy[0].clone(); - MinimalOp::Async( - poll_fn(move |cx| { - let mut state = state.borrow_mut(); - let resource_holder = state - .resource_table - .get_mut::(rid as u32) - .ok_or_else(bad_resource_id)?; - - let mut task_tracker_id: Option = None; - let nread = match resource_holder.resource.poll_read(cx, &mut zero_copy) - { - Poll::Ready(t) => { - if let Some(id) = task_tracker_id { - resource_holder.untrack_task(id); - } - t - } - Poll::Pending => { - task_tracker_id.replace(resource_holder.track_task(cx)?); - return Poll::Pending; - } - }?; - Poll::Ready(Ok(nread as i32)) - }) - .boxed_local(), - ) - } -} - -/// `DenoAsyncWrite` is the same as the `tokio_io::AsyncWrite` trait -/// but uses an `AnyError` error instead of `std::io:Error` -pub trait DenoAsyncWrite { - fn poll_write( - &mut self, - cx: &mut Context, - buf: &[u8], - ) -> Poll>; - - fn poll_close(&mut self, cx: &mut Context) -> Poll>; - - fn poll_flush(&mut self, cx: &mut Context) -> Poll>; -} - -impl DenoAsyncWrite for StreamResource { - fn poll_write( - &mut self, - cx: &mut Context, - buf: &[u8], - ) -> Poll> { - use StreamResource::*; - let f: &mut dyn UnpinAsyncWrite = match self { - FsFile(Some((f, _))) => f, - FsFile(None) => return Poll::Pending, - TcpStream(Some(f)) => f, - #[cfg(not(windows))] - UnixStream(f) => f, - ClientTlsStream(f) => f, - ServerTlsStream(f) => f, - ChildStdin(f) => f, - _ => return Err(bad_resource_id()).into(), - }; - - let v = ready!(Pin::new(f).poll_write(cx, buf))?; - Ok(v).into() - } - - fn poll_flush(&mut self, cx: &mut Context) -> Poll> { - use StreamResource::*; - let f: &mut dyn UnpinAsyncWrite = match self { - FsFile(Some((f, _))) => f, - FsFile(None) => return Poll::Pending, - TcpStream(Some(f)) => f, - #[cfg(not(windows))] - UnixStream(f) => f, - ClientTlsStream(f) => f, - ServerTlsStream(f) => f, - ChildStdin(f) => f, - _ => return Err(bad_resource_id()).into(), - }; - - ready!(Pin::new(f).poll_flush(cx))?; - Ok(()).into() - } - - fn poll_close(&mut self, _cx: &mut Context) -> Poll> { - unimplemented!() - } -} - -pub fn op_write( - state: Rc>, - is_sync: bool, - rid: i32, - zero_copy: BufVec, -) -> MinimalOp { - debug!("write rid={}", rid); - match zero_copy.len() { - 0 => return MinimalOp::Sync(Err(no_buffer_specified())), - 1 => {} - _ => panic!("Invalid number of arguments"), - } - - if is_sync { - MinimalOp::Sync({ - // First we look up the rid in the resource table. - std_file_resource(&mut state.borrow_mut(), rid as u32, move |r| match r { - Ok(std_file) => { - use std::io::Write; - std_file - .write(&zero_copy[0]) - .map(|nwritten: usize| nwritten as i32) - .map_err(AnyError::from) - } - Err(_) => Err(type_error("sync read not allowed on this resource")), - }) - }) - } else { - let zero_copy = zero_copy[0].clone(); - MinimalOp::Async( - async move { - let nwritten = poll_fn(|cx| { - let mut state = state.borrow_mut(); - let resource_holder = state - .resource_table - .get_mut::(rid as u32) - .ok_or_else(bad_resource_id)?; - resource_holder.resource.poll_write(cx, &zero_copy) - }) - .await?; - - // TODO(bartlomieju): this step was added during upgrade to Tokio 0.2 - // and the reasons for the need to explicitly flush are not fully known. - // Figure out why it's needed and preferably remove it. - // https://github.com/denoland/deno/issues/3565 - poll_fn(|cx| { - let mut state = state.borrow_mut(); - let resource_holder = state - .resource_table - .get_mut::(rid as u32) - .ok_or_else(bad_resource_id)?; - resource_holder.resource.poll_flush(cx) - }) - .await?; - - Ok(nwritten as i32) - } - .boxed_local(), - ) - } -} - -/// Helper function for operating on a std::fs::File stored in the resource table. -/// -/// We store file system file resources as tokio::fs::File, so this is a little -/// utility function that gets a std::fs:File when you need to do blocking -/// operations. -/// -/// Returns ErrorKind::Busy if the resource is being used by another op. -pub fn std_file_resource( - state: &mut OpState, - rid: u32, - mut f: F, -) -> Result -where - F: FnMut( - Result<&mut std::fs::File, &mut StreamResource>, - ) -> Result, -{ - // First we look up the rid in the resource table. - let mut r = state.resource_table.get_mut::(rid); - if let Some(ref mut resource_holder) = r { - // Sync write only works for FsFile. It doesn't make sense to do this - // for non-blocking sockets. So we error out if not FsFile. - match &mut resource_holder.resource { - StreamResource::FsFile(option_file_metadata) => { - // The object in the resource table is a tokio::fs::File - but in - // order to do a blocking write on it, we must turn it into a - // std::fs::File. Hopefully this code compiles down to nothing. - if let Some((tokio_file, metadata)) = option_file_metadata.take() { - match tokio_file.try_into_std() { - Ok(mut std_file) => { - let result = f(Ok(&mut std_file)); - // Turn the std_file handle back into a tokio file, put it back - // in the resource table. - let tokio_file = tokio::fs::File::from_std(std_file); - resource_holder.resource = - StreamResource::FsFile(Some((tokio_file, metadata))); - // return the result. - result - } - Err(tokio_file) => { - // This function will return an error containing the file if - // some operation is in-flight. - resource_holder.resource = - StreamResource::FsFile(Some((tokio_file, metadata))); - Err(resource_unavailable()) - } - } - } else { - Err(resource_unavailable()) - } - } - _ => f(Err(&mut resource_holder.resource)), - } - } else { - Err(bad_resource_id()) - } -} diff --git a/cli/ops/mod.rs b/cli/ops/mod.rs index 56c0f1ad5..24eca3e77 100644 --- a/cli/ops/mod.rs +++ b/cli/ops/mod.rs @@ -1,32 +1,8 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -mod dispatch_minimal; -pub use dispatch_minimal::MinimalOp; - -pub mod crypto; pub mod errors; -pub mod fetch; -pub mod fs; -pub mod fs_events; -pub mod io; -pub mod net; -#[cfg(unix)] -mod net_unix; -pub mod os; -pub mod permissions; -pub mod plugin; -pub mod process; -pub mod runtime; pub mod runtime_compiler; -pub mod signal; -pub mod timers; -pub mod tls; -pub mod tty; -pub mod web_worker; -pub mod websocket; -pub mod worker_host; -use crate::metrics::metrics_op; use deno_core::error::AnyError; use deno_core::json_op_async; use deno_core::json_op_sync; @@ -35,6 +11,7 @@ use deno_core::BufVec; use deno_core::JsRuntime; use deno_core::OpState; use deno_core::ZeroCopyBuf; +use deno_runtime::metrics::metrics_op; use std::cell::RefCell; use std::future::Future; use std::rc::Rc; @@ -54,34 +31,3 @@ where { rt.register_op(name, metrics_op(json_op_sync(op_fn))); } - -pub struct UnstableChecker { - pub unstable: bool, -} - -impl UnstableChecker { - /// Quits the process if the --unstable flag was not provided. - /// - /// This is intentionally a non-recoverable check so that people cannot probe - /// for unstable APIs from stable programs. - // NOTE(bartlomieju): keep in sync with `cli/program_state.rs` - pub fn check_unstable(&self, api_name: &str) { - if !self.unstable { - eprintln!( - "Unstable API '{}'. The --unstable flag must be provided.", - api_name - ); - std::process::exit(70); - } - } -} -/// Helper for checking unstable features. Used for sync ops. -pub fn check_unstable(state: &OpState, api_name: &str) { - state.borrow::().check_unstable(api_name) -} - -/// Helper for checking unstable features. Used for async ops. -pub fn check_unstable2(state: &Rc>, api_name: &str) { - let state = state.borrow(); - state.borrow::().check_unstable(api_name) -} diff --git a/cli/ops/net.rs b/cli/ops/net.rs deleted file mode 100644 index 98ff83fc0..000000000 --- a/cli/ops/net.rs +++ /dev/null @@ -1,566 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::ops::io::StreamResource; -use crate::ops::io::StreamResourceHolder; -use crate::permissions::Permissions; -use crate::resolve_addr::resolve_addr; -use deno_core::error::bad_resource; -use deno_core::error::bad_resource_id; -use deno_core::error::custom_error; -use deno_core::error::generic_error; -use deno_core::error::type_error; -use deno_core::error::AnyError; -use deno_core::futures; -use deno_core::futures::future::poll_fn; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use deno_core::BufVec; -use deno_core::OpState; -use deno_core::ZeroCopyBuf; -use serde::Deserialize; -use std::cell::RefCell; -use std::net::Shutdown; -use std::net::SocketAddr; -use std::rc::Rc; -use std::task::Context; -use std::task::Poll; -use tokio::net::TcpListener; -use tokio::net::TcpStream; -use tokio::net::UdpSocket; - -#[cfg(unix)] -use super::net_unix; -#[cfg(unix)] -use std::path::Path; - -pub fn init(rt: &mut deno_core::JsRuntime) { - super::reg_json_async(rt, "op_accept", op_accept); - super::reg_json_async(rt, "op_connect", op_connect); - super::reg_json_sync(rt, "op_shutdown", op_shutdown); - super::reg_json_sync(rt, "op_listen", op_listen); - super::reg_json_async(rt, "op_datagram_receive", op_datagram_receive); - super::reg_json_async(rt, "op_datagram_send", op_datagram_send); -} - -#[derive(Deserialize)] -pub(crate) struct AcceptArgs { - pub rid: i32, - pub transport: String, -} - -async fn accept_tcp( - state: Rc>, - args: AcceptArgs, - _zero_copy: BufVec, -) -> Result { - let rid = args.rid as u32; - - let accept_fut = poll_fn(|cx| { - let mut state = state.borrow_mut(); - let listener_resource = state - .resource_table - .get_mut::(rid) - .ok_or_else(|| bad_resource("Listener has been closed"))?; - let listener = &mut listener_resource.listener; - match listener.poll_accept(cx).map_err(AnyError::from) { - Poll::Ready(Ok((stream, addr))) => { - listener_resource.untrack_task(); - Poll::Ready(Ok((stream, addr))) - } - Poll::Pending => { - listener_resource.track_task(cx)?; - Poll::Pending - } - Poll::Ready(Err(e)) => { - listener_resource.untrack_task(); - Poll::Ready(Err(e)) - } - } - }); - let (tcp_stream, _socket_addr) = accept_fut.await?; - let local_addr = tcp_stream.local_addr()?; - let remote_addr = tcp_stream.peer_addr()?; - - let mut state = state.borrow_mut(); - let rid = state.resource_table.add( - "tcpStream", - Box::new(StreamResourceHolder::new(StreamResource::TcpStream(Some( - tcp_stream, - )))), - ); - Ok(json!({ - "rid": rid, - "localAddr": { - "hostname": local_addr.ip().to_string(), - "port": local_addr.port(), - "transport": "tcp", - }, - "remoteAddr": { - "hostname": remote_addr.ip().to_string(), - "port": remote_addr.port(), - "transport": "tcp", - } - })) -} - -async fn op_accept( - state: Rc>, - args: Value, - bufs: BufVec, -) -> Result { - let args: AcceptArgs = serde_json::from_value(args)?; - match args.transport.as_str() { - "tcp" => accept_tcp(state, args, bufs).await, - #[cfg(unix)] - "unix" => net_unix::accept_unix(state, args, bufs).await, - _ => Err(generic_error(format!( - "Unsupported transport protocol {}", - args.transport - ))), - } -} - -#[derive(Deserialize)] -pub(crate) struct ReceiveArgs { - pub rid: i32, - pub transport: String, -} - -async fn receive_udp( - state: Rc>, - args: ReceiveArgs, - zero_copy: BufVec, -) -> Result { - assert_eq!(zero_copy.len(), 1, "Invalid number of arguments"); - let mut zero_copy = zero_copy[0].clone(); - - let rid = args.rid as u32; - - let receive_fut = poll_fn(|cx| { - let mut state = state.borrow_mut(); - let resource = state - .resource_table - .get_mut::(rid) - .ok_or_else(|| bad_resource("Socket has been closed"))?; - let socket = &mut resource.socket; - socket - .poll_recv_from(cx, &mut zero_copy) - .map_err(AnyError::from) - }); - let (size, remote_addr) = receive_fut.await?; - Ok(json!({ - "size": size, - "remoteAddr": { - "hostname": remote_addr.ip().to_string(), - "port": remote_addr.port(), - "transport": "udp", - } - })) -} - -async fn op_datagram_receive( - state: Rc>, - args: Value, - zero_copy: BufVec, -) -> Result { - assert_eq!(zero_copy.len(), 1, "Invalid number of arguments"); - - let args: ReceiveArgs = serde_json::from_value(args)?; - match args.transport.as_str() { - "udp" => receive_udp(state, args, zero_copy).await, - #[cfg(unix)] - "unixpacket" => net_unix::receive_unix_packet(state, args, zero_copy).await, - _ => Err(generic_error(format!( - "Unsupported transport protocol {}", - args.transport - ))), - } -} - -#[derive(Deserialize)] -struct SendArgs { - rid: i32, - transport: String, - #[serde(flatten)] - transport_args: ArgsEnum, -} - -async fn op_datagram_send( - state: Rc>, - args: Value, - zero_copy: BufVec, -) -> Result { - assert_eq!(zero_copy.len(), 1, "Invalid number of arguments"); - let zero_copy = zero_copy[0].clone(); - - match serde_json::from_value(args)? { - SendArgs { - rid, - transport, - transport_args: ArgsEnum::Ip(args), - } if transport == "udp" => { - { - let s = state.borrow(); - s.borrow::() - .check_net(&args.hostname, args.port)?; - } - let addr = resolve_addr(&args.hostname, args.port)?; - poll_fn(move |cx| { - let mut state = state.borrow_mut(); - let resource = state - .resource_table - .get_mut::(rid as u32) - .ok_or_else(|| bad_resource("Socket has been closed"))?; - resource - .socket - .poll_send_to(cx, &zero_copy, &addr) - .map_ok(|byte_length| json!(byte_length)) - .map_err(AnyError::from) - }) - .await - } - #[cfg(unix)] - SendArgs { - rid, - transport, - transport_args: ArgsEnum::Unix(args), - } if transport == "unixpacket" => { - let address_path = Path::new(&args.path); - { - let s = state.borrow(); - s.borrow::().check_write(&address_path)?; - } - let mut state = state.borrow_mut(); - let resource = state - .resource_table - .get_mut::(rid as u32) - .ok_or_else(|| { - custom_error("NotConnected", "Socket has been closed") - })?; - let socket = &mut resource.socket; - let byte_length = socket - .send_to(&zero_copy, &resource.local_addr.as_pathname().unwrap()) - .await?; - - Ok(json!(byte_length)) - } - _ => Err(type_error("Wrong argument format!")), - } -} - -#[derive(Deserialize)] -struct ConnectArgs { - transport: String, - #[serde(flatten)] - transport_args: ArgsEnum, -} - -async fn op_connect( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - match serde_json::from_value(args)? { - ConnectArgs { - transport, - transport_args: ArgsEnum::Ip(args), - } if transport == "tcp" => { - { - let state_ = state.borrow(); - state_ - .borrow::() - .check_net(&args.hostname, args.port)?; - } - let addr = resolve_addr(&args.hostname, args.port)?; - let tcp_stream = TcpStream::connect(&addr).await?; - let local_addr = tcp_stream.local_addr()?; - let remote_addr = tcp_stream.peer_addr()?; - - let mut state_ = state.borrow_mut(); - let rid = state_.resource_table.add( - "tcpStream", - Box::new(StreamResourceHolder::new(StreamResource::TcpStream(Some( - tcp_stream, - )))), - ); - Ok(json!({ - "rid": rid, - "localAddr": { - "hostname": local_addr.ip().to_string(), - "port": local_addr.port(), - "transport": transport, - }, - "remoteAddr": { - "hostname": remote_addr.ip().to_string(), - "port": remote_addr.port(), - "transport": transport, - } - })) - } - #[cfg(unix)] - ConnectArgs { - transport, - transport_args: ArgsEnum::Unix(args), - } if transport == "unix" => { - let address_path = Path::new(&args.path); - super::check_unstable2(&state, "Deno.connect"); - { - let state_ = state.borrow(); - state_.borrow::().check_read(&address_path)?; - state_.borrow::().check_write(&address_path)?; - } - let path = args.path; - let unix_stream = net_unix::UnixStream::connect(Path::new(&path)).await?; - let local_addr = unix_stream.local_addr()?; - let remote_addr = unix_stream.peer_addr()?; - - let mut state_ = state.borrow_mut(); - let rid = state_.resource_table.add( - "unixStream", - Box::new(StreamResourceHolder::new(StreamResource::UnixStream( - unix_stream, - ))), - ); - Ok(json!({ - "rid": rid, - "localAddr": { - "path": local_addr.as_pathname(), - "transport": transport, - }, - "remoteAddr": { - "path": remote_addr.as_pathname(), - "transport": transport, - } - })) - } - _ => Err(type_error("Wrong argument format!")), - } -} - -#[derive(Deserialize)] -struct ShutdownArgs { - rid: i32, - how: i32, -} - -fn op_shutdown( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.shutdown"); - - let args: ShutdownArgs = serde_json::from_value(args)?; - - let rid = args.rid as u32; - let how = args.how; - - let shutdown_mode = match how { - 0 => Shutdown::Read, - 1 => Shutdown::Write, - _ => unimplemented!(), - }; - - let resource_holder = state - .resource_table - .get_mut::(rid) - .ok_or_else(bad_resource_id)?; - match resource_holder.resource { - StreamResource::TcpStream(Some(ref mut stream)) => { - TcpStream::shutdown(stream, shutdown_mode)?; - } - #[cfg(unix)] - StreamResource::UnixStream(ref mut stream) => { - net_unix::UnixStream::shutdown(stream, shutdown_mode)?; - } - _ => return Err(bad_resource_id()), - } - - Ok(json!({})) -} - -#[allow(dead_code)] -struct TcpListenerResource { - listener: TcpListener, - waker: Option, - local_addr: SocketAddr, -} - -impl Drop for TcpListenerResource { - fn drop(&mut self) { - self.wake_task(); - } -} - -impl TcpListenerResource { - /// Track the current task so future awaiting for connection - /// can be notified when listener is closed. - /// - /// Throws an error if another task is already tracked. - pub fn track_task(&mut self, cx: &Context) -> Result<(), AnyError> { - // Currently, we only allow tracking a single accept task for a listener. - // This might be changed in the future with multiple workers. - // Caveat: TcpListener by itself also only tracks an accept task at a time. - // See https://github.com/tokio-rs/tokio/issues/846#issuecomment-454208883 - if self.waker.is_some() { - return Err(custom_error("Busy", "Another accept task is ongoing")); - } - - let waker = futures::task::AtomicWaker::new(); - waker.register(cx.waker()); - self.waker.replace(waker); - Ok(()) - } - - /// Notifies a task when listener is closed so accept future can resolve. - pub fn wake_task(&mut self) { - if let Some(waker) = self.waker.as_ref() { - waker.wake(); - } - } - - /// Stop tracking a task. - /// Happens when the task is done and thus no further tracking is needed. - pub fn untrack_task(&mut self) { - if self.waker.is_some() { - self.waker.take(); - } - } -} - -struct UdpSocketResource { - socket: UdpSocket, -} - -#[derive(Deserialize)] -struct IpListenArgs { - hostname: String, - port: u16, -} - -#[derive(Deserialize)] -#[serde(untagged)] -enum ArgsEnum { - Ip(IpListenArgs), - #[cfg(unix)] - Unix(net_unix::UnixListenArgs), -} - -#[derive(Deserialize)] -struct ListenArgs { - transport: String, - #[serde(flatten)] - transport_args: ArgsEnum, -} - -fn listen_tcp( - state: &mut OpState, - addr: SocketAddr, -) -> Result<(u32, SocketAddr), AnyError> { - let std_listener = std::net::TcpListener::bind(&addr)?; - let listener = TcpListener::from_std(std_listener)?; - let local_addr = listener.local_addr()?; - let listener_resource = TcpListenerResource { - listener, - waker: None, - local_addr, - }; - let rid = state - .resource_table - .add("tcpListener", Box::new(listener_resource)); - - Ok((rid, local_addr)) -} - -fn listen_udp( - state: &mut OpState, - addr: SocketAddr, -) -> Result<(u32, SocketAddr), AnyError> { - let std_socket = std::net::UdpSocket::bind(&addr)?; - let socket = UdpSocket::from_std(std_socket)?; - let local_addr = socket.local_addr()?; - let socket_resource = UdpSocketResource { socket }; - let rid = state - .resource_table - .add("udpSocket", Box::new(socket_resource)); - - Ok((rid, local_addr)) -} - -fn op_listen( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let permissions = state.borrow::(); - match serde_json::from_value(args)? { - ListenArgs { - transport, - transport_args: ArgsEnum::Ip(args), - } => { - { - if transport == "udp" { - super::check_unstable(state, "Deno.listenDatagram"); - } - permissions.check_net(&args.hostname, args.port)?; - } - let addr = resolve_addr(&args.hostname, args.port)?; - let (rid, local_addr) = if transport == "tcp" { - listen_tcp(state, addr)? - } else { - listen_udp(state, addr)? - }; - debug!( - "New listener {} {}:{}", - rid, - local_addr.ip().to_string(), - local_addr.port() - ); - Ok(json!({ - "rid": rid, - "localAddr": { - "hostname": local_addr.ip().to_string(), - "port": local_addr.port(), - "transport": transport, - }, - })) - } - #[cfg(unix)] - ListenArgs { - transport, - transport_args: ArgsEnum::Unix(args), - } if transport == "unix" || transport == "unixpacket" => { - let address_path = Path::new(&args.path); - { - if transport == "unix" { - super::check_unstable(state, "Deno.listen"); - } - if transport == "unixpacket" { - super::check_unstable(state, "Deno.listenDatagram"); - } - permissions.check_read(&address_path)?; - permissions.check_write(&address_path)?; - } - let (rid, local_addr) = if transport == "unix" { - net_unix::listen_unix(state, &address_path)? - } else { - net_unix::listen_unix_packet(state, &address_path)? - }; - debug!( - "New listener {} {}", - rid, - local_addr.as_pathname().unwrap().display(), - ); - Ok(json!({ - "rid": rid, - "localAddr": { - "path": local_addr.as_pathname(), - "transport": transport, - }, - })) - } - #[cfg(unix)] - _ => Err(type_error("Wrong argument format!")), - } -} diff --git a/cli/ops/net_unix.rs b/cli/ops/net_unix.rs deleted file mode 100644 index 4c416a5a4..000000000 --- a/cli/ops/net_unix.rs +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::ops::io::StreamResource; -use crate::ops::io::StreamResourceHolder; -use crate::ops::net::AcceptArgs; -use crate::ops::net::ReceiveArgs; -use deno_core::error::bad_resource; -use deno_core::error::AnyError; -use deno_core::futures::future::poll_fn; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use deno_core::BufVec; -use deno_core::OpState; -use serde::Deserialize; -use std::cell::RefCell; -use std::fs::remove_file; -use std::os::unix; -use std::path::Path; -use std::rc::Rc; -use std::task::Poll; -use tokio::net::UnixDatagram; -use tokio::net::UnixListener; -pub use tokio::net::UnixStream; - -struct UnixListenerResource { - listener: UnixListener, -} - -pub struct UnixDatagramResource { - pub socket: UnixDatagram, - pub local_addr: unix::net::SocketAddr, -} - -#[derive(Deserialize)] -pub struct UnixListenArgs { - pub path: String, -} - -pub(crate) async fn accept_unix( - state: Rc>, - args: AcceptArgs, - _bufs: BufVec, -) -> Result { - let rid = args.rid as u32; - - let accept_fut = poll_fn(|cx| { - let mut state = state.borrow_mut(); - let listener_resource = state - .resource_table - .get_mut::(rid) - .ok_or_else(|| bad_resource("Listener has been closed"))?; - let listener = &mut listener_resource.listener; - use deno_core::futures::StreamExt; - match listener.poll_next_unpin(cx) { - Poll::Ready(Some(stream)) => { - //listener_resource.untrack_task(); - Poll::Ready(stream) - } - Poll::Ready(None) => todo!(), - Poll::Pending => { - //listener_resource.track_task(cx)?; - Poll::Pending - } - } - .map_err(AnyError::from) - }); - let unix_stream = accept_fut.await?; - - let local_addr = unix_stream.local_addr()?; - let remote_addr = unix_stream.peer_addr()?; - let mut state = state.borrow_mut(); - let rid = state.resource_table.add( - "unixStream", - Box::new(StreamResourceHolder::new(StreamResource::UnixStream( - unix_stream, - ))), - ); - Ok(json!({ - "rid": rid, - "localAddr": { - "path": local_addr.as_pathname(), - "transport": "unix", - }, - "remoteAddr": { - "path": remote_addr.as_pathname(), - "transport": "unix", - } - })) -} - -pub(crate) async fn receive_unix_packet( - state: Rc>, - args: ReceiveArgs, - bufs: BufVec, -) -> Result { - assert_eq!(bufs.len(), 1, "Invalid number of arguments"); - - let rid = args.rid as u32; - let mut buf = bufs.into_iter().next().unwrap(); - - let mut state = state.borrow_mut(); - let resource = state - .resource_table - .get_mut::(rid) - .ok_or_else(|| bad_resource("Socket has been closed"))?; - let (size, remote_addr) = resource.socket.recv_from(&mut buf).await?; - Ok(json!({ - "size": size, - "remoteAddr": { - "path": remote_addr.as_pathname(), - "transport": "unixpacket", - } - })) -} - -pub fn listen_unix( - state: &mut OpState, - addr: &Path, -) -> Result<(u32, unix::net::SocketAddr), AnyError> { - if addr.exists() { - remove_file(&addr).unwrap(); - } - let listener = UnixListener::bind(&addr)?; - let local_addr = listener.local_addr()?; - let listener_resource = UnixListenerResource { listener }; - let rid = state - .resource_table - .add("unixListener", Box::new(listener_resource)); - - Ok((rid, local_addr)) -} - -pub fn listen_unix_packet( - state: &mut OpState, - addr: &Path, -) -> Result<(u32, unix::net::SocketAddr), AnyError> { - if addr.exists() { - remove_file(&addr).unwrap(); - } - let socket = UnixDatagram::bind(&addr)?; - let local_addr = socket.local_addr()?; - let datagram_resource = UnixDatagramResource { - socket, - local_addr: local_addr.clone(), - }; - let rid = state - .resource_table - .add("unixDatagram", Box::new(datagram_resource)); - - Ok((rid, local_addr)) -} diff --git a/cli/ops/os.rs b/cli/ops/os.rs deleted file mode 100644 index 6fd404a23..000000000 --- a/cli/ops/os.rs +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::permissions::Permissions; -use deno_core::error::AnyError; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use deno_core::url::Url; -use deno_core::OpState; -use deno_core::ZeroCopyBuf; -use serde::Deserialize; -use std::collections::HashMap; -use std::env; - -pub fn init(rt: &mut deno_core::JsRuntime) { - super::reg_json_sync(rt, "op_exit", op_exit); - super::reg_json_sync(rt, "op_env", op_env); - super::reg_json_sync(rt, "op_exec_path", op_exec_path); - super::reg_json_sync(rt, "op_set_env", op_set_env); - super::reg_json_sync(rt, "op_get_env", op_get_env); - super::reg_json_sync(rt, "op_delete_env", op_delete_env); - super::reg_json_sync(rt, "op_hostname", op_hostname); - super::reg_json_sync(rt, "op_loadavg", op_loadavg); - super::reg_json_sync(rt, "op_os_release", op_os_release); - super::reg_json_sync(rt, "op_system_memory_info", op_system_memory_info); - super::reg_json_sync(rt, "op_system_cpu_info", op_system_cpu_info); -} - -fn op_exec_path( - state: &mut OpState, - _args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let current_exe = env::current_exe().unwrap(); - state - .borrow::() - .check_read_blind(¤t_exe, "exec_path")?; - // Now apply URL parser to current exe to get fully resolved path, otherwise - // we might get `./` and `../` bits in `exec_path` - let exe_url = Url::from_file_path(current_exe).unwrap(); - let path = exe_url.to_file_path().unwrap(); - Ok(json!(path)) -} - -#[derive(Deserialize)] -struct SetEnv { - key: String, - value: String, -} - -fn op_set_env( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: SetEnv = serde_json::from_value(args)?; - state.borrow::().check_env()?; - env::set_var(args.key, args.value); - Ok(json!({})) -} - -fn op_env( - state: &mut OpState, - _args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - state.borrow::().check_env()?; - let v = env::vars().collect::>(); - Ok(json!(v)) -} - -#[derive(Deserialize)] -struct GetEnv { - key: String, -} - -fn op_get_env( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: GetEnv = serde_json::from_value(args)?; - state.borrow::().check_env()?; - let r = match env::var(args.key) { - Err(env::VarError::NotPresent) => json!([]), - v => json!([v?]), - }; - Ok(r) -} - -#[derive(Deserialize)] -struct DeleteEnv { - key: String, -} - -fn op_delete_env( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: DeleteEnv = serde_json::from_value(args)?; - state.borrow::().check_env()?; - env::remove_var(args.key); - Ok(json!({})) -} - -#[derive(Deserialize)] -struct Exit { - code: i32, -} - -fn op_exit( - _state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: Exit = serde_json::from_value(args)?; - std::process::exit(args.code) -} - -fn op_loadavg( - state: &mut OpState, - _args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.loadavg"); - state.borrow::().check_env()?; - match sys_info::loadavg() { - Ok(loadavg) => Ok(json!([loadavg.one, loadavg.five, loadavg.fifteen])), - Err(_) => Ok(json!([0f64, 0f64, 0f64])), - } -} - -fn op_hostname( - state: &mut OpState, - _args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.hostname"); - state.borrow::().check_env()?; - let hostname = sys_info::hostname().unwrap_or_else(|_| "".to_string()); - Ok(json!(hostname)) -} - -fn op_os_release( - state: &mut OpState, - _args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.osRelease"); - state.borrow::().check_env()?; - let release = sys_info::os_release().unwrap_or_else(|_| "".to_string()); - Ok(json!(release)) -} - -fn op_system_memory_info( - state: &mut OpState, - _args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.systemMemoryInfo"); - state.borrow::().check_env()?; - match sys_info::mem_info() { - Ok(info) => Ok(json!({ - "total": info.total, - "free": info.free, - "available": info.avail, - "buffers": info.buffers, - "cached": info.cached, - "swapTotal": info.swap_total, - "swapFree": info.swap_free - })), - Err(_) => Ok(json!({})), - } -} - -fn op_system_cpu_info( - state: &mut OpState, - _args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.systemCpuInfo"); - state.borrow::().check_env()?; - - let cores = sys_info::cpu_num().ok(); - let speed = sys_info::cpu_speed().ok(); - - Ok(json!({ - "cores": cores, - "speed": speed - })) -} diff --git a/cli/ops/permissions.rs b/cli/ops/permissions.rs deleted file mode 100644 index 7474c0e37..000000000 --- a/cli/ops/permissions.rs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::permissions::Permissions; -use deno_core::error::custom_error; -use deno_core::error::AnyError; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use deno_core::OpState; -use deno_core::ZeroCopyBuf; -use serde::Deserialize; -use std::path::Path; - -pub fn init(rt: &mut deno_core::JsRuntime) { - super::reg_json_sync(rt, "op_query_permission", op_query_permission); - super::reg_json_sync(rt, "op_revoke_permission", op_revoke_permission); - super::reg_json_sync(rt, "op_request_permission", op_request_permission); -} - -#[derive(Deserialize)] -struct PermissionArgs { - name: String, - url: Option, - path: Option, -} - -pub fn op_query_permission( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: PermissionArgs = serde_json::from_value(args)?; - let permissions = state.borrow::(); - let path = args.path.as_deref(); - let perm = match args.name.as_ref() { - "read" => permissions.query_read(&path.as_deref().map(Path::new)), - "write" => permissions.query_write(&path.as_deref().map(Path::new)), - "net" => permissions.query_net_url(&args.url.as_deref())?, - "env" => permissions.query_env(), - "run" => permissions.query_run(), - "plugin" => permissions.query_plugin(), - "hrtime" => permissions.query_hrtime(), - n => { - return Err(custom_error( - "ReferenceError", - format!("No such permission name: {}", n), - )) - } - }; - Ok(json!({ "state": perm.to_string() })) -} - -pub fn op_revoke_permission( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: PermissionArgs = serde_json::from_value(args)?; - let permissions = state.borrow_mut::(); - let path = args.path.as_deref(); - let perm = match args.name.as_ref() { - "read" => permissions.revoke_read(&path.as_deref().map(Path::new)), - "write" => permissions.revoke_write(&path.as_deref().map(Path::new)), - "net" => permissions.revoke_net(&args.url.as_deref())?, - "env" => permissions.revoke_env(), - "run" => permissions.revoke_run(), - "plugin" => permissions.revoke_plugin(), - "hrtime" => permissions.revoke_hrtime(), - n => { - return Err(custom_error( - "ReferenceError", - format!("No such permission name: {}", n), - )) - } - }; - Ok(json!({ "state": perm.to_string() })) -} - -pub fn op_request_permission( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: PermissionArgs = serde_json::from_value(args)?; - let permissions = state.borrow_mut::(); - let path = args.path.as_deref(); - let perm = match args.name.as_ref() { - "read" => permissions.request_read(&path.as_deref().map(Path::new)), - "write" => permissions.request_write(&path.as_deref().map(Path::new)), - "net" => permissions.request_net(&args.url.as_deref())?, - "env" => permissions.request_env(), - "run" => permissions.request_run(), - "plugin" => permissions.request_plugin(), - "hrtime" => permissions.request_hrtime(), - n => { - return Err(custom_error( - "ReferenceError", - format!("No such permission name: {}", n), - )) - } - }; - Ok(json!({ "state": perm.to_string() })) -} diff --git a/cli/ops/plugin.rs b/cli/ops/plugin.rs deleted file mode 100644 index 1f3669b6f..000000000 --- a/cli/ops/plugin.rs +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::metrics::metrics_op; -use crate::permissions::Permissions; -use deno_core::error::AnyError; -use deno_core::futures::prelude::*; -use deno_core::plugin_api; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use deno_core::BufVec; -use deno_core::JsRuntime; -use deno_core::Op; -use deno_core::OpAsyncFuture; -use deno_core::OpId; -use deno_core::OpState; -use deno_core::ZeroCopyBuf; -use dlopen::symbor::Library; -use serde::Deserialize; -use std::cell::RefCell; -use std::path::PathBuf; -use std::pin::Pin; -use std::rc::Rc; -use std::task::Context; -use std::task::Poll; - -pub fn init(rt: &mut JsRuntime) { - super::reg_json_sync(rt, "op_open_plugin", op_open_plugin); -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct OpenPluginArgs { - filename: String, -} - -pub fn op_open_plugin( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: OpenPluginArgs = serde_json::from_value(args)?; - let filename = PathBuf::from(&args.filename); - - super::check_unstable(state, "Deno.openPlugin"); - let permissions = state.borrow::(); - permissions.check_plugin(&filename)?; - - debug!("Loading Plugin: {:#?}", filename); - let plugin_lib = Library::open(filename).map(Rc::new)?; - let plugin_resource = PluginResource::new(&plugin_lib); - - let rid; - let deno_plugin_init; - { - rid = state - .resource_table - .add("plugin", Box::new(plugin_resource)); - deno_plugin_init = *unsafe { - state - .resource_table - .get::(rid) - .unwrap() - .lib - .symbol::("deno_plugin_init") - .unwrap() - }; - } - - let mut interface = PluginInterface::new(state, &plugin_lib); - deno_plugin_init(&mut interface); - - Ok(json!(rid)) -} - -struct PluginResource { - lib: Rc, -} - -impl PluginResource { - fn new(lib: &Rc) -> Self { - Self { lib: lib.clone() } - } -} - -struct PluginInterface<'a> { - state: &'a mut OpState, - plugin_lib: &'a Rc, -} - -impl<'a> PluginInterface<'a> { - fn new(state: &'a mut OpState, plugin_lib: &'a Rc) -> Self { - Self { state, plugin_lib } - } -} - -impl<'a> plugin_api::Interface for PluginInterface<'a> { - /// Does the same as `core::Isolate::register_op()`, but additionally makes - /// the registered op dispatcher, as well as the op futures created by it, - /// keep reference to the plugin `Library` object, so that the plugin doesn't - /// get unloaded before all its op registrations and the futures created by - /// them are dropped. - fn register_op( - &mut self, - name: &str, - dispatch_op_fn: plugin_api::DispatchOpFn, - ) -> OpId { - let plugin_lib = self.plugin_lib.clone(); - let plugin_op_fn = move |state_rc: Rc>, - mut zero_copy: BufVec| { - let mut state = state_rc.borrow_mut(); - let mut interface = PluginInterface::new(&mut state, &plugin_lib); - let op = dispatch_op_fn(&mut interface, &mut zero_copy); - match op { - sync_op @ Op::Sync(..) => sync_op, - Op::Async(fut) => Op::Async(PluginOpAsyncFuture::new(&plugin_lib, fut)), - Op::AsyncUnref(fut) => { - Op::AsyncUnref(PluginOpAsyncFuture::new(&plugin_lib, fut)) - } - _ => unreachable!(), - } - }; - self - .state - .op_table - .register_op(name, metrics_op(Box::new(plugin_op_fn))) - } -} - -struct PluginOpAsyncFuture { - fut: Option, - _plugin_lib: Rc, -} - -impl PluginOpAsyncFuture { - fn new(plugin_lib: &Rc, fut: OpAsyncFuture) -> Pin> { - let wrapped_fut = Self { - fut: Some(fut), - _plugin_lib: plugin_lib.clone(), - }; - Box::pin(wrapped_fut) - } -} - -impl Future for PluginOpAsyncFuture { - type Output = ::Output; - fn poll(mut self: Pin<&mut Self>, ctx: &mut Context) -> Poll { - self.fut.as_mut().unwrap().poll_unpin(ctx) - } -} - -impl Drop for PluginOpAsyncFuture { - fn drop(&mut self) { - self.fut.take(); - } -} diff --git a/cli/ops/process.rs b/cli/ops/process.rs deleted file mode 100644 index 60a6d5095..000000000 --- a/cli/ops/process.rs +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use super::io::{std_file_resource, StreamResource, StreamResourceHolder}; -use crate::permissions::Permissions; -use crate::signal::kill; -use deno_core::error::bad_resource_id; -use deno_core::error::type_error; -use deno_core::error::AnyError; -use deno_core::futures::future::poll_fn; -use deno_core::futures::future::FutureExt; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use deno_core::BufVec; -use deno_core::OpState; -use deno_core::ZeroCopyBuf; -use serde::Deserialize; -use std::cell::RefCell; -use std::rc::Rc; -use tokio::process::Command; - -#[cfg(unix)] -use std::os::unix::process::ExitStatusExt; - -pub fn init(rt: &mut deno_core::JsRuntime) { - super::reg_json_sync(rt, "op_run", op_run); - super::reg_json_async(rt, "op_run_status", op_run_status); - super::reg_json_sync(rt, "op_kill", op_kill); -} - -fn clone_file( - state: &mut OpState, - rid: u32, -) -> Result { - std_file_resource(state, rid, move |r| match r { - Ok(std_file) => std_file.try_clone().map_err(AnyError::from), - Err(_) => Err(bad_resource_id()), - }) -} - -fn subprocess_stdio_map(s: &str) -> Result { - match s { - "inherit" => Ok(std::process::Stdio::inherit()), - "piped" => Ok(std::process::Stdio::piped()), - "null" => Ok(std::process::Stdio::null()), - _ => Err(type_error("Invalid resource for stdio")), - } -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct RunArgs { - cmd: Vec, - cwd: Option, - env: Vec<(String, String)>, - stdin: String, - stdout: String, - stderr: String, - stdin_rid: u32, - stdout_rid: u32, - stderr_rid: u32, -} - -struct ChildResource { - child: tokio::process::Child, -} - -fn op_run( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let run_args: RunArgs = serde_json::from_value(args)?; - state.borrow::().check_run()?; - - let args = run_args.cmd; - let env = run_args.env; - let cwd = run_args.cwd; - - let mut c = Command::new(args.get(0).unwrap()); - (1..args.len()).for_each(|i| { - let arg = args.get(i).unwrap(); - c.arg(arg); - }); - cwd.map(|d| c.current_dir(d)); - for (key, value) in &env { - c.env(key, value); - } - - // TODO: make this work with other resources, eg. sockets - if !run_args.stdin.is_empty() { - c.stdin(subprocess_stdio_map(run_args.stdin.as_ref())?); - } else { - let file = clone_file(state, run_args.stdin_rid)?; - c.stdin(file); - } - - if !run_args.stdout.is_empty() { - c.stdout(subprocess_stdio_map(run_args.stdout.as_ref())?); - } else { - let file = clone_file(state, run_args.stdout_rid)?; - c.stdout(file); - } - - if !run_args.stderr.is_empty() { - c.stderr(subprocess_stdio_map(run_args.stderr.as_ref())?); - } else { - let file = clone_file(state, run_args.stderr_rid)?; - c.stderr(file); - } - - // We want to kill child when it's closed - c.kill_on_drop(true); - - // Spawn the command. - let mut child = c.spawn()?; - let pid = child.id(); - - let stdin_rid = match child.stdin.take() { - Some(child_stdin) => { - let rid = state.resource_table.add( - "childStdin", - Box::new(StreamResourceHolder::new(StreamResource::ChildStdin( - child_stdin, - ))), - ); - Some(rid) - } - None => None, - }; - - let stdout_rid = match child.stdout.take() { - Some(child_stdout) => { - let rid = state.resource_table.add( - "childStdout", - Box::new(StreamResourceHolder::new(StreamResource::ChildStdout( - child_stdout, - ))), - ); - Some(rid) - } - None => None, - }; - - let stderr_rid = match child.stderr.take() { - Some(child_stderr) => { - let rid = state.resource_table.add( - "childStderr", - Box::new(StreamResourceHolder::new(StreamResource::ChildStderr( - child_stderr, - ))), - ); - Some(rid) - } - None => None, - }; - - let child_resource = ChildResource { child }; - let child_rid = state.resource_table.add("child", Box::new(child_resource)); - - Ok(json!({ - "rid": child_rid, - "pid": pid, - "stdinRid": stdin_rid, - "stdoutRid": stdout_rid, - "stderrRid": stderr_rid, - })) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct RunStatusArgs { - rid: i32, -} - -async fn op_run_status( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: RunStatusArgs = serde_json::from_value(args)?; - let rid = args.rid as u32; - - { - let s = state.borrow(); - s.borrow::().check_run()?; - } - - let run_status = poll_fn(|cx| { - let mut state = state.borrow_mut(); - let child_resource = state - .resource_table - .get_mut::(rid) - .ok_or_else(bad_resource_id)?; - let child = &mut child_resource.child; - child.poll_unpin(cx).map_err(AnyError::from) - }) - .await?; - - let code = run_status.code(); - - #[cfg(unix)] - let signal = run_status.signal(); - #[cfg(not(unix))] - let signal = None; - - code - .or(signal) - .expect("Should have either an exit code or a signal."); - let got_signal = signal.is_some(); - - Ok(json!({ - "gotSignal": got_signal, - "exitCode": code.unwrap_or(-1), - "exitSignal": signal.unwrap_or(-1), - })) -} - -#[derive(Deserialize)] -struct KillArgs { - pid: i32, - signo: i32, -} - -fn op_kill( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.kill"); - state.borrow::().check_run()?; - - let args: KillArgs = serde_json::from_value(args)?; - kill(args.pid, args.signo)?; - Ok(json!({})) -} diff --git a/cli/ops/runtime.rs b/cli/ops/runtime.rs deleted file mode 100644 index cb3b53d53..000000000 --- a/cli/ops/runtime.rs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::metrics::Metrics; -use crate::permissions::Permissions; -use deno_core::error::AnyError; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use deno_core::ModuleSpecifier; -use deno_core::OpState; -use deno_core::ZeroCopyBuf; - -pub fn init(rt: &mut deno_core::JsRuntime, main_module: ModuleSpecifier) { - { - let op_state = rt.op_state(); - let mut state = op_state.borrow_mut(); - state.put::(main_module); - } - super::reg_json_sync(rt, "op_main_module", op_main_module); - super::reg_json_sync(rt, "op_metrics", op_metrics); -} - -fn op_main_module( - state: &mut OpState, - _args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let main = state.borrow::().to_string(); - let main_url = ModuleSpecifier::resolve_url_or_path(&main)?; - if main_url.as_url().scheme() == "file" { - let main_path = std::env::current_dir().unwrap().join(main_url.to_string()); - state - .borrow::() - .check_read_blind(&main_path, "main_module")?; - } - Ok(json!(&main)) -} - -fn op_metrics( - state: &mut OpState, - _args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let m = state.borrow::(); - - Ok(json!({ - "opsDispatched": m.ops_dispatched, - "opsDispatchedSync": m.ops_dispatched_sync, - "opsDispatchedAsync": m.ops_dispatched_async, - "opsDispatchedAsyncUnref": m.ops_dispatched_async_unref, - "opsCompleted": m.ops_completed, - "opsCompletedSync": m.ops_completed_sync, - "opsCompletedAsync": m.ops_completed_async, - "opsCompletedAsyncUnref": m.ops_completed_async_unref, - "bytesSentControl": m.bytes_sent_control, - "bytesSentData": m.bytes_sent_data, - "bytesReceived": m.bytes_received - })) -} - -pub fn ppid() -> Value { - #[cfg(windows)] - { - // Adopted from rustup: - // https://github.com/rust-lang/rustup/blob/1.21.1/src/cli/self_update.rs#L1036 - // Copyright Diggory Blake, the Mozilla Corporation, and rustup contributors. - // Licensed under either of - // - Apache License, Version 2.0 - // - MIT license - use std::mem; - use winapi::shared::minwindef::DWORD; - use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE}; - use winapi::um::processthreadsapi::GetCurrentProcessId; - use winapi::um::tlhelp32::{ - CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32, - TH32CS_SNAPPROCESS, - }; - unsafe { - // Take a snapshot of system processes, one of which is ours - // and contains our parent's pid - let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if snapshot == INVALID_HANDLE_VALUE { - return serde_json::to_value(-1).unwrap(); - } - - let mut entry: PROCESSENTRY32 = mem::zeroed(); - entry.dwSize = mem::size_of::() as DWORD; - - // Iterate over system processes looking for ours - let success = Process32First(snapshot, &mut entry); - if success == 0 { - CloseHandle(snapshot); - return serde_json::to_value(-1).unwrap(); - } - - let this_pid = GetCurrentProcessId(); - while entry.th32ProcessID != this_pid { - let success = Process32Next(snapshot, &mut entry); - if success == 0 { - CloseHandle(snapshot); - return serde_json::to_value(-1).unwrap(); - } - } - CloseHandle(snapshot); - - // FIXME: Using the process ID exposes a race condition - // wherein the parent process already exited and the OS - // reassigned its ID. - let parent_id = entry.th32ParentProcessID; - serde_json::to_value(parent_id).unwrap() - } - } - #[cfg(not(windows))] - { - use std::os::unix::process::parent_id; - serde_json::to_value(parent_id()).unwrap() - } -} diff --git a/cli/ops/runtime_compiler.rs b/cli/ops/runtime_compiler.rs index 03ba88c76..ec9806e60 100644 --- a/cli/ops/runtime_compiler.rs +++ b/cli/ops/runtime_compiler.rs @@ -6,12 +6,12 @@ use crate::media_type::MediaType; use crate::module_graph::BundleType; use crate::module_graph::EmitOptions; use crate::module_graph::GraphBuilder; -use crate::permissions::Permissions; use crate::program_state::ProgramState; use crate::specifier_handler::FetchHandler; use crate::specifier_handler::MemoryHandler; use crate::specifier_handler::SpecifierHandler; use crate::tsc_config; +use deno_runtime::permissions::Permissions; use std::sync::Arc; use deno_core::error::AnyError; @@ -49,9 +49,9 @@ async fn op_compile( ) -> Result { let args: CompileArgs = serde_json::from_value(args)?; if args.bundle { - super::check_unstable2(&state, "Deno.bundle"); + deno_runtime::ops::check_unstable2(&state, "Deno.bundle"); } else { - super::check_unstable2(&state, "Deno.compile"); + deno_runtime::ops::check_unstable2(&state, "Deno.compile"); } let program_state = state.borrow().borrow::>().clone(); let runtime_permissions = { @@ -113,7 +113,7 @@ async fn op_transpile( args: Value, _data: BufVec, ) -> Result { - super::check_unstable2(&state, "Deno.transpileOnly"); + deno_runtime::ops::check_unstable2(&state, "Deno.transpileOnly"); let args: TranspileArgs = serde_json::from_value(args)?; let mut compiler_options = tsc_config::TsConfig::new(json!({ diff --git a/cli/ops/signal.rs b/cli/ops/signal.rs deleted file mode 100644 index be6bc0a35..000000000 --- a/cli/ops/signal.rs +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use deno_core::error::AnyError; -use deno_core::serde_json::Value; -use deno_core::BufVec; -use deno_core::OpState; -use deno_core::ZeroCopyBuf; -use std::cell::RefCell; -use std::rc::Rc; - -#[cfg(unix)] -use deno_core::error::bad_resource_id; -#[cfg(unix)] -use deno_core::futures::future::poll_fn; -#[cfg(unix)] -use deno_core::serde_json; -#[cfg(unix)] -use deno_core::serde_json::json; -#[cfg(unix)] -use serde::Deserialize; -#[cfg(unix)] -use std::task::Waker; -#[cfg(unix)] -use tokio::signal::unix::{signal, Signal, SignalKind}; - -pub fn init(rt: &mut deno_core::JsRuntime) { - super::reg_json_sync(rt, "op_signal_bind", op_signal_bind); - super::reg_json_sync(rt, "op_signal_unbind", op_signal_unbind); - super::reg_json_async(rt, "op_signal_poll", op_signal_poll); -} - -#[cfg(unix)] -/// The resource for signal stream. -/// The second element is the waker of polling future. -pub struct SignalStreamResource(pub Signal, pub Option); - -#[cfg(unix)] -#[derive(Deserialize)] -struct BindSignalArgs { - signo: i32, -} - -#[cfg(unix)] -#[derive(Deserialize)] -struct SignalArgs { - rid: i32, -} - -#[cfg(unix)] -fn op_signal_bind( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.signal"); - let args: BindSignalArgs = serde_json::from_value(args)?; - let rid = state.resource_table.add( - "signal", - Box::new(SignalStreamResource( - signal(SignalKind::from_raw(args.signo)).expect(""), - None, - )), - ); - Ok(json!({ - "rid": rid, - })) -} - -#[cfg(unix)] -async fn op_signal_poll( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - super::check_unstable2(&state, "Deno.signal"); - let args: SignalArgs = serde_json::from_value(args)?; - let rid = args.rid as u32; - - let future = poll_fn(move |cx| { - let mut state = state.borrow_mut(); - if let Some(mut signal) = - state.resource_table.get_mut::(rid) - { - signal.1 = Some(cx.waker().clone()); - return signal.0.poll_recv(cx); - } - std::task::Poll::Ready(None) - }); - let result = future.await; - Ok(json!({ "done": result.is_none() })) -} - -#[cfg(unix)] -pub fn op_signal_unbind( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.signal"); - let args: SignalArgs = serde_json::from_value(args)?; - let rid = args.rid as u32; - let resource = state.resource_table.get_mut::(rid); - if let Some(signal) = resource { - if let Some(waker) = &signal.1 { - // Wakes up the pending poll if exists. - // This prevents the poll future from getting stuck forever. - waker.clone().wake(); - } - } - state - .resource_table - .close(rid) - .ok_or_else(bad_resource_id)?; - Ok(json!({})) -} - -#[cfg(not(unix))] -pub fn op_signal_bind( - _state: &mut OpState, - _args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - unimplemented!(); -} - -#[cfg(not(unix))] -fn op_signal_unbind( - _state: &mut OpState, - _args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - unimplemented!(); -} - -#[cfg(not(unix))] -async fn op_signal_poll( - _state: Rc>, - _args: Value, - _zero_copy: BufVec, -) -> Result { - unimplemented!(); -} diff --git a/cli/ops/timers.rs b/cli/ops/timers.rs deleted file mode 100644 index 8037fd698..000000000 --- a/cli/ops/timers.rs +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -//! This module helps deno implement timers. -//! -//! As an optimization, we want to avoid an expensive calls into rust for every -//! setTimeout in JavaScript. Thus in //js/timers.ts a data structure is -//! implemented that calls into Rust for only the smallest timeout. Thus we -//! only need to be able to start, cancel and await a single timer (or Delay, as Tokio -//! calls it) for an entire Isolate. This is what is implemented here. - -use super::dispatch_minimal::minimal_op; -use super::dispatch_minimal::MinimalOp; -use crate::metrics::metrics_op; -use crate::permissions::Permissions; -use deno_core::error::type_error; -use deno_core::error::AnyError; -use deno_core::futures; -use deno_core::futures::channel::oneshot; -use deno_core::futures::FutureExt; -use deno_core::futures::TryFutureExt; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use deno_core::BufVec; -use deno_core::OpState; -use deno_core::ZeroCopyBuf; -use serde::Deserialize; -use std::cell::RefCell; -use std::future::Future; -use std::pin::Pin; -use std::rc::Rc; -use std::thread::sleep; -use std::time::Duration; -use std::time::Instant; - -pub type StartTime = Instant; - -type TimerFuture = Pin>>>; - -#[derive(Default)] -pub struct GlobalTimer { - tx: Option>, - pub future: Option, -} - -impl GlobalTimer { - pub fn cancel(&mut self) { - if let Some(tx) = self.tx.take() { - tx.send(()).ok(); - } - } - - pub fn new_timeout(&mut self, deadline: Instant) { - if self.tx.is_some() { - self.cancel(); - } - assert!(self.tx.is_none()); - self.future.take(); - - let (tx, rx) = oneshot::channel(); - self.tx = Some(tx); - - let delay = tokio::time::delay_until(deadline.into()); - let rx = rx - .map_err(|err| panic!("Unexpected error in receiving channel {:?}", err)); - - let fut = futures::future::select(delay, rx) - .then(|_| futures::future::ok(())) - .boxed_local(); - self.future = Some(fut); - } -} - -pub fn init(rt: &mut deno_core::JsRuntime) { - { - let op_state = rt.op_state(); - let mut state = op_state.borrow_mut(); - state.put::(GlobalTimer::default()); - state.put::(StartTime::now()); - } - super::reg_json_sync(rt, "op_global_timer_stop", op_global_timer_stop); - super::reg_json_sync(rt, "op_global_timer_start", op_global_timer_start); - super::reg_json_async(rt, "op_global_timer", op_global_timer); - rt.register_op("op_now", metrics_op(minimal_op(op_now))); - super::reg_json_sync(rt, "op_sleep_sync", op_sleep_sync); -} - -fn op_global_timer_stop( - state: &mut OpState, - _args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let global_timer = state.borrow_mut::(); - global_timer.cancel(); - Ok(json!({})) -} - -#[derive(Deserialize)] -struct GlobalTimerArgs { - timeout: u64, -} - -// Set up a timer that will be later awaited by JS promise. -// It's a separate op, because canceling a timeout immediately -// after setting it caused a race condition (because Tokio timeout) -// might have been registered after next event loop tick. -// -// See https://github.com/denoland/deno/issues/7599 for more -// details. -fn op_global_timer_start( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: GlobalTimerArgs = serde_json::from_value(args)?; - let val = args.timeout; - - let deadline = Instant::now() + Duration::from_millis(val); - let global_timer = state.borrow_mut::(); - global_timer.new_timeout(deadline); - Ok(json!({})) -} - -async fn op_global_timer( - state: Rc>, - _args: Value, - _zero_copy: BufVec, -) -> Result { - let maybe_timer_fut = { - let mut s = state.borrow_mut(); - let global_timer = s.borrow_mut::(); - global_timer.future.take() - }; - if let Some(timer_fut) = maybe_timer_fut { - let _ = timer_fut.await; - } - Ok(json!({})) -} - -// Returns a milliseconds and nanoseconds subsec -// since the start time of the deno runtime. -// If the High precision flag is not set, the -// nanoseconds are rounded on 2ms. -fn op_now( - state: Rc>, - // Arguments are discarded - _sync: bool, - _x: i32, - mut zero_copy: BufVec, -) -> MinimalOp { - match zero_copy.len() { - 0 => return MinimalOp::Sync(Err(type_error("no buffer specified"))), - 1 => {} - _ => { - return MinimalOp::Sync(Err(type_error("Invalid number of arguments"))) - } - } - - let op_state = state.borrow(); - let start_time = op_state.borrow::(); - let seconds = start_time.elapsed().as_secs(); - let mut subsec_nanos = start_time.elapsed().subsec_nanos() as f64; - let reduced_time_precision = 2_000_000.0; // 2ms in nanoseconds - - // If the permission is not enabled - // Round the nano result on 2 milliseconds - // see: https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#Reduced_time_precision - if op_state.borrow::().check_hrtime().is_err() { - subsec_nanos -= subsec_nanos % reduced_time_precision; - } - - let result = (seconds * 1_000) as f64 + (subsec_nanos / 1_000_000.0); - - (&mut zero_copy[0]).copy_from_slice(&result.to_be_bytes()); - - MinimalOp::Sync(Ok(0)) -} - -#[derive(Deserialize)] -struct SleepArgs { - millis: u64, -} - -fn op_sleep_sync( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.sleepSync"); - let args: SleepArgs = serde_json::from_value(args)?; - sleep(Duration::from_millis(args.millis)); - Ok(json!({})) -} diff --git a/cli/ops/tls.rs b/cli/ops/tls.rs deleted file mode 100644 index 37fd8f206..000000000 --- a/cli/ops/tls.rs +++ /dev/null @@ -1,431 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use super::io::{StreamResource, StreamResourceHolder}; -use crate::permissions::Permissions; -use crate::resolve_addr::resolve_addr; -use deno_core::error::bad_resource; -use deno_core::error::bad_resource_id; -use deno_core::error::custom_error; -use deno_core::error::AnyError; -use deno_core::futures; -use deno_core::futures::future::poll_fn; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use deno_core::BufVec; -use deno_core::OpState; -use deno_core::ZeroCopyBuf; -use serde::Deserialize; -use std::cell::RefCell; -use std::convert::From; -use std::fs::File; -use std::io::BufReader; -use std::net::SocketAddr; -use std::path::Path; -use std::rc::Rc; -use std::sync::Arc; -use std::task::Context; -use std::task::Poll; -use tokio::net::TcpListener; -use tokio::net::TcpStream; -use tokio_rustls::{rustls::ClientConfig, TlsConnector}; -use tokio_rustls::{ - rustls::{ - internal::pemfile::{certs, pkcs8_private_keys, rsa_private_keys}, - Certificate, NoClientAuth, PrivateKey, ServerConfig, - }, - TlsAcceptor, -}; -use webpki::DNSNameRef; - -pub fn init(rt: &mut deno_core::JsRuntime) { - super::reg_json_async(rt, "op_start_tls", op_start_tls); - super::reg_json_async(rt, "op_connect_tls", op_connect_tls); - super::reg_json_sync(rt, "op_listen_tls", op_listen_tls); - super::reg_json_async(rt, "op_accept_tls", op_accept_tls); -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct ConnectTLSArgs { - transport: String, - hostname: String, - port: u16, - cert_file: Option, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct StartTLSArgs { - rid: u32, - cert_file: Option, - hostname: String, -} - -async fn op_start_tls( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: StartTLSArgs = serde_json::from_value(args)?; - let rid = args.rid as u32; - let cert_file = args.cert_file.clone(); - - let mut domain = args.hostname; - if domain.is_empty() { - domain.push_str("localhost"); - } - { - super::check_unstable2(&state, "Deno.startTls"); - let s = state.borrow(); - let permissions = s.borrow::(); - permissions.check_net(&domain, 0)?; - if let Some(path) = cert_file.clone() { - permissions.check_read(Path::new(&path))?; - } - } - let mut resource_holder = { - let mut state_ = state.borrow_mut(); - match state_.resource_table.remove::(rid) { - Some(resource) => *resource, - None => return Err(bad_resource_id()), - } - }; - - if let StreamResource::TcpStream(ref mut tcp_stream) = - resource_holder.resource - { - let tcp_stream = tcp_stream.take().unwrap(); - let local_addr = tcp_stream.local_addr()?; - let remote_addr = tcp_stream.peer_addr()?; - let mut config = ClientConfig::new(); - config - .root_store - .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); - if let Some(path) = cert_file { - let key_file = File::open(path)?; - let reader = &mut BufReader::new(key_file); - config.root_store.add_pem_file(reader).unwrap(); - } - - let tls_connector = TlsConnector::from(Arc::new(config)); - let dnsname = - DNSNameRef::try_from_ascii_str(&domain).expect("Invalid DNS lookup"); - let tls_stream = tls_connector.connect(dnsname, tcp_stream).await?; - - let rid = { - let mut state_ = state.borrow_mut(); - state_.resource_table.add( - "clientTlsStream", - Box::new(StreamResourceHolder::new(StreamResource::ClientTlsStream( - Box::new(tls_stream), - ))), - ) - }; - Ok(json!({ - "rid": rid, - "localAddr": { - "hostname": local_addr.ip().to_string(), - "port": local_addr.port(), - "transport": "tcp", - }, - "remoteAddr": { - "hostname": remote_addr.ip().to_string(), - "port": remote_addr.port(), - "transport": "tcp", - } - })) - } else { - Err(bad_resource_id()) - } -} - -async fn op_connect_tls( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: ConnectTLSArgs = serde_json::from_value(args)?; - let cert_file = args.cert_file.clone(); - { - let s = state.borrow(); - let permissions = s.borrow::(); - permissions.check_net(&args.hostname, args.port)?; - if let Some(path) = cert_file.clone() { - permissions.check_read(Path::new(&path))?; - } - } - let mut domain = args.hostname.clone(); - if domain.is_empty() { - domain.push_str("localhost"); - } - - let addr = resolve_addr(&args.hostname, args.port)?; - let tcp_stream = TcpStream::connect(&addr).await?; - let local_addr = tcp_stream.local_addr()?; - let remote_addr = tcp_stream.peer_addr()?; - let mut config = ClientConfig::new(); - config - .root_store - .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); - if let Some(path) = cert_file { - let key_file = File::open(path)?; - let reader = &mut BufReader::new(key_file); - config.root_store.add_pem_file(reader).unwrap(); - } - let tls_connector = TlsConnector::from(Arc::new(config)); - let dnsname = - DNSNameRef::try_from_ascii_str(&domain).expect("Invalid DNS lookup"); - let tls_stream = tls_connector.connect(dnsname, tcp_stream).await?; - let rid = { - let mut state_ = state.borrow_mut(); - state_.resource_table.add( - "clientTlsStream", - Box::new(StreamResourceHolder::new(StreamResource::ClientTlsStream( - Box::new(tls_stream), - ))), - ) - }; - Ok(json!({ - "rid": rid, - "localAddr": { - "hostname": local_addr.ip().to_string(), - "port": local_addr.port(), - "transport": args.transport, - }, - "remoteAddr": { - "hostname": remote_addr.ip().to_string(), - "port": remote_addr.port(), - "transport": args.transport, - } - })) -} - -fn load_certs(path: &str) -> Result, AnyError> { - let cert_file = File::open(path)?; - let reader = &mut BufReader::new(cert_file); - - let certs = certs(reader) - .map_err(|_| custom_error("InvalidData", "Unable to decode certificate"))?; - - if certs.is_empty() { - let e = custom_error("InvalidData", "No certificates found in cert file"); - return Err(e); - } - - Ok(certs) -} - -fn key_decode_err() -> AnyError { - custom_error("InvalidData", "Unable to decode key") -} - -fn key_not_found_err() -> AnyError { - custom_error("InvalidData", "No keys found in key file") -} - -/// Starts with -----BEGIN RSA PRIVATE KEY----- -fn load_rsa_keys(path: &str) -> Result, AnyError> { - let key_file = File::open(path)?; - let reader = &mut BufReader::new(key_file); - let keys = rsa_private_keys(reader).map_err(|_| key_decode_err())?; - Ok(keys) -} - -/// Starts with -----BEGIN PRIVATE KEY----- -fn load_pkcs8_keys(path: &str) -> Result, AnyError> { - let key_file = File::open(path)?; - let reader = &mut BufReader::new(key_file); - let keys = pkcs8_private_keys(reader).map_err(|_| key_decode_err())?; - Ok(keys) -} - -fn load_keys(path: &str) -> Result, AnyError> { - let path = path.to_string(); - let mut keys = load_rsa_keys(&path)?; - - if keys.is_empty() { - keys = load_pkcs8_keys(&path)?; - } - - if keys.is_empty() { - return Err(key_not_found_err()); - } - - Ok(keys) -} - -#[allow(dead_code)] -pub struct TlsListenerResource { - listener: TcpListener, - tls_acceptor: TlsAcceptor, - waker: Option, - local_addr: SocketAddr, -} - -impl Drop for TlsListenerResource { - fn drop(&mut self) { - self.wake_task(); - } -} - -impl TlsListenerResource { - /// Track the current task so future awaiting for connection - /// can be notified when listener is closed. - /// - /// Throws an error if another task is already tracked. - pub fn track_task(&mut self, cx: &Context) -> Result<(), AnyError> { - // Currently, we only allow tracking a single accept task for a listener. - // This might be changed in the future with multiple workers. - // Caveat: TcpListener by itself also only tracks an accept task at a time. - // See https://github.com/tokio-rs/tokio/issues/846#issuecomment-454208883 - if self.waker.is_some() { - return Err(custom_error("Busy", "Another accept task is ongoing")); - } - - let waker = futures::task::AtomicWaker::new(); - waker.register(cx.waker()); - self.waker.replace(waker); - Ok(()) - } - - /// Notifies a task when listener is closed so accept future can resolve. - pub fn wake_task(&mut self) { - if let Some(waker) = self.waker.as_ref() { - waker.wake(); - } - } - - /// Stop tracking a task. - /// Happens when the task is done and thus no further tracking is needed. - pub fn untrack_task(&mut self) { - self.waker.take(); - } -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct ListenTlsArgs { - transport: String, - hostname: String, - port: u16, - cert_file: String, - key_file: String, -} - -fn op_listen_tls( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: ListenTlsArgs = serde_json::from_value(args)?; - assert_eq!(args.transport, "tcp"); - - let cert_file = args.cert_file; - let key_file = args.key_file; - { - let permissions = state.borrow::(); - permissions.check_net(&args.hostname, args.port)?; - permissions.check_read(Path::new(&cert_file))?; - permissions.check_read(Path::new(&key_file))?; - } - let mut config = ServerConfig::new(NoClientAuth::new()); - config - .set_single_cert(load_certs(&cert_file)?, load_keys(&key_file)?.remove(0)) - .expect("invalid key or certificate"); - let tls_acceptor = TlsAcceptor::from(Arc::new(config)); - let addr = resolve_addr(&args.hostname, args.port)?; - let std_listener = std::net::TcpListener::bind(&addr)?; - let listener = TcpListener::from_std(std_listener)?; - let local_addr = listener.local_addr()?; - let tls_listener_resource = TlsListenerResource { - listener, - tls_acceptor, - waker: None, - local_addr, - }; - - let rid = state - .resource_table - .add("tlsListener", Box::new(tls_listener_resource)); - - Ok(json!({ - "rid": rid, - "localAddr": { - "hostname": local_addr.ip().to_string(), - "port": local_addr.port(), - "transport": args.transport, - }, - })) -} - -#[derive(Deserialize)] -struct AcceptTlsArgs { - rid: i32, -} - -async fn op_accept_tls( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: AcceptTlsArgs = serde_json::from_value(args)?; - let rid = args.rid as u32; - let accept_fut = poll_fn(|cx| { - let mut state = state.borrow_mut(); - let listener_resource = state - .resource_table - .get_mut::(rid) - .ok_or_else(|| bad_resource("Listener has been closed"))?; - let listener = &mut listener_resource.listener; - match listener.poll_accept(cx).map_err(AnyError::from) { - Poll::Ready(Ok((stream, addr))) => { - listener_resource.untrack_task(); - Poll::Ready(Ok((stream, addr))) - } - Poll::Pending => { - listener_resource.track_task(cx)?; - Poll::Pending - } - Poll::Ready(Err(e)) => { - listener_resource.untrack_task(); - Poll::Ready(Err(e)) - } - } - }); - let (tcp_stream, _socket_addr) = accept_fut.await?; - let local_addr = tcp_stream.local_addr()?; - let remote_addr = tcp_stream.peer_addr()?; - let tls_acceptor = { - let state_ = state.borrow(); - let resource = state_ - .resource_table - .get::(rid) - .ok_or_else(bad_resource_id) - .expect("Can't find tls listener"); - resource.tls_acceptor.clone() - }; - let tls_stream = tls_acceptor.accept(tcp_stream).await?; - let rid = { - let mut state_ = state.borrow_mut(); - state_.resource_table.add( - "serverTlsStream", - Box::new(StreamResourceHolder::new(StreamResource::ServerTlsStream( - Box::new(tls_stream), - ))), - ) - }; - Ok(json!({ - "rid": rid, - "localAddr": { - "transport": "tcp", - "hostname": local_addr.ip().to_string(), - "port": local_addr.port() - }, - "remoteAddr": { - "transport": "tcp", - "hostname": remote_addr.ip().to_string(), - "port": remote_addr.port() - } - })) -} diff --git a/cli/ops/tty.rs b/cli/ops/tty.rs deleted file mode 100644 index be1d7d3e4..000000000 --- a/cli/ops/tty.rs +++ /dev/null @@ -1,334 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use super::io::std_file_resource; -use super::io::StreamResource; -use super::io::StreamResourceHolder; -use deno_core::error::bad_resource_id; -use deno_core::error::last_os_error; -use deno_core::error::not_supported; -use deno_core::error::resource_unavailable; -use deno_core::error::AnyError; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use deno_core::OpState; -use deno_core::ZeroCopyBuf; -use serde::Deserialize; -use serde::Serialize; - -#[cfg(unix)] -use nix::sys::termios; - -#[cfg(windows)] -use deno_core::error::custom_error; -#[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(last_os_error()); - } else if handle.is_null() { - return Err(custom_error("ReferenceError", "null handle")); - } - Ok(handle) -} - -pub fn init(rt: &mut deno_core::JsRuntime) { - super::reg_json_sync(rt, "op_set_raw", op_set_raw); - super::reg_json_sync(rt, "op_isatty", op_isatty); - super::reg_json_sync(rt, "op_console_size", op_console_size); -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct SetRawOptions { - cbreak: bool, -} - -#[derive(Deserialize)] -struct SetRawArgs { - rid: u32, - mode: bool, - options: SetRawOptions, -} - -fn op_set_raw( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.setRaw"); - - let args: SetRawArgs = serde_json::from_value(args)?; - let rid = args.rid; - let is_raw = args.mode; - let cbreak = args.options.cbreak; - - // 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::shared::minwindef::FALSE; - use winapi::um::{consoleapi, handleapi}; - - let resource_holder = - state.resource_table.get_mut::(rid); - if resource_holder.is_none() { - return Err(bad_resource_id()); - } - if cbreak { - return Err(not_supported()); - } - let resource_holder = resource_holder.unwrap(); - - // For now, only stdin. - let handle = match &mut resource_holder.resource { - StreamResource::FsFile(ref mut option_file_metadata) => { - if let Some((tokio_file, metadata)) = option_file_metadata.take() { - match tokio_file.try_into_std() { - Ok(std_file) => { - let raw_handle = std_file.as_raw_handle(); - // Turn the std_file handle back into a tokio file, put it back - // in the resource table. - let tokio_file = tokio::fs::File::from_std(std_file); - resource_holder.resource = - StreamResource::FsFile(Some((tokio_file, metadata))); - // return the result. - raw_handle - } - Err(tokio_file) => { - // This function will return an error containing the file if - // some operation is in-flight. - resource_holder.resource = - StreamResource::FsFile(Some((tokio_file, metadata))); - return Err(resource_unavailable()); - } - } - } else { - return Err(resource_unavailable()); - } - } - _ => { - return Err(bad_resource_id()); - } - }; - - if handle == handleapi::INVALID_HANDLE_VALUE { - return Err(last_os_error()); - } else if handle.is_null() { - return Err(custom_error("ReferenceError", "null handle")); - } - let mut original_mode: DWORD = 0; - if unsafe { consoleapi::GetConsoleMode(handle, &mut original_mode) } - == FALSE - { - return Err(last_os_error()); - } - let new_mode = if is_raw { - original_mode & !RAW_MODE_MASK - } else { - original_mode | RAW_MODE_MASK - }; - if unsafe { consoleapi::SetConsoleMode(handle, new_mode) } == FALSE { - return Err(last_os_error()); - } - - Ok(json!({})) - } - #[cfg(unix)] - { - use std::os::unix::io::AsRawFd; - - let resource_holder = - state.resource_table.get_mut::(rid); - if resource_holder.is_none() { - return Err(bad_resource_id()); - } - - if is_raw { - let (raw_fd, maybe_tty_mode) = - match &mut resource_holder.unwrap().resource { - StreamResource::FsFile(Some((f, ref mut metadata))) => { - (f.as_raw_fd(), &mut metadata.tty.mode) - } - StreamResource::FsFile(None) => return Err(resource_unavailable()), - _ => { - return Err(not_supported()); - } - }; - - if maybe_tty_mode.is_none() { - // Save original mode. - let original_mode = termios::tcgetattr(raw_fd)?; - maybe_tty_mode.replace(original_mode); - } - - let mut raw = maybe_tty_mode.clone().unwrap(); - - 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); - if !cbreak { - raw.local_flags &= !(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(json!({})) - } else { - // Try restore saved mode. - let (raw_fd, maybe_tty_mode) = - match &mut resource_holder.unwrap().resource { - StreamResource::FsFile(Some((f, ref mut metadata))) => { - (f.as_raw_fd(), &mut metadata.tty.mode) - } - StreamResource::FsFile(None) => { - return Err(resource_unavailable()); - } - _ => { - return Err(bad_resource_id()); - } - }; - - if let Some(mode) = maybe_tty_mode.take() { - termios::tcsetattr(raw_fd, termios::SetArg::TCSADRAIN, &mode)?; - } - - Ok(json!({})) - } - } -} - -#[derive(Deserialize)] -struct IsattyArgs { - rid: u32, -} - -fn op_isatty( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: IsattyArgs = serde_json::from_value(args)?; - let rid = args.rid; - - let isatty: bool = std_file_resource(state, rid as u32, move |r| match r { - Ok(std_file) => { - #[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. - Ok(unsafe { consoleapi::GetConsoleMode(handle, &mut test_mode) != 0 }) - } - #[cfg(unix)] - { - use std::os::unix::io::AsRawFd; - let raw_fd = std_file.as_raw_fd(); - Ok(unsafe { libc::isatty(raw_fd as libc::c_int) == 1 }) - } - } - Err(StreamResource::FsFile(_)) => unreachable!(), - _ => Ok(false), - })?; - Ok(json!(isatty)) -} - -#[derive(Deserialize)] -struct ConsoleSizeArgs { - rid: u32, -} - -#[derive(Serialize)] -struct ConsoleSize { - columns: u32, - rows: u32, -} - -fn op_console_size( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - super::check_unstable(state, "Deno.consoleSize"); - - let args: ConsoleSizeArgs = serde_json::from_value(args)?; - let rid = args.rid; - - let size = std_file_resource(state, rid as u32, move |r| match r { - Ok(std_file) => { - #[cfg(windows)] - { - use std::os::windows::io::AsRawHandle; - let handle = std_file.as_raw_handle(); - - unsafe { - let mut bufinfo: winapi::um::wincon::CONSOLE_SCREEN_BUFFER_INFO = - std::mem::zeroed(); - - if winapi::um::wincon::GetConsoleScreenBufferInfo( - handle, - &mut bufinfo, - ) == 0 - { - return Err(last_os_error()); - } - - Ok(ConsoleSize { - columns: bufinfo.dwSize.X as u32, - rows: bufinfo.dwSize.Y as u32, - }) - } - } - - #[cfg(unix)] - { - use std::os::unix::io::AsRawFd; - - let fd = std_file.as_raw_fd(); - unsafe { - let mut size: libc::winsize = std::mem::zeroed(); - if libc::ioctl(fd, libc::TIOCGWINSZ, &mut size as *mut _) != 0 { - return Err(last_os_error()); - } - - // TODO (caspervonb) return a tuple instead - Ok(ConsoleSize { - columns: size.ws_col as u32, - rows: size.ws_row as u32, - }) - } - } - } - Err(_) => Err(bad_resource_id()), - })?; - - Ok(json!(size)) -} diff --git a/cli/ops/web_worker.rs b/cli/ops/web_worker.rs deleted file mode 100644 index d88330a04..000000000 --- a/cli/ops/web_worker.rs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::web_worker::WebWorkerHandle; -use crate::web_worker::WorkerEvent; -use deno_core::futures::channel::mpsc; -use deno_core::serde_json::json; - -pub fn init( - rt: &mut deno_core::JsRuntime, - sender: mpsc::Sender, - handle: WebWorkerHandle, -) { - // Post message to host as guest worker. - let sender_ = sender.clone(); - super::reg_json_sync( - rt, - "op_worker_post_message", - move |_state, _args, bufs| { - assert_eq!(bufs.len(), 1, "Invalid number of arguments"); - let msg_buf: Box<[u8]> = (*bufs[0]).into(); - sender_ - .clone() - .try_send(WorkerEvent::Message(msg_buf)) - .expect("Failed to post message to host"); - Ok(json!({})) - }, - ); - - // Notify host that guest worker closes. - super::reg_json_sync(rt, "op_worker_close", move |_state, _args, _bufs| { - // Notify parent that we're finished - sender.clone().close_channel(); - // Terminate execution of current worker - handle.terminate(); - Ok(json!({})) - }); -} diff --git a/cli/ops/websocket.rs b/cli/ops/websocket.rs deleted file mode 100644 index a8c591a33..000000000 --- a/cli/ops/websocket.rs +++ /dev/null @@ -1,326 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::permissions::Permissions; -use core::task::Poll; -use deno_core::error::bad_resource_id; -use deno_core::error::type_error; -use deno_core::error::AnyError; -use deno_core::futures::future::poll_fn; -use deno_core::futures::StreamExt; -use deno_core::futures::{ready, SinkExt}; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use deno_core::url; -use deno_core::BufVec; -use deno_core::OpState; -use deno_core::{serde_json, ZeroCopyBuf}; -use http::{Method, Request, Uri}; -use serde::Deserialize; -use std::borrow::Cow; -use std::cell::RefCell; -use std::fs::File; -use std::io::BufReader; -use std::rc::Rc; -use std::sync::Arc; -use tokio::net::TcpStream; -use tokio_rustls::{rustls::ClientConfig, TlsConnector}; -use tokio_tungstenite::stream::Stream as StreamSwitcher; -use tokio_tungstenite::tungstenite::Error as TungsteniteError; -use tokio_tungstenite::tungstenite::{ - handshake::client::Response, protocol::frame::coding::CloseCode, - protocol::CloseFrame, Message, -}; -use tokio_tungstenite::{client_async, WebSocketStream}; -use webpki::DNSNameRef; - -#[derive(Clone)] -struct WsCaFile(String); -#[derive(Clone)] -struct WsUserAgent(String); - -pub fn init( - rt: &mut deno_core::JsRuntime, - maybe_ca_file: Option<&str>, - user_agent: String, -) { - { - let op_state = rt.op_state(); - let mut state = op_state.borrow_mut(); - if let Some(ca_file) = maybe_ca_file { - state.put::(WsCaFile(ca_file.to_string())); - } - state.put::(WsUserAgent(user_agent)); - } - super::reg_json_sync(rt, "op_ws_check_permission", op_ws_check_permission); - super::reg_json_async(rt, "op_ws_create", op_ws_create); - super::reg_json_async(rt, "op_ws_send", op_ws_send); - super::reg_json_async(rt, "op_ws_close", op_ws_close); - super::reg_json_async(rt, "op_ws_next_event", op_ws_next_event); -} - -type MaybeTlsStream = - StreamSwitcher>; - -type WsStream = WebSocketStream; - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct CheckPermissionArgs { - url: String, -} - -// This op is needed because creating a WS instance in JavaScript is a sync -// operation and should throw error when permissions are not fullfiled, -// but actual op that connects WS is async. -pub fn op_ws_check_permission( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: CheckPermissionArgs = serde_json::from_value(args)?; - - state - .borrow::() - .check_net_url(&url::Url::parse(&args.url)?)?; - - Ok(json!({})) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct CreateArgs { - url: String, - protocols: String, -} - -pub async fn op_ws_create( - state: Rc>, - args: Value, - _bufs: BufVec, -) -> Result { - let args: CreateArgs = serde_json::from_value(args)?; - - { - let s = state.borrow(); - s.borrow::() - .check_net_url(&url::Url::parse(&args.url)?) - .expect( - "Permission check should have been done in op_ws_check_permission", - ); - } - - let maybe_ca_file = state.borrow().try_borrow::().cloned(); - let user_agent = state.borrow().borrow::().0.clone(); - let uri: Uri = args.url.parse()?; - let mut request = Request::builder().method(Method::GET).uri(&uri); - - request = request.header("User-Agent", user_agent); - - if !args.protocols.is_empty() { - request = request.header("Sec-WebSocket-Protocol", args.protocols); - } - - let request = request.body(())?; - let domain = &uri.host().unwrap().to_string(); - let port = &uri.port_u16().unwrap_or(match uri.scheme_str() { - Some("wss") => 443, - Some("ws") => 80, - _ => unreachable!(), - }); - let addr = format!("{}:{}", domain, port); - let try_socket = TcpStream::connect(addr).await; - let tcp_socket = match try_socket.map_err(TungsteniteError::Io) { - Ok(socket) => socket, - Err(_) => return Ok(json!({"success": false})), - }; - - let socket: MaybeTlsStream = match uri.scheme_str() { - Some("ws") => StreamSwitcher::Plain(tcp_socket), - Some("wss") => { - let mut config = ClientConfig::new(); - config - .root_store - .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); - - if let Some(ws_ca_file) = maybe_ca_file { - let key_file = File::open(ws_ca_file.0)?; - let reader = &mut BufReader::new(key_file); - config.root_store.add_pem_file(reader).unwrap(); - } - - let tls_connector = TlsConnector::from(Arc::new(config)); - let dnsname = - DNSNameRef::try_from_ascii_str(&domain).expect("Invalid DNS lookup"); - let tls_socket = tls_connector.connect(dnsname, tcp_socket).await?; - StreamSwitcher::Tls(tls_socket) - } - _ => unreachable!(), - }; - - let (stream, response): (WsStream, Response) = - client_async(request, socket).await.map_err(|err| { - type_error(format!( - "failed to connect to WebSocket: {}", - err.to_string() - )) - })?; - - let mut state = state.borrow_mut(); - let rid = state - .resource_table - .add("webSocketStream", Box::new(stream)); - - let protocol = match response.headers().get("Sec-WebSocket-Protocol") { - Some(header) => header.to_str().unwrap(), - None => "", - }; - let extensions = response - .headers() - .get_all("Sec-WebSocket-Extensions") - .iter() - .map(|header| header.to_str().unwrap()) - .collect::(); - Ok(json!({ - "success": true, - "rid": rid, - "protocol": protocol, - "extensions": extensions - })) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct SendArgs { - rid: u32, - text: Option, -} - -pub async fn op_ws_send( - state: Rc>, - args: Value, - bufs: BufVec, -) -> Result { - let args: SendArgs = serde_json::from_value(args)?; - - let mut maybe_msg = Some(match args.text { - Some(text) => Message::Text(text), - None => Message::Binary(bufs[0].to_vec()), - }); - let rid = args.rid; - - poll_fn(move |cx| { - let mut state = state.borrow_mut(); - let stream = state - .resource_table - .get_mut::(rid) - .ok_or_else(bad_resource_id)?; - - // TODO(ry) Handle errors below instead of unwrap. - // Need to map `TungsteniteError` to `AnyError`. - ready!(stream.poll_ready_unpin(cx)).unwrap(); - if let Some(msg) = maybe_msg.take() { - stream.start_send_unpin(msg).unwrap(); - } - ready!(stream.poll_flush_unpin(cx)).unwrap(); - - Poll::Ready(Ok(json!({}))) - }) - .await -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct CloseArgs { - rid: u32, - code: Option, - reason: Option, -} - -pub async fn op_ws_close( - state: Rc>, - args: Value, - _bufs: BufVec, -) -> Result { - let args: CloseArgs = serde_json::from_value(args)?; - let rid = args.rid; - let mut maybe_msg = Some(Message::Close(args.code.map(|c| CloseFrame { - code: CloseCode::from(c), - reason: match args.reason { - Some(reason) => Cow::from(reason), - None => Default::default(), - }, - }))); - - poll_fn(move |cx| { - let mut state = state.borrow_mut(); - let stream = state - .resource_table - .get_mut::(rid) - .ok_or_else(bad_resource_id)?; - - // TODO(ry) Handle errors below instead of unwrap. - // Need to map `TungsteniteError` to `AnyError`. - ready!(stream.poll_ready_unpin(cx)).unwrap(); - if let Some(msg) = maybe_msg.take() { - stream.start_send_unpin(msg).unwrap(); - } - ready!(stream.poll_flush_unpin(cx)).unwrap(); - ready!(stream.poll_close_unpin(cx)).unwrap(); - - Poll::Ready(Ok(json!({}))) - }) - .await -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct NextEventArgs { - rid: u32, -} - -pub async fn op_ws_next_event( - state: Rc>, - args: Value, - _bufs: BufVec, -) -> Result { - let args: NextEventArgs = serde_json::from_value(args)?; - poll_fn(move |cx| { - let mut state = state.borrow_mut(); - let stream = state - .resource_table - .get_mut::(args.rid) - .ok_or_else(bad_resource_id)?; - stream - .poll_next_unpin(cx) - .map(|val| { - match val { - Some(Ok(Message::Text(text))) => json!({ - "type": "string", - "data": text - }), - Some(Ok(Message::Binary(data))) => { - // TODO(ry): don't use json to send binary data. - json!({ - "type": "binary", - "data": data - }) - } - Some(Ok(Message::Close(Some(frame)))) => json!({ - "type": "close", - "code": u16::from(frame.code), - "reason": frame.reason.as_ref() - }), - Some(Ok(Message::Close(None))) => json!({ "type": "close" }), - Some(Ok(Message::Ping(_))) => json!({"type": "ping"}), - Some(Ok(Message::Pong(_))) => json!({"type": "pong"}), - Some(Err(_)) => json!({"type": "error"}), - None => { - state.resource_table.close(args.rid).unwrap(); - json!({"type": "closed"}) - } - } - }) - .map(Ok) - }) - .await -} diff --git a/cli/ops/worker_host.rs b/cli/ops/worker_host.rs deleted file mode 100644 index 871e4b9fe..000000000 --- a/cli/ops/worker_host.rs +++ /dev/null @@ -1,318 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::permissions::Permissions; -use crate::web_worker::run_web_worker; -use crate::web_worker::WebWorker; -use crate::web_worker::WebWorkerHandle; -use crate::web_worker::WorkerEvent; -use deno_core::error::generic_error; -use deno_core::error::AnyError; -use deno_core::error::JsError; -use deno_core::futures::channel::mpsc; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use deno_core::BufVec; -use deno_core::ModuleSpecifier; -use deno_core::OpState; -use deno_core::ZeroCopyBuf; -use serde::Deserialize; -use std::cell::RefCell; -use std::collections::HashMap; -use std::convert::From; -use std::rc::Rc; -use std::sync::Arc; -use std::thread::JoinHandle; - -pub struct CreateWebWorkerArgs { - pub name: String, - pub worker_id: u32, - pub permissions: Permissions, - pub main_module: ModuleSpecifier, - pub use_deno_namespace: bool, -} - -pub type CreateWebWorkerCb = - dyn Fn(CreateWebWorkerArgs) -> WebWorker + Sync + Send; - -/// A holder for callback that is used to create a new -/// WebWorker. It's a struct instead of a type alias -/// because `GothamState` used in `OpState` overrides -/// value if type alises have the same underlying type -#[derive(Clone)] -pub struct CreateWebWorkerCbHolder(Arc); - -#[derive(Deserialize)] -struct HostUnhandledErrorArgs { - message: String, -} - -pub fn init( - rt: &mut deno_core::JsRuntime, - sender: Option>, - create_web_worker_cb: Arc, -) { - { - let op_state = rt.op_state(); - let mut state = op_state.borrow_mut(); - state.put::(WorkersTable::default()); - state.put::(WorkerId::default()); - - let create_module_loader = CreateWebWorkerCbHolder(create_web_worker_cb); - state.put::(create_module_loader); - } - super::reg_json_sync(rt, "op_create_worker", op_create_worker); - super::reg_json_sync( - rt, - "op_host_terminate_worker", - op_host_terminate_worker, - ); - super::reg_json_sync(rt, "op_host_post_message", op_host_post_message); - super::reg_json_async(rt, "op_host_get_message", op_host_get_message); - super::reg_json_sync( - rt, - "op_host_unhandled_error", - move |_state, args, _zero_copy| { - if let Some(mut sender) = sender.clone() { - let args: HostUnhandledErrorArgs = serde_json::from_value(args)?; - sender - .try_send(WorkerEvent::Error(generic_error(args.message))) - .expect("Failed to propagate error event to parent worker"); - Ok(json!(true)) - } else { - Err(generic_error("Cannot be called from main worker.")) - } - }, - ); -} - -pub struct WorkerThread { - join_handle: JoinHandle>, - worker_handle: WebWorkerHandle, -} - -pub type WorkersTable = HashMap; -pub type WorkerId = u32; - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct CreateWorkerArgs { - name: Option, - specifier: String, - has_source_code: bool, - source_code: String, - use_deno_namespace: bool, -} - -/// Create worker as the host -fn op_create_worker( - state: &mut OpState, - args: Value, - _data: &mut [ZeroCopyBuf], -) -> Result { - let args: CreateWorkerArgs = serde_json::from_value(args)?; - - let specifier = args.specifier.clone(); - let maybe_source_code = if args.has_source_code { - Some(args.source_code.clone()) - } else { - None - }; - let args_name = args.name; - let use_deno_namespace = args.use_deno_namespace; - if use_deno_namespace { - super::check_unstable(state, "Worker.deno"); - } - let permissions = state.borrow::().clone(); - let worker_id = state.take::(); - let create_module_loader = state.take::(); - state.put::(create_module_loader.clone()); - state.put::(worker_id + 1); - - let module_specifier = ModuleSpecifier::resolve_url(&specifier)?; - let worker_name = args_name.unwrap_or_else(|| "".to_string()); - - let (handle_sender, handle_receiver) = - std::sync::mpsc::sync_channel::>(1); - - // Setup new thread - let thread_builder = - std::thread::Builder::new().name(format!("deno-worker-{}", worker_id)); - - // Spawn it - let join_handle = thread_builder.spawn(move || { - // Any error inside this block is terminal: - // - JS worker is useless - meaning it throws an exception and can't do anything else, - // all action done upon it should be noops - // - newly spawned thread exits - - let worker = (create_module_loader.0)(CreateWebWorkerArgs { - name: worker_name, - worker_id, - permissions, - main_module: module_specifier.clone(), - use_deno_namespace, - }); - - // Send thread safe handle to newly created worker to host thread - handle_sender.send(Ok(worker.thread_safe_handle())).unwrap(); - drop(handle_sender); - - // At this point the only method of communication with host - // is using `worker.internal_channels`. - // - // Host can already push messages and interact with worker. - run_web_worker(worker, module_specifier, maybe_source_code) - })?; - - let worker_handle = handle_receiver.recv().unwrap()?; - - let worker_thread = WorkerThread { - join_handle, - worker_handle, - }; - - // At this point all interactions with worker happen using thread - // safe handler returned from previous function calls - state - .borrow_mut::() - .insert(worker_id, worker_thread); - - Ok(json!({ "id": worker_id })) -} - -#[derive(Deserialize)] -struct WorkerArgs { - id: i32, -} - -fn op_host_terminate_worker( - state: &mut OpState, - args: Value, - _data: &mut [ZeroCopyBuf], -) -> Result { - let args: WorkerArgs = serde_json::from_value(args)?; - let id = args.id as u32; - let worker_thread = state - .borrow_mut::() - .remove(&id) - .expect("No worker handle found"); - worker_thread.worker_handle.terminate(); - worker_thread - .join_handle - .join() - .expect("Panic in worker thread") - .expect("Panic in worker event loop"); - Ok(json!({})) -} - -fn serialize_worker_event(event: WorkerEvent) -> Value { - match event { - WorkerEvent::Message(buf) => json!({ "type": "msg", "data": buf }), - WorkerEvent::TerminalError(error) => match error.downcast::() { - Ok(js_error) => json!({ - "type": "terminalError", - "error": { - "message": js_error.message, - "fileName": js_error.script_resource_name, - "lineNumber": js_error.line_number, - "columnNumber": js_error.start_column, - } - }), - Err(error) => json!({ - "type": "terminalError", - "error": { - "message": error.to_string(), - } - }), - }, - WorkerEvent::Error(error) => match error.downcast::() { - Ok(js_error) => json!({ - "type": "error", - "error": { - "message": js_error.message, - "fileName": js_error.script_resource_name, - "lineNumber": js_error.line_number, - "columnNumber": js_error.start_column, - } - }), - Err(error) => json!({ - "type": "error", - "error": { - "message": error.to_string(), - } - }), - }, - } -} - -/// Try to remove worker from workers table - NOTE: `Worker.terminate()` -/// might have been called already meaning that we won't find worker in -/// table - in that case ignore. -fn try_remove_and_close(state: Rc>, id: u32) { - let mut s = state.borrow_mut(); - let workers = s.borrow_mut::(); - if let Some(mut worker_thread) = workers.remove(&id) { - worker_thread.worker_handle.sender.close_channel(); - worker_thread - .join_handle - .join() - .expect("Worker thread panicked") - .expect("Panic in worker event loop"); - } -} - -/// Get message from guest worker as host -async fn op_host_get_message( - state: Rc>, - args: Value, - _zero_copy: BufVec, -) -> Result { - let args: WorkerArgs = serde_json::from_value(args)?; - let id = args.id as u32; - - let worker_handle = { - let s = state.borrow(); - let workers_table = s.borrow::(); - let maybe_handle = workers_table.get(&id); - if let Some(handle) = maybe_handle { - handle.worker_handle.clone() - } else { - // If handle was not found it means worker has already shutdown - return Ok(json!({ "type": "close" })); - } - }; - - let maybe_event = worker_handle.get_event().await?; - if let Some(event) = maybe_event { - // Terminal error means that worker should be removed from worker table. - if let WorkerEvent::TerminalError(_) = &event { - try_remove_and_close(state, id); - } - return Ok(serialize_worker_event(event)); - } - - // If there was no event from worker it means it has already been closed. - try_remove_and_close(state, id); - Ok(json!({ "type": "close" })) -} - -/// Post message to guest worker as host -fn op_host_post_message( - state: &mut OpState, - args: Value, - data: &mut [ZeroCopyBuf], -) -> Result { - assert_eq!(data.len(), 1, "Invalid number of arguments"); - let args: WorkerArgs = serde_json::from_value(args)?; - let id = args.id as u32; - let msg = Vec::from(&*data[0]).into_boxed_slice(); - - debug!("post message to worker {}", id); - let worker_thread = state - .borrow::() - .get(&id) - .expect("No worker handle found"); - worker_thread.worker_handle.post_message(msg)?; - Ok(json!({})) -} diff --git a/cli/permissions.rs b/cli/permissions.rs deleted file mode 100644 index cc3ce8242..000000000 --- a/cli/permissions.rs +++ /dev/null @@ -1,1095 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::colors; -use crate::flags::Flags; -use crate::fs_util::resolve_from_cwd; -use deno_core::error::custom_error; -use deno_core::error::uri_error; -use deno_core::error::AnyError; -use deno_core::url; -use deno_core::ModuleSpecifier; -use serde::Deserialize; -use std::collections::HashSet; -use std::env::current_dir; -use std::fmt; -use std::hash::Hash; -#[cfg(not(test))] -use std::io; -use std::path::{Path, PathBuf}; -#[cfg(test)] -use std::sync::atomic::AtomicBool; -#[cfg(test)] -use std::sync::atomic::Ordering; -#[cfg(test)] -use std::sync::Mutex; - -const PERMISSION_EMOJI: &str = "⚠️"; - -/// Tri-state value for storing permission state -#[derive(PartialEq, Debug, Clone, Copy, Deserialize)] -pub enum PermissionState { - Granted = 0, - Prompt = 1, - Denied = 2, -} - -impl PermissionState { - /// Check the permission state. - fn check(self, msg: &str, flag_name: &str) -> Result<(), AnyError> { - if self == PermissionState::Granted { - log_perm_access(msg); - return Ok(()); - } - let message = format!("{}, run again with the {} flag", msg, flag_name); - Err(custom_error("PermissionDenied", message)) - } -} - -impl fmt::Display for PermissionState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - PermissionState::Granted => f.pad("granted"), - PermissionState::Prompt => f.pad("prompt"), - PermissionState::Denied => f.pad("denied"), - } - } -} - -impl Default for PermissionState { - fn default() -> Self { - PermissionState::Prompt - } -} - -#[derive(Clone, Debug, Default, Deserialize, PartialEq)] -pub struct UnaryPermission { - pub global_state: PermissionState, - pub granted_list: HashSet, - pub denied_list: HashSet, -} - -#[derive(Clone, Debug, Default, Deserialize, PartialEq)] -pub struct Permissions { - pub read: UnaryPermission, - pub write: UnaryPermission, - pub net: UnaryPermission, - pub env: PermissionState, - pub run: PermissionState, - pub plugin: PermissionState, - pub hrtime: PermissionState, -} - -fn resolve_fs_allowlist(allowlist: &[PathBuf]) -> HashSet { - allowlist - .iter() - .map(|raw_path| resolve_from_cwd(Path::new(&raw_path)).unwrap()) - .collect() -} - -impl Permissions { - pub fn from_flags(flags: &Flags) -> Self { - fn state_from_flag_bool(flag: bool) -> PermissionState { - if flag { - PermissionState::Granted - } else { - PermissionState::Prompt - } - } - Self { - read: UnaryPermission:: { - global_state: state_from_flag_bool(flags.allow_read), - granted_list: resolve_fs_allowlist(&flags.read_allowlist), - ..Default::default() - }, - write: UnaryPermission:: { - global_state: state_from_flag_bool(flags.allow_write), - granted_list: resolve_fs_allowlist(&flags.write_allowlist), - ..Default::default() - }, - net: UnaryPermission:: { - global_state: state_from_flag_bool(flags.allow_net), - granted_list: flags.net_allowlist.iter().cloned().collect(), - ..Default::default() - }, - env: state_from_flag_bool(flags.allow_env), - run: state_from_flag_bool(flags.allow_run), - plugin: state_from_flag_bool(flags.allow_plugin), - hrtime: state_from_flag_bool(flags.allow_hrtime), - } - } - - /// Arbitrary helper. Resolves the path from CWD, and also gets a path that - /// can be displayed without leaking the CWD when not allowed. - fn resolved_and_display_path(&self, path: &Path) -> (PathBuf, PathBuf) { - let resolved_path = resolve_from_cwd(path).unwrap(); - let display_path = if path.is_absolute() { - path.to_path_buf() - } else { - match self - .query_read(&Some(¤t_dir().unwrap())) - .check("", "") - { - Ok(_) => resolved_path.clone(), - Err(_) => path.to_path_buf(), - } - }; - (resolved_path, display_path) - } - - pub fn allow_all() -> Self { - Self { - read: UnaryPermission { - global_state: PermissionState::Granted, - ..Default::default() - }, - write: UnaryPermission { - global_state: PermissionState::Granted, - ..Default::default() - }, - net: UnaryPermission { - global_state: PermissionState::Granted, - ..Default::default() - }, - env: PermissionState::Granted, - run: PermissionState::Granted, - plugin: PermissionState::Granted, - hrtime: PermissionState::Granted, - } - } - - pub fn query_read(&self, path: &Option<&Path>) -> PermissionState { - let path = path.map(|p| resolve_from_cwd(p).unwrap()); - if self.read.global_state == PermissionState::Denied - && match path.as_ref() { - None => true, - Some(path) => check_path_blocklist(path, &self.read.denied_list), - } - { - return PermissionState::Denied; - } - if self.read.global_state == PermissionState::Granted - || match path.as_ref() { - None => false, - Some(path) => check_path_allowlist(path, &self.read.granted_list), - } - { - return PermissionState::Granted; - } - PermissionState::Prompt - } - - pub fn query_write(&self, path: &Option<&Path>) -> PermissionState { - let path = path.map(|p| resolve_from_cwd(p).unwrap()); - if self.write.global_state == PermissionState::Denied - && match path.as_ref() { - None => true, - Some(path) => check_path_blocklist(path, &self.write.denied_list), - } - { - return PermissionState::Denied; - } - if self.write.global_state == PermissionState::Granted - || match path.as_ref() { - None => false, - Some(path) => check_path_allowlist(path, &self.write.granted_list), - } - { - return PermissionState::Granted; - } - PermissionState::Prompt - } - - pub fn query_net(&self, host: &str, port: Option) -> PermissionState { - if self.net.global_state == PermissionState::Denied - || check_host_and_port_list(host, port, &self.net.denied_list) - { - return PermissionState::Denied; - } - if self.net.global_state == PermissionState::Granted - || check_host_and_port_list(host, port, &self.net.granted_list) - { - return PermissionState::Granted; - } - PermissionState::Prompt - } - - pub fn query_net_url( - &self, - url: &Option<&str>, - ) -> Result { - if url.is_none() { - return Ok(self.net.global_state); - } - let url: &str = url.unwrap(); - // If url is invalid, then throw a TypeError. - let parsed = url::Url::parse(url)?; - // The url may be parsed correctly but still lack a host, i.e. "localhost:235" or "mailto:someone@somewhere.com" or "file:/1.txt" - // Note that host:port combos are parsed as scheme:path - if parsed.host().is_none() { - return Err(custom_error( - "URIError", - "invalid urlormat: ://[:port][/subpath]", - )); - } - Ok(self.query_net( - &parsed.host().unwrap().to_string(), - parsed.port_or_known_default(), - )) - } - - pub fn query_env(&self) -> PermissionState { - self.env - } - - pub fn query_run(&self) -> PermissionState { - self.run - } - - pub fn query_plugin(&self) -> PermissionState { - self.plugin - } - - pub fn query_hrtime(&self) -> PermissionState { - self.hrtime - } - - pub fn request_read(&mut self, path: &Option<&Path>) -> PermissionState { - if let Some(path) = path { - let (resolved_path, display_path) = self.resolved_and_display_path(path); - let state = self.query_read(&Some(&resolved_path)); - if state == PermissionState::Prompt { - if permission_prompt(&format!( - "Deno requests read access to \"{}\"", - display_path.display() - )) { - self - .read - .granted_list - .retain(|path| !path.starts_with(&resolved_path)); - self.read.granted_list.insert(resolved_path); - return PermissionState::Granted; - } else { - self - .read - .denied_list - .retain(|path| !resolved_path.starts_with(path)); - self.read.denied_list.insert(resolved_path); - self.read.global_state = PermissionState::Denied; - return PermissionState::Denied; - } - } - state - } else { - let state = self.query_read(&None); - if state == PermissionState::Prompt { - if permission_prompt("Deno requests read access") { - self.read.granted_list.clear(); - self.read.global_state = PermissionState::Granted; - return PermissionState::Granted; - } else { - self.read.global_state = PermissionState::Denied; - return PermissionState::Denied; - } - } - state - } - } - - pub fn request_write(&mut self, path: &Option<&Path>) -> PermissionState { - if let Some(path) = path { - let (resolved_path, display_path) = self.resolved_and_display_path(path); - let state = self.query_write(&Some(&resolved_path)); - if state == PermissionState::Prompt { - if permission_prompt(&format!( - "Deno requests write access to \"{}\"", - display_path.display() - )) { - self - .write - .granted_list - .retain(|path| !path.starts_with(&resolved_path)); - self.write.granted_list.insert(resolved_path); - return PermissionState::Granted; - } else { - self - .write - .denied_list - .retain(|path| !resolved_path.starts_with(path)); - self.write.denied_list.insert(resolved_path); - self.write.global_state = PermissionState::Denied; - return PermissionState::Denied; - } - } - state - } else { - let state = self.query_write(&None); - if state == PermissionState::Prompt { - if permission_prompt("Deno requests write access") { - self.write.granted_list.clear(); - self.write.global_state = PermissionState::Granted; - return PermissionState::Granted; - } else { - self.write.global_state = PermissionState::Denied; - return PermissionState::Denied; - } - } - state - } - } - - pub fn request_net( - &mut self, - url: &Option<&str>, - ) -> Result { - if let Some(url) = url { - let state = self.query_net_url(&Some(url))?; - if state == PermissionState::Prompt { - if permission_prompt(&format!( - "Deno requests network access to \"{}\"", - url - )) { - self.net.granted_list.insert(url.to_string()); - return Ok(PermissionState::Granted); - } else { - self.net.denied_list.insert(url.to_string()); - self.net.global_state = PermissionState::Denied; - return Ok(PermissionState::Denied); - } - } - Ok(state) - } else { - let state = self.query_net_url(&None)?; - if state == PermissionState::Prompt { - if permission_prompt("Deno requests network access") { - self.net.granted_list.clear(); - self.net.global_state = PermissionState::Granted; - return Ok(PermissionState::Granted); - } else { - self.net.global_state = PermissionState::Denied; - return Ok(PermissionState::Denied); - } - } - Ok(state) - } - } - - pub fn request_env(&mut self) -> PermissionState { - if self.env == PermissionState::Prompt { - if permission_prompt("Deno requests access to environment variables") { - self.env = PermissionState::Granted; - } else { - self.env = PermissionState::Denied; - } - } - self.env - } - - pub fn request_run(&mut self) -> PermissionState { - if self.run == PermissionState::Prompt { - if permission_prompt("Deno requests to access to run a subprocess") { - self.run = PermissionState::Granted; - } else { - self.run = PermissionState::Denied; - } - } - self.run - } - - pub fn request_plugin(&mut self) -> PermissionState { - if self.plugin == PermissionState::Prompt { - if permission_prompt("Deno requests to open plugins") { - self.plugin = PermissionState::Granted; - } else { - self.plugin = PermissionState::Denied; - } - } - self.plugin - } - - pub fn request_hrtime(&mut self) -> PermissionState { - if self.hrtime == PermissionState::Prompt { - if permission_prompt("Deno requests access to high precision time") { - self.hrtime = PermissionState::Granted; - } else { - self.hrtime = PermissionState::Denied; - } - } - self.hrtime - } - - pub fn revoke_read(&mut self, path: &Option<&Path>) -> PermissionState { - if let Some(path) = path { - let path = resolve_from_cwd(path).unwrap(); - self - .read - .granted_list - .retain(|path_| !path_.starts_with(&path)); - } else { - self.read.granted_list.clear(); - if self.read.global_state == PermissionState::Granted { - self.read.global_state = PermissionState::Prompt; - } - } - self.query_read(path) - } - - pub fn revoke_write(&mut self, path: &Option<&Path>) -> PermissionState { - if let Some(path) = path { - let path = resolve_from_cwd(path).unwrap(); - self - .write - .granted_list - .retain(|path_| !path_.starts_with(&path)); - } else { - self.write.granted_list.clear(); - if self.write.global_state == PermissionState::Granted { - self.write.global_state = PermissionState::Prompt; - } - } - self.query_write(path) - } - - pub fn revoke_net( - &mut self, - url: &Option<&str>, - ) -> Result { - if let Some(url) = url { - self.net.granted_list.remove(*url); - } else { - self.net.granted_list.clear(); - if self.net.global_state == PermissionState::Granted { - self.net.global_state = PermissionState::Prompt; - } - } - self.query_net_url(url) - } - - pub fn revoke_env(&mut self) -> PermissionState { - if self.env == PermissionState::Granted { - self.env = PermissionState::Prompt; - } - self.env - } - - pub fn revoke_run(&mut self) -> PermissionState { - if self.run == PermissionState::Granted { - self.run = PermissionState::Prompt; - } - self.run - } - - pub fn revoke_plugin(&mut self) -> PermissionState { - if self.plugin == PermissionState::Granted { - self.plugin = PermissionState::Prompt; - } - self.plugin - } - - pub fn revoke_hrtime(&mut self) -> PermissionState { - if self.hrtime == PermissionState::Granted { - self.hrtime = PermissionState::Prompt; - } - self.hrtime - } - - pub fn check_read(&self, path: &Path) -> Result<(), AnyError> { - let (resolved_path, display_path) = self.resolved_and_display_path(path); - self.query_read(&Some(&resolved_path)).check( - &format!("read access to \"{}\"", display_path.display()), - "--allow-read", - ) - } - - /// As `check_read()`, but permission error messages will anonymize the path - /// by replacing it with the given `display`. - pub fn check_read_blind( - &self, - path: &Path, - display: &str, - ) -> Result<(), AnyError> { - let resolved_path = resolve_from_cwd(path).unwrap(); - self - .query_read(&Some(&resolved_path)) - .check(&format!("read access to <{}>", display), "--allow-read") - } - - pub fn check_write(&self, path: &Path) -> Result<(), AnyError> { - let (resolved_path, display_path) = self.resolved_and_display_path(path); - self.query_write(&Some(&resolved_path)).check( - &format!("write access to \"{}\"", display_path.display()), - "--allow-write", - ) - } - - pub fn check_net(&self, hostname: &str, port: u16) -> Result<(), AnyError> { - self.query_net(hostname, Some(port)).check( - &format!("network access to \"{}:{}\"", hostname, port), - "--allow-net", - ) - } - - pub fn check_net_url(&self, url: &url::Url) -> Result<(), AnyError> { - let host = url.host_str().ok_or_else(|| uri_error("missing host"))?; - self - .query_net(host, url.port_or_known_default()) - .check(&format!("network access to \"{}\"", url), "--allow-net") - } - - /// A helper function that determines if the module specifier is a local or - /// remote, and performs a read or net check for the specifier. - pub fn check_specifier( - &self, - specifier: &ModuleSpecifier, - ) -> Result<(), AnyError> { - let url = specifier.as_url(); - if url.scheme() == "file" { - let path = url.to_file_path().unwrap(); - self.check_read(&path) - } else { - self.check_net_url(url) - } - } - - pub fn check_env(&self) -> Result<(), AnyError> { - self - .env - .check("access to environment variables", "--allow-env") - } - - pub fn check_run(&self) -> Result<(), AnyError> { - self.run.check("access to run a subprocess", "--allow-run") - } - - pub fn check_plugin(&self, path: &Path) -> Result<(), AnyError> { - let (_, display_path) = self.resolved_and_display_path(path); - self.plugin.check( - &format!("access to open a plugin: {}", display_path.display()), - "--allow-plugin", - ) - } - - pub fn check_hrtime(&self) -> Result<(), AnyError> { - self - .hrtime - .check("access to high precision time", "--allow-hrtime") - } -} - -impl deno_fetch::FetchPermissions for Permissions { - fn check_net_url(&self, url: &url::Url) -> Result<(), AnyError> { - Permissions::check_net_url(self, url) - } - - fn check_read(&self, p: &PathBuf) -> Result<(), AnyError> { - Permissions::check_read(self, p) - } -} - -/// Shows the permission prompt and returns the answer according to the user input. -/// This loops until the user gives the proper input. -#[cfg(not(test))] -fn permission_prompt(message: &str) -> bool { - if !atty::is(atty::Stream::Stdin) || !atty::is(atty::Stream::Stderr) { - return false; - }; - let msg = format!( - "️{} {}. Grant? [g/d (g = grant, d = deny)] ", - PERMISSION_EMOJI, message - ); - // print to stderr so that if deno is > to a file this is still displayed. - eprint!("{}", colors::bold(&msg)); - loop { - let mut input = String::new(); - let stdin = io::stdin(); - let result = stdin.read_line(&mut input); - if result.is_err() { - return false; - }; - let ch = input.chars().next().unwrap(); - match ch.to_ascii_lowercase() { - 'g' => return true, - 'd' => return false, - _ => { - // If we don't get a recognized option try again. - let msg_again = - format!("Unrecognized option '{}' [g/d (g = grant, d = deny)] ", ch); - eprint!("{}", colors::bold(&msg_again)); - } - }; - } -} - -#[cfg(test)] -lazy_static! { - /// Lock this when you use `set_prompt_result` in a test case. - static ref PERMISSION_PROMPT_GUARD: Mutex<()> = Mutex::new(()); -} - -#[cfg(test)] -static STUB_PROMPT_VALUE: AtomicBool = AtomicBool::new(true); - -#[cfg(test)] -fn set_prompt_result(value: bool) { - STUB_PROMPT_VALUE.store(value, Ordering::SeqCst); -} - -// When testing, permission prompt returns the value of STUB_PROMPT_VALUE -// which we set from the test functions. -#[cfg(test)] -fn permission_prompt(_message: &str) -> bool { - STUB_PROMPT_VALUE.load(Ordering::SeqCst) -} - -fn log_perm_access(message: &str) { - debug!( - "{}", - colors::bold(&format!("{}️ Granted {}", PERMISSION_EMOJI, message)) - ); -} - -fn check_path_allowlist(path: &Path, allowlist: &HashSet) -> bool { - for path_ in allowlist { - if path.starts_with(path_) { - return true; - } - } - false -} - -fn check_path_blocklist(path: &Path, blocklist: &HashSet) -> bool { - for path_ in blocklist { - if path_.starts_with(path) { - return true; - } - } - false -} - -fn check_host_and_port_list( - host: &str, - port: Option, - allowlist: &HashSet, -) -> bool { - allowlist.contains(host) - || (port.is_some() - && allowlist.contains(&format!("{}:{}", host, port.unwrap()))) -} - -#[cfg(test)] -mod tests { - use super::*; - use deno_core::serde_json; - - // Creates vector of strings, Vec - macro_rules! svec { - ($($x:expr),*) => (vec![$($x.to_string()),*]); - } - - #[test] - fn check_paths() { - let allowlist = vec![ - PathBuf::from("/a/specific/dir/name"), - PathBuf::from("/a/specific"), - PathBuf::from("/b/c"), - ]; - - let perms = Permissions::from_flags(&Flags { - read_allowlist: allowlist.clone(), - write_allowlist: allowlist, - ..Default::default() - }); - - // Inside of /a/specific and /a/specific/dir/name - assert!(perms.check_read(Path::new("/a/specific/dir/name")).is_ok()); - assert!(perms.check_write(Path::new("/a/specific/dir/name")).is_ok()); - - // Inside of /a/specific but outside of /a/specific/dir/name - assert!(perms.check_read(Path::new("/a/specific/dir")).is_ok()); - assert!(perms.check_write(Path::new("/a/specific/dir")).is_ok()); - - // Inside of /a/specific and /a/specific/dir/name - assert!(perms - .check_read(Path::new("/a/specific/dir/name/inner")) - .is_ok()); - assert!(perms - .check_write(Path::new("/a/specific/dir/name/inner")) - .is_ok()); - - // Inside of /a/specific but outside of /a/specific/dir/name - assert!(perms.check_read(Path::new("/a/specific/other/dir")).is_ok()); - assert!(perms - .check_write(Path::new("/a/specific/other/dir")) - .is_ok()); - - // Exact match with /b/c - assert!(perms.check_read(Path::new("/b/c")).is_ok()); - assert!(perms.check_write(Path::new("/b/c")).is_ok()); - - // Sub path within /b/c - assert!(perms.check_read(Path::new("/b/c/sub/path")).is_ok()); - assert!(perms.check_write(Path::new("/b/c/sub/path")).is_ok()); - - // Sub path within /b/c, needs normalizing - assert!(perms - .check_read(Path::new("/b/c/sub/path/../path/.")) - .is_ok()); - assert!(perms - .check_write(Path::new("/b/c/sub/path/../path/.")) - .is_ok()); - - // Inside of /b but outside of /b/c - assert!(perms.check_read(Path::new("/b/e")).is_err()); - assert!(perms.check_write(Path::new("/b/e")).is_err()); - - // Inside of /a but outside of /a/specific - assert!(perms.check_read(Path::new("/a/b")).is_err()); - assert!(perms.check_write(Path::new("/a/b")).is_err()); - } - - #[test] - fn test_check_net() { - let perms = Permissions::from_flags(&Flags { - net_allowlist: svec![ - "localhost", - "deno.land", - "github.com:3000", - "127.0.0.1", - "172.16.0.2:8000", - "www.github.com:443" - ], - ..Default::default() - }); - - let domain_tests = vec![ - ("localhost", 1234, true), - ("deno.land", 0, true), - ("deno.land", 3000, true), - ("deno.lands", 0, false), - ("deno.lands", 3000, false), - ("github.com", 3000, true), - ("github.com", 0, false), - ("github.com", 2000, false), - ("github.net", 3000, false), - ("127.0.0.1", 0, true), - ("127.0.0.1", 3000, true), - ("127.0.0.2", 0, false), - ("127.0.0.2", 3000, false), - ("172.16.0.2", 8000, true), - ("172.16.0.2", 0, false), - ("172.16.0.2", 6000, false), - ("172.16.0.1", 8000, false), - // Just some random hosts that should err - ("somedomain", 0, false), - ("192.168.0.1", 0, false), - ]; - - let url_tests = vec![ - // Any protocol + port for localhost should be ok, since we don't specify - ("http://localhost", true), - ("https://localhost", true), - ("https://localhost:4443", true), - ("tcp://localhost:5000", true), - ("udp://localhost:6000", true), - // Correct domain + any port and protocol should be ok incorrect shouldn't - ("https://deno.land/std/example/welcome.ts", true), - ("https://deno.land:3000/std/example/welcome.ts", true), - ("https://deno.lands/std/example/welcome.ts", false), - ("https://deno.lands:3000/std/example/welcome.ts", false), - // Correct domain + port should be ok all other combinations should err - ("https://github.com:3000/denoland/deno", true), - ("https://github.com/denoland/deno", false), - ("https://github.com:2000/denoland/deno", false), - ("https://github.net:3000/denoland/deno", false), - // Correct ipv4 address + any port should be ok others should err - ("tcp://127.0.0.1", true), - ("https://127.0.0.1", true), - ("tcp://127.0.0.1:3000", true), - ("https://127.0.0.1:3000", true), - ("tcp://127.0.0.2", false), - ("https://127.0.0.2", false), - ("tcp://127.0.0.2:3000", false), - ("https://127.0.0.2:3000", false), - // Correct address + port should be ok all other combinations should err - ("tcp://172.16.0.2:8000", true), - ("https://172.16.0.2:8000", true), - ("tcp://172.16.0.2", false), - ("https://172.16.0.2", false), - ("tcp://172.16.0.2:6000", false), - ("https://172.16.0.2:6000", false), - ("tcp://172.16.0.1:8000", false), - ("https://172.16.0.1:8000", false), - // Testing issue #6531 (Network permissions check doesn't account for well-known default ports) so we dont regress - ("https://www.github.com:443/robots.txt", true), - ]; - - for (url_str, is_ok) in url_tests.iter() { - let u = url::Url::parse(url_str).unwrap(); - assert_eq!(*is_ok, perms.check_net_url(&u).is_ok()); - } - - for (host, port, is_ok) in domain_tests.iter() { - assert_eq!(*is_ok, perms.check_net(host, *port).is_ok()); - } - } - - #[test] - fn check_specifiers() { - let read_allowlist = if cfg!(target_os = "windows") { - vec![PathBuf::from("C:\\a")] - } else { - vec![PathBuf::from("/a")] - }; - let perms = Permissions::from_flags(&Flags { - read_allowlist, - net_allowlist: svec!["localhost"], - ..Default::default() - }); - - let mut fixtures = vec![ - ( - ModuleSpecifier::resolve_url_or_path("http://localhost:4545/mod.ts") - .unwrap(), - true, - ), - ( - ModuleSpecifier::resolve_url_or_path("http://deno.land/x/mod.ts") - .unwrap(), - false, - ), - ]; - - if cfg!(target_os = "windows") { - fixtures.push(( - ModuleSpecifier::resolve_url_or_path("file:///C:/a/mod.ts").unwrap(), - true, - )); - fixtures.push(( - ModuleSpecifier::resolve_url_or_path("file:///C:/b/mod.ts").unwrap(), - false, - )); - } else { - fixtures.push(( - ModuleSpecifier::resolve_url_or_path("file:///a/mod.ts").unwrap(), - true, - )); - fixtures.push(( - ModuleSpecifier::resolve_url_or_path("file:///b/mod.ts").unwrap(), - false, - )); - } - - for (specifier, expected) in fixtures { - assert_eq!(perms.check_specifier(&specifier).is_ok(), expected); - } - } - - #[test] - fn test_deserialize_perms() { - let json_perms = r#" - { - "read": { - "global_state": "Granted", - "granted_list": [], - "denied_list": [] - }, - "write": { - "global_state": "Granted", - "granted_list": [], - "denied_list": [] - }, - "net": { - "global_state": "Granted", - "granted_list": [], - "denied_list": [] - }, - "env": "Granted", - "run": "Granted", - "plugin": "Granted", - "hrtime": "Granted" - } - "#; - let perms0 = Permissions { - read: UnaryPermission { - global_state: PermissionState::Granted, - ..Default::default() - }, - write: UnaryPermission { - global_state: PermissionState::Granted, - ..Default::default() - }, - net: UnaryPermission { - global_state: PermissionState::Granted, - ..Default::default() - }, - env: PermissionState::Granted, - run: PermissionState::Granted, - hrtime: PermissionState::Granted, - plugin: PermissionState::Granted, - }; - let deserialized_perms: Permissions = - serde_json::from_str(json_perms).unwrap(); - assert_eq!(perms0, deserialized_perms); - } - - #[test] - fn test_query() { - let perms1 = Permissions { - read: UnaryPermission { - global_state: PermissionState::Granted, - ..Default::default() - }, - write: UnaryPermission { - global_state: PermissionState::Granted, - ..Default::default() - }, - net: UnaryPermission { - global_state: PermissionState::Granted, - ..Default::default() - }, - env: PermissionState::Granted, - run: PermissionState::Granted, - plugin: PermissionState::Granted, - hrtime: PermissionState::Granted, - }; - let perms2 = Permissions { - read: UnaryPermission { - global_state: PermissionState::Prompt, - granted_list: resolve_fs_allowlist(&[PathBuf::from("/foo")]), - ..Default::default() - }, - write: UnaryPermission { - global_state: PermissionState::Prompt, - granted_list: resolve_fs_allowlist(&[PathBuf::from("/foo")]), - ..Default::default() - }, - net: UnaryPermission { - global_state: PermissionState::Prompt, - granted_list: ["127.0.0.1:8000".to_string()].iter().cloned().collect(), - ..Default::default() - }, - env: PermissionState::Prompt, - run: PermissionState::Prompt, - plugin: PermissionState::Prompt, - hrtime: PermissionState::Prompt, - }; - #[rustfmt::skip] - { - assert_eq!(perms1.query_read(&None), PermissionState::Granted); - assert_eq!(perms1.query_read(&Some(&Path::new("/foo"))), PermissionState::Granted); - assert_eq!(perms2.query_read(&None), PermissionState::Prompt); - assert_eq!(perms2.query_read(&Some(&Path::new("/foo"))), PermissionState::Granted); - assert_eq!(perms2.query_read(&Some(&Path::new("/foo/bar"))), PermissionState::Granted); - assert_eq!(perms1.query_write(&None), PermissionState::Granted); - assert_eq!(perms1.query_write(&Some(&Path::new("/foo"))), PermissionState::Granted); - assert_eq!(perms2.query_write(&None), PermissionState::Prompt); - assert_eq!(perms2.query_write(&Some(&Path::new("/foo"))), PermissionState::Granted); - assert_eq!(perms2.query_write(&Some(&Path::new("/foo/bar"))), PermissionState::Granted); - assert_eq!(perms1.query_net_url(&None).unwrap(), PermissionState::Granted); - assert_eq!(perms1.query_net_url(&Some("http://127.0.0.1:8000")).unwrap(), PermissionState::Granted); - assert_eq!(perms2.query_net_url(&None).unwrap(), PermissionState::Prompt); - assert_eq!(perms2.query_net_url(&Some("http://127.0.0.1:8000")).unwrap(), PermissionState::Granted); - assert_eq!(perms1.query_env(), PermissionState::Granted); - assert_eq!(perms2.query_env(), PermissionState::Prompt); - assert_eq!(perms1.query_run(), PermissionState::Granted); - assert_eq!(perms2.query_run(), PermissionState::Prompt); - assert_eq!(perms1.query_plugin(), PermissionState::Granted); - assert_eq!(perms2.query_plugin(), PermissionState::Prompt); - assert_eq!(perms1.query_hrtime(), PermissionState::Granted); - assert_eq!(perms2.query_hrtime(), PermissionState::Prompt); - }; - } - - #[test] - fn test_request() { - let mut perms = Permissions { - read: UnaryPermission { - global_state: PermissionState::Prompt, - ..Default::default() - }, - write: UnaryPermission { - global_state: PermissionState::Prompt, - ..Default::default() - }, - net: UnaryPermission { - global_state: PermissionState::Prompt, - ..Default::default() - }, - env: PermissionState::Prompt, - run: PermissionState::Prompt, - plugin: PermissionState::Prompt, - hrtime: PermissionState::Prompt, - }; - #[rustfmt::skip] - { - let _guard = PERMISSION_PROMPT_GUARD.lock().unwrap(); - set_prompt_result(true); - assert_eq!(perms.request_read(&Some(&Path::new("/foo"))), PermissionState::Granted); - assert_eq!(perms.query_read(&None), PermissionState::Prompt); - set_prompt_result(false); - assert_eq!(perms.request_read(&Some(&Path::new("/foo/bar"))), PermissionState::Granted); - set_prompt_result(false); - assert_eq!(perms.request_write(&Some(&Path::new("/foo"))), PermissionState::Denied); - assert_eq!(perms.query_write(&Some(&Path::new("/foo/bar"))), PermissionState::Prompt); - set_prompt_result(true); - assert_eq!(perms.request_write(&None), PermissionState::Denied); - set_prompt_result(true); - assert_eq!(perms.request_net(&None).unwrap(), PermissionState::Granted); - set_prompt_result(false); - assert_eq!(perms.request_net(&Some("http://127.0.0.1:8000")).unwrap(), PermissionState::Granted); - set_prompt_result(true); - assert_eq!(perms.request_env(), PermissionState::Granted); - set_prompt_result(false); - assert_eq!(perms.request_env(), PermissionState::Granted); - set_prompt_result(false); - assert_eq!(perms.request_run(), PermissionState::Denied); - set_prompt_result(true); - assert_eq!(perms.request_run(), PermissionState::Denied); - set_prompt_result(true); - assert_eq!(perms.request_plugin(), PermissionState::Granted); - set_prompt_result(false); - assert_eq!(perms.request_plugin(), PermissionState::Granted); - set_prompt_result(false); - assert_eq!(perms.request_hrtime(), PermissionState::Denied); - set_prompt_result(true); - assert_eq!(perms.request_hrtime(), PermissionState::Denied); - }; - } - - #[test] - fn test_revoke() { - let mut perms = Permissions { - read: UnaryPermission { - global_state: PermissionState::Prompt, - granted_list: resolve_fs_allowlist(&[PathBuf::from("/foo")]), - ..Default::default() - }, - write: UnaryPermission { - global_state: PermissionState::Prompt, - granted_list: resolve_fs_allowlist(&[PathBuf::from("/foo")]), - ..Default::default() - }, - net: UnaryPermission { - global_state: PermissionState::Denied, - ..Default::default() - }, - env: PermissionState::Granted, - run: PermissionState::Granted, - plugin: PermissionState::Prompt, - hrtime: PermissionState::Denied, - }; - #[rustfmt::skip] - { - assert_eq!(perms.revoke_read(&Some(&Path::new("/foo/bar"))), PermissionState::Granted); - assert_eq!(perms.revoke_read(&Some(&Path::new("/foo"))), PermissionState::Prompt); - assert_eq!(perms.query_read(&Some(&Path::new("/foo/bar"))), PermissionState::Prompt); - assert_eq!(perms.revoke_write(&Some(&Path::new("/foo/bar"))), PermissionState::Granted); - assert_eq!(perms.revoke_write(&None), PermissionState::Prompt); - assert_eq!(perms.query_write(&Some(&Path::new("/foo/bar"))), PermissionState::Prompt); - assert_eq!(perms.revoke_net(&None).unwrap(), PermissionState::Denied); - assert_eq!(perms.revoke_env(), PermissionState::Prompt); - assert_eq!(perms.revoke_run(), PermissionState::Prompt); - assert_eq!(perms.revoke_plugin(), PermissionState::Prompt); - assert_eq!(perms.revoke_hrtime(), PermissionState::Denied); - }; - } -} diff --git a/cli/program_state.rs b/cli/program_state.rs index 7b86b7de5..223d043ba 100644 --- a/cli/program_state.rs +++ b/cli/program_state.rs @@ -7,16 +7,16 @@ use crate::flags; use crate::http_cache; use crate::http_util; use crate::import_map::ImportMap; -use crate::inspector::InspectorServer; use crate::lockfile::Lockfile; use crate::media_type::MediaType; use crate::module_graph::CheckOptions; use crate::module_graph::GraphBuilder; use crate::module_graph::TranspileOptions; use crate::module_graph::TypeLib; -use crate::permissions::Permissions; use crate::source_maps::SourceMapGetter; use crate::specifier_handler::FetchHandler; +use deno_runtime::inspector::InspectorServer; +use deno_runtime::permissions::Permissions; use deno_core::error::generic_error; use deno_core::error::AnyError; diff --git a/cli/resolve_addr.rs b/cli/resolve_addr.rs deleted file mode 100644 index c3dc52f8f..000000000 --- a/cli/resolve_addr.rs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use deno_core::error::AnyError; -use std::net::SocketAddr; -use std::net::ToSocketAddrs; - -/// Resolve network address. Returns a future. -pub fn resolve_addr(hostname: &str, port: u16) -> Result { - // Default to localhost if given just the port. Example: ":80" - let addr: &str = if !hostname.is_empty() { - &hostname - } else { - "0.0.0.0" - }; - - // If this looks like an ipv6 IP address. Example: "[2001:db8::1]" - // Then we remove the brackets. - let addr = if addr.starts_with('[') && addr.ends_with(']') { - let l = addr.len() - 1; - addr.get(1..l).unwrap() - } else { - addr - }; - let addr_port_pair = (addr, port); - let mut iter = addr_port_pair.to_socket_addrs()?; - Ok(iter.next().unwrap()) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::net::Ipv4Addr; - use std::net::Ipv6Addr; - use std::net::SocketAddrV4; - use std::net::SocketAddrV6; - - #[test] - fn resolve_addr1() { - let expected = - SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 80)); - let actual = resolve_addr("127.0.0.1", 80).unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn resolve_addr2() { - let expected = - SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 80)); - let actual = resolve_addr("", 80).unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn resolve_addr3() { - let expected = - SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 0, 2, 1), 25)); - let actual = resolve_addr("192.0.2.1", 25).unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn resolve_addr_ipv6() { - let expected = SocketAddr::V6(SocketAddrV6::new( - Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1), - 8080, - 0, - 0, - )); - let actual = resolve_addr("[2001:db8::1]", 8080).unwrap(); - assert_eq!(actual, expected); - } -} diff --git a/cli/rt/00_bootstrap_namespace.js b/cli/rt/00_bootstrap_namespace.js deleted file mode 100644 index 514cbe3f0..000000000 --- a/cli/rt/00_bootstrap_namespace.js +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -// The only purpose of this file is to set up "globalThis.__bootstrap" namespace, -// that is used by scripts in this directory to reference exports between -// the files. - -// This namespace is removed during runtime bootstrapping process. - -globalThis.__bootstrap = globalThis.__bootstrap || {}; diff --git a/cli/rt/01_build.js b/cli/rt/01_build.js deleted file mode 100644 index 7c1dc817e..000000000 --- a/cli/rt/01_build.js +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const build = { - target: "unknown", - arch: "unknown", - os: "unknown", - vendor: "unknown", - env: undefined, - }; - - function setBuildInfo(target) { - const [arch, vendor, os, env] = target.split("-", 4); - build.target = target; - build.arch = arch; - build.vendor = vendor; - build.os = os; - build.env = env; - Object.freeze(build); - } - - window.__bootstrap.build = { - build, - setBuildInfo, - }; -})(this); diff --git a/cli/rt/01_colors.js b/cli/rt/01_colors.js deleted file mode 100644 index 39e4a7a18..000000000 --- a/cli/rt/01_colors.js +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - function code(open, close) { - return { - open: `\x1b[${open}m`, - close: `\x1b[${close}m`, - regexp: new RegExp(`\\x1b\\[${close}m`, "g"), - }; - } - - function run(str, code) { - return `${code.open}${str.replace(code.regexp, code.open)}${code.close}`; - } - - function bold(str) { - return run(str, code(1, 22)); - } - - function italic(str) { - return run(str, code(3, 23)); - } - - function yellow(str) { - return run(str, code(33, 39)); - } - - function cyan(str) { - return run(str, code(36, 39)); - } - - function red(str) { - return run(str, code(31, 39)); - } - - function green(str) { - return run(str, code(32, 39)); - } - - function bgRed(str) { - return run(str, code(41, 49)); - } - - function white(str) { - return run(str, code(37, 39)); - } - - function gray(str) { - return run(str, code(90, 39)); - } - - function magenta(str) { - return run(str, code(35, 39)); - } - - function dim(str) { - return run(str, code(2, 22)); - } - - // https://github.com/chalk/ansi-regex/blob/2b56fb0c7a07108e5b54241e8faec160d393aedb/index.js - const ANSI_PATTERN = new RegExp( - [ - "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", - "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", - ].join("|"), - "g", - ); - - function stripColor(string) { - return string.replace(ANSI_PATTERN, ""); - } - - function maybeColor(fn) { - return !(globalThis.Deno?.noColor ?? false) ? fn : (s) => s; - } - - window.__bootstrap.colors = { - bold, - italic, - yellow, - cyan, - red, - green, - bgRed, - white, - gray, - magenta, - dim, - stripColor, - maybeColor, - }; -})(this); diff --git a/cli/rt/01_errors.js b/cli/rt/01_errors.js deleted file mode 100644 index d5933069b..000000000 --- a/cli/rt/01_errors.js +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - class NotFound extends Error { - constructor(msg) { - super(msg); - this.name = "NotFound"; - } - } - - class PermissionDenied extends Error { - constructor(msg) { - super(msg); - this.name = "PermissionDenied"; - } - } - - class ConnectionRefused extends Error { - constructor(msg) { - super(msg); - this.name = "ConnectionRefused"; - } - } - - class ConnectionReset extends Error { - constructor(msg) { - super(msg); - this.name = "ConnectionReset"; - } - } - - class ConnectionAborted extends Error { - constructor(msg) { - super(msg); - this.name = "ConnectionAborted"; - } - } - - class NotConnected extends Error { - constructor(msg) { - super(msg); - this.name = "NotConnected"; - } - } - - class AddrInUse extends Error { - constructor(msg) { - super(msg); - this.name = "AddrInUse"; - } - } - - class AddrNotAvailable extends Error { - constructor(msg) { - super(msg); - this.name = "AddrNotAvailable"; - } - } - - class BrokenPipe extends Error { - constructor(msg) { - super(msg); - this.name = "BrokenPipe"; - } - } - - class AlreadyExists extends Error { - constructor(msg) { - super(msg); - this.name = "AlreadyExists"; - } - } - - class InvalidData extends Error { - constructor(msg) { - super(msg); - this.name = "InvalidData"; - } - } - - class TimedOut extends Error { - constructor(msg) { - super(msg); - this.name = "TimedOut"; - } - } - - class Interrupted extends Error { - constructor(msg) { - super(msg); - this.name = "Interrupted"; - } - } - - class WriteZero extends Error { - constructor(msg) { - super(msg); - this.name = "WriteZero"; - } - } - - class UnexpectedEof extends Error { - constructor(msg) { - super(msg); - this.name = "UnexpectedEof"; - } - } - - class BadResource extends Error { - constructor(msg) { - super(msg); - this.name = "BadResource"; - } - } - - class Http extends Error { - constructor(msg) { - super(msg); - this.name = "Http"; - } - } - - class Busy extends Error { - constructor(msg) { - super(msg); - this.name = "Busy"; - } - } - - class NotSupported extends Error { - constructor(msg) { - super(msg); - this.name = "NotSupported"; - } - } - - const errors = { - NotFound, - PermissionDenied, - ConnectionRefused, - ConnectionReset, - ConnectionAborted, - NotConnected, - AddrInUse, - AddrNotAvailable, - BrokenPipe, - AlreadyExists, - InvalidData, - TimedOut, - Interrupted, - WriteZero, - UnexpectedEof, - BadResource, - Http, - Busy, - NotSupported, - }; - - window.__bootstrap.errors = { - errors, - }; -})(this); diff --git a/cli/rt/01_internals.js b/cli/rt/01_internals.js deleted file mode 100644 index eee9eeaf7..000000000 --- a/cli/rt/01_internals.js +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const internalSymbol = Symbol("Deno.internal"); - - // The object where all the internal fields for testing will be living. - const internalObject = {}; - - // Register a field to internalObject for test access, - // through Deno[Deno.internal][name]. - function exposeForTest(name, value) { - Object.defineProperty(internalObject, name, { - value, - enumerable: false, - }); - } - - window.__bootstrap.internals = { - internalSymbol, - internalObject, - exposeForTest, - }; -})(this); diff --git a/cli/rt/01_version.js b/cli/rt/01_version.js deleted file mode 100644 index 325e1156f..000000000 --- a/cli/rt/01_version.js +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const version = { - deno: "", - v8: "", - typescript: "", - }; - - function setVersions( - denoVersion, - v8Version, - tsVersion, - ) { - version.deno = denoVersion; - version.v8 = v8Version; - version.typescript = tsVersion; - - Object.freeze(version); - } - - window.__bootstrap.version = { - version, - setVersions, - }; -})(this); diff --git a/cli/rt/01_web_util.js b/cli/rt/01_web_util.js deleted file mode 100644 index a9573a71d..000000000 --- a/cli/rt/01_web_util.js +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const illegalConstructorKey = Symbol("illegalConstructorKey"); - - function requiredArguments( - name, - length, - required, - ) { - if (length < required) { - const errMsg = `${name} requires at least ${required} argument${ - required === 1 ? "" : "s" - }, but only ${length} present`; - throw new TypeError(errMsg); - } - } - - const objectCloneMemo = new WeakMap(); - - function cloneArrayBuffer( - srcBuffer, - srcByteOffset, - srcLength, - _cloneConstructor, - ) { - // this function fudges the return type but SharedArrayBuffer is disabled for a while anyway - return srcBuffer.slice( - srcByteOffset, - srcByteOffset + srcLength, - ); - } - - /** Clone a value in a similar way to structured cloning. It is similar to a - * StructureDeserialize(StructuredSerialize(...)). */ - function cloneValue(value) { - switch (typeof value) { - case "number": - case "string": - case "boolean": - case "undefined": - case "bigint": - return value; - case "object": { - if (objectCloneMemo.has(value)) { - return objectCloneMemo.get(value); - } - if (value === null) { - return value; - } - if (value instanceof Date) { - return new Date(value.valueOf()); - } - if (value instanceof RegExp) { - return new RegExp(value); - } - if (value instanceof SharedArrayBuffer) { - return value; - } - if (value instanceof ArrayBuffer) { - const cloned = cloneArrayBuffer( - value, - 0, - value.byteLength, - ArrayBuffer, - ); - objectCloneMemo.set(value, cloned); - return cloned; - } - if (ArrayBuffer.isView(value)) { - const clonedBuffer = cloneValue(value.buffer); - // Use DataViewConstructor type purely for type-checking, can be a - // DataView or TypedArray. They use the same constructor signature, - // only DataView has a length in bytes and TypedArrays use a length in - // terms of elements, so we adjust for that. - let length; - if (value instanceof DataView) { - length = value.byteLength; - } else { - length = value.length; - } - return new (value.constructor)( - clonedBuffer, - value.byteOffset, - length, - ); - } - if (value instanceof Map) { - const clonedMap = new Map(); - objectCloneMemo.set(value, clonedMap); - value.forEach((v, k) => { - clonedMap.set(cloneValue(k), cloneValue(v)); - }); - return clonedMap; - } - if (value instanceof Set) { - // assumes that cloneValue still takes only one argument - const clonedSet = new Set([...value].map(cloneValue)); - objectCloneMemo.set(value, clonedSet); - return clonedSet; - } - - // default for objects - const clonedObj = {}; - objectCloneMemo.set(value, clonedObj); - const sourceKeys = Object.getOwnPropertyNames(value); - for (const key of sourceKeys) { - clonedObj[key] = cloneValue(value[key]); - } - Reflect.setPrototypeOf(clonedObj, Reflect.getPrototypeOf(value)); - return clonedObj; - } - case "symbol": - case "function": - default: - throw new DOMException("Uncloneable value in stream", "DataCloneError"); - } - } - - const handlerSymbol = Symbol("eventHandlers"); - function makeWrappedHandler(handler) { - function wrappedHandler(...args) { - if (typeof wrappedHandler.handler !== "function") { - return; - } - return wrappedHandler.handler.call(this, ...args); - } - wrappedHandler.handler = handler; - return wrappedHandler; - } - function defineEventHandler(emitter, name, defaultValue = undefined) { - // HTML specification section 8.1.5.1 - Object.defineProperty(emitter, `on${name}`, { - get() { - return this[handlerSymbol]?.get(name)?.handler ?? defaultValue; - }, - set(value) { - if (!this[handlerSymbol]) { - this[handlerSymbol] = new Map(); - } - let handlerWrapper = this[handlerSymbol]?.get(name); - if (handlerWrapper) { - handlerWrapper.handler = value; - } else { - handlerWrapper = makeWrappedHandler(value); - this.addEventListener(name, handlerWrapper); - } - this[handlerSymbol].set(name, handlerWrapper); - }, - configurable: true, - enumerable: true, - }); - } - window.__bootstrap.webUtil = { - illegalConstructorKey, - requiredArguments, - defineEventHandler, - cloneValue, - }; -})(this); diff --git a/cli/rt/02_console.js b/cli/rt/02_console.js deleted file mode 100644 index 971837bd6..000000000 --- a/cli/rt/02_console.js +++ /dev/null @@ -1,1732 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - const exposeForTest = window.__bootstrap.internals.exposeForTest; - const colors = window.__bootstrap.colors; - - function isInvalidDate(x) { - return isNaN(x.getTime()); - } - - function hasOwnProperty(obj, v) { - if (obj == null) { - return false; - } - return Object.prototype.hasOwnProperty.call(obj, v); - } - - // Copyright Joyent, Inc. and other Node contributors. MIT license. - // Forked from Node's lib/internal/cli_table.js - - function isTypedArray(x) { - return ArrayBuffer.isView(x) && !(x instanceof DataView); - } - - const tableChars = { - middleMiddle: "─", - rowMiddle: "┼", - topRight: "┐", - topLeft: "┌", - leftMiddle: "├", - topMiddle: "┬", - bottomRight: "┘", - bottomLeft: "└", - bottomMiddle: "┴", - rightMiddle: "┤", - left: "│ ", - right: " │", - middle: " │ ", - }; - - function isFullWidthCodePoint(code) { - // Code points are partially derived from: - // http://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt - return ( - code >= 0x1100 && - (code <= 0x115f || // Hangul Jamo - code === 0x2329 || // LEFT-POINTING ANGLE BRACKET - code === 0x232a || // RIGHT-POINTING ANGLE BRACKET - // CJK Radicals Supplement .. Enclosed CJK Letters and Months - (code >= 0x2e80 && code <= 0x3247 && code !== 0x303f) || - // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A - (code >= 0x3250 && code <= 0x4dbf) || - // CJK Unified Ideographs .. Yi Radicals - (code >= 0x4e00 && code <= 0xa4c6) || - // Hangul Jamo Extended-A - (code >= 0xa960 && code <= 0xa97c) || - // Hangul Syllables - (code >= 0xac00 && code <= 0xd7a3) || - // CJK Compatibility Ideographs - (code >= 0xf900 && code <= 0xfaff) || - // Vertical Forms - (code >= 0xfe10 && code <= 0xfe19) || - // CJK Compatibility Forms .. Small Form Variants - (code >= 0xfe30 && code <= 0xfe6b) || - // Halfwidth and Fullwidth Forms - (code >= 0xff01 && code <= 0xff60) || - (code >= 0xffe0 && code <= 0xffe6) || - // Kana Supplement - (code >= 0x1b000 && code <= 0x1b001) || - // Enclosed Ideographic Supplement - (code >= 0x1f200 && code <= 0x1f251) || - // Miscellaneous Symbols and Pictographs 0x1f300 - 0x1f5ff - // Emoticons 0x1f600 - 0x1f64f - (code >= 0x1f300 && code <= 0x1f64f) || - // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane - (code >= 0x20000 && code <= 0x3fffd)) - ); - } - - function getStringWidth(str) { - str = colors.stripColor(str).normalize("NFC"); - let width = 0; - - for (const ch of str) { - width += isFullWidthCodePoint(ch.codePointAt(0)) ? 2 : 1; - } - - return width; - } - - function renderRow(row, columnWidths) { - let out = tableChars.left; - for (let i = 0; i < row.length; i++) { - const cell = row[i]; - const len = getStringWidth(cell); - const needed = (columnWidths[i] - len) / 2; - // round(needed) + ceil(needed) will always add up to the amount - // of spaces we need while also left justifying the output. - out += `${" ".repeat(needed)}${cell}${" ".repeat(Math.ceil(needed))}`; - if (i !== row.length - 1) { - out += tableChars.middle; - } - } - out += tableChars.right; - return out; - } - - function cliTable(head, columns) { - const rows = []; - const columnWidths = head.map((h) => getStringWidth(h)); - const longestColumn = columns.reduce( - (n, a) => Math.max(n, a.length), - 0, - ); - - for (let i = 0; i < head.length; i++) { - const column = columns[i]; - for (let j = 0; j < longestColumn; j++) { - if (rows[j] === undefined) { - rows[j] = []; - } - const value = (rows[j][i] = hasOwnProperty(column, j) ? column[j] : ""); - const width = columnWidths[i] || 0; - const counted = getStringWidth(value); - columnWidths[i] = Math.max(width, counted); - } - } - - const divider = columnWidths.map((i) => - tableChars.middleMiddle.repeat(i + 2) - ); - - let result = `${tableChars.topLeft}${divider.join(tableChars.topMiddle)}` + - `${tableChars.topRight}\n${renderRow(head, columnWidths)}\n` + - `${tableChars.leftMiddle}${divider.join(tableChars.rowMiddle)}` + - `${tableChars.rightMiddle}\n`; - - for (const row of rows) { - result += `${renderRow(row, columnWidths)}\n`; - } - - result += - `${tableChars.bottomLeft}${divider.join(tableChars.bottomMiddle)}` + - tableChars.bottomRight; - - return result; - } - /* End of forked part */ - - const DEFAULT_INSPECT_OPTIONS = { - depth: 4, - indentLevel: 0, - sorted: false, - trailingComma: false, - compact: true, - iterableLimit: 100, - showProxy: false, - colors: false, - getters: false, - }; - - const DEFAULT_INDENT = " "; // Default indent string - - const LINE_BREAKING_LENGTH = 80; - const MIN_GROUP_LENGTH = 6; - const STR_ABBREVIATE_SIZE = 100; - - const PROMISE_STRING_BASE_LENGTH = 12; - - class CSI { - static kClear = "\x1b[1;1H"; - static kClearScreenDown = "\x1b[0J"; - } - - function getClassInstanceName(instance) { - if (typeof instance != "object") { - return ""; - } - const constructor = instance?.constructor; - if (typeof constructor == "function") { - return constructor.name ?? ""; - } - return ""; - } - - function maybeColor(fn, inspectOptions) { - return inspectOptions.colors ? fn : (s) => s; - } - - function inspectFunction(value, _ctx) { - if (customInspect in value && typeof value[customInspect] === "function") { - try { - return String(value[customInspect]()); - } catch { - // pass - } - } - // Might be Function/AsyncFunction/GeneratorFunction/AsyncGeneratorFunction - let cstrName = Object.getPrototypeOf(value)?.constructor?.name; - if (!cstrName) { - // If prototype is removed or broken, - // use generic 'Function' instead. - cstrName = "Function"; - } - if (value.name && value.name !== "anonymous") { - // from MDN spec - return `[${cstrName}: ${value.name}]`; - } - return `[${cstrName}]`; - } - - function inspectIterable( - value, - ctx, - level, - options, - inspectOptions, - ) { - const cyan = maybeColor(colors.cyan, inspectOptions); - if (level >= inspectOptions.depth) { - return cyan(`[${options.typeName}]`); - } - ctx.add(value); - - const entries = []; - - const iter = value.entries(); - let entriesLength = 0; - const next = () => { - return iter.next(); - }; - for (const el of iter) { - if (entriesLength < inspectOptions.iterableLimit) { - entries.push( - options.entryHandler( - el, - ctx, - level + 1, - inspectOptions, - next.bind(iter), - ), - ); - } - entriesLength++; - } - ctx.delete(value); - - if (options.sort) { - entries.sort(); - } - - if (entriesLength > inspectOptions.iterableLimit) { - const nmore = entriesLength - inspectOptions.iterableLimit; - entries.push(`... ${nmore} more items`); - } - - const iPrefix = `${options.displayName ? options.displayName + " " : ""}`; - - const initIndentation = `\n${DEFAULT_INDENT.repeat(level + 1)}`; - const entryIndentation = `,\n${DEFAULT_INDENT.repeat(level + 1)}`; - const closingIndentation = `${inspectOptions.trailingComma ? "," : ""}\n${ - DEFAULT_INDENT.repeat(level) - }`; - - let iContent; - if (options.group && entries.length > MIN_GROUP_LENGTH) { - const groups = groupEntries(entries, level, value); - iContent = `${initIndentation}${ - groups.join(entryIndentation) - }${closingIndentation}`; - } else { - iContent = entries.length === 0 ? "" : ` ${entries.join(", ")} `; - if ( - colors.stripColor(iContent).length > LINE_BREAKING_LENGTH || - !inspectOptions.compact - ) { - iContent = `${initIndentation}${ - entries.join(entryIndentation) - }${closingIndentation}`; - } - } - - return `${iPrefix}${options.delims[0]}${iContent}${options.delims[1]}`; - } - - // Ported from Node.js - // Copyright Node.js contributors. All rights reserved. - function groupEntries( - entries, - level, - value, - iterableLimit = 100, - ) { - let totalLength = 0; - let maxLength = 0; - let entriesLength = entries.length; - if (iterableLimit < entriesLength) { - // This makes sure the "... n more items" part is not taken into account. - entriesLength--; - } - const separatorSpace = 2; // Add 1 for the space and 1 for the separator. - const dataLen = new Array(entriesLength); - // Calculate the total length of all output entries and the individual max - // entries length of all output entries. - // IN PROGRESS: Colors are being taken into account. - for (let i = 0; i < entriesLength; i++) { - // Taking colors into account: removing the ANSI color - // codes from the string before measuring its length - const len = colors.stripColor(entries[i]).length; - dataLen[i] = len; - totalLength += len + separatorSpace; - if (maxLength < len) maxLength = len; - } - // Add two to `maxLength` as we add a single whitespace character plus a comma - // in-between two entries. - const actualMax = maxLength + separatorSpace; - // Check if at least three entries fit next to each other and prevent grouping - // of arrays that contains entries of very different length (i.e., if a single - // entry is longer than 1/5 of all other entries combined). Otherwise the - // space in-between small entries would be enormous. - if ( - actualMax * 3 + (level + 1) < LINE_BREAKING_LENGTH && - (totalLength / actualMax > 5 || maxLength <= 6) - ) { - const approxCharHeights = 2.5; - const averageBias = Math.sqrt(actualMax - totalLength / entries.length); - const biasedMax = Math.max(actualMax - 3 - averageBias, 1); - // Dynamically check how many columns seem possible. - const columns = Math.min( - // Ideally a square should be drawn. We expect a character to be about 2.5 - // times as high as wide. This is the area formula to calculate a square - // which contains n rectangles of size `actualMax * approxCharHeights`. - // Divide that by `actualMax` to receive the correct number of columns. - // The added bias increases the columns for short entries. - Math.round( - Math.sqrt(approxCharHeights * biasedMax * entriesLength) / biasedMax, - ), - // Do not exceed the breakLength. - Math.floor((LINE_BREAKING_LENGTH - (level + 1)) / actualMax), - // Limit the columns to a maximum of fifteen. - 15, - ); - // Return with the original output if no grouping should happen. - if (columns <= 1) { - return entries; - } - const tmp = []; - const maxLineLength = []; - for (let i = 0; i < columns; i++) { - let lineMaxLength = 0; - for (let j = i; j < entries.length; j += columns) { - if (dataLen[j] > lineMaxLength) lineMaxLength = dataLen[j]; - } - lineMaxLength += separatorSpace; - maxLineLength[i] = lineMaxLength; - } - let order = "padStart"; - if (value !== undefined) { - for (let i = 0; i < entries.length; i++) { - if ( - typeof value[i] !== "number" && - typeof value[i] !== "bigint" - ) { - order = "padEnd"; - break; - } - } - } - // Each iteration creates a single line of grouped entries. - for (let i = 0; i < entriesLength; i += columns) { - // The last lines may contain less entries than columns. - const max = Math.min(i + columns, entriesLength); - let str = ""; - let j = i; - for (; j < max - 1; j++) { - const lengthOfColorCodes = entries[j].length - dataLen[j]; - const padding = maxLineLength[j - i] + lengthOfColorCodes; - str += `${entries[j]}, `[order](padding, " "); - } - if (order === "padStart") { - const lengthOfColorCodes = entries[j].length - dataLen[j]; - const padding = maxLineLength[j - i] + - lengthOfColorCodes - - separatorSpace; - str += entries[j].padStart(padding, " "); - } else { - str += entries[j]; - } - tmp.push(str); - } - if (iterableLimit < entries.length) { - tmp.push(entries[entriesLength]); - } - entries = tmp; - } - return entries; - } - - function inspectValue( - value, - ctx, - level, - inspectOptions, - ) { - const proxyDetails = core.getProxyDetails(value); - if (proxyDetails != null) { - return inspectOptions.showProxy - ? inspectProxy(proxyDetails, ctx, level, inspectOptions) - : inspectValue(proxyDetails[0], ctx, level, inspectOptions); - } - - const green = maybeColor(colors.green, inspectOptions); - const yellow = maybeColor(colors.yellow, inspectOptions); - const dim = maybeColor(colors.dim, inspectOptions); - const cyan = maybeColor(colors.cyan, inspectOptions); - const bold = maybeColor(colors.bold, inspectOptions); - const red = maybeColor(colors.red, inspectOptions); - - switch (typeof value) { - case "string": - return green(quoteString(value)); - case "number": // Numbers are yellow - // Special handling of -0 - return yellow(Object.is(value, -0) ? "-0" : `${value}`); - case "boolean": // booleans are yellow - return yellow(String(value)); - case "undefined": // undefined is dim - return dim(String(value)); - case "symbol": // Symbols are green - return green(maybeQuoteSymbol(value)); - case "bigint": // Bigints are yellow - return yellow(`${value}n`); - case "function": // Function string is cyan - return cyan(inspectFunction(value, ctx)); - case "object": // null is bold - if (value === null) { - return bold("null"); - } - - if (ctx.has(value)) { - // Circular string is cyan - return cyan("[Circular]"); - } - - return inspectObject(value, ctx, level, inspectOptions); - default: - // Not implemented is red - return red("[Not Implemented]"); - } - } - - // We can match Node's quoting behavior exactly by swapping the double quote and - // single quote in this array. That would give preference to single quotes. - // However, we prefer double quotes as the default. - const QUOTES = ['"', "'", "`"]; - - /** Surround the string in quotes. - * - * The quote symbol is chosen by taking the first of the `QUOTES` array which - * does not occur in the string. If they all occur, settle with `QUOTES[0]`. - * - * Insert a backslash before any occurrence of the chosen quote symbol and - * before any backslash. - */ - function quoteString(string) { - const quote = QUOTES.find((c) => !string.includes(c)) ?? QUOTES[0]; - const escapePattern = new RegExp(`(?=[${quote}\\\\])`, "g"); - string = string.replace(escapePattern, "\\"); - string = replaceEscapeSequences(string); - return `${quote}${string}${quote}`; - } - - // Replace escape sequences that can modify output. - function replaceEscapeSequences(string) { - return string - .replace(/[\b]/g, "\\b") - .replace(/\f/g, "\\f") - .replace(/\n/g, "\\n") - .replace(/\r/g, "\\r") - .replace(/\t/g, "\\t") - .replace(/\v/g, "\\v") - .replace( - // deno-lint-ignore no-control-regex - /[\x00-\x1f\x7f-\x9f]/g, - (c) => "\\x" + c.charCodeAt(0).toString(16).padStart(2, "0"), - ); - } - - // Surround a string with quotes when it is required (e.g the string not a valid identifier). - function maybeQuoteString(string) { - if (/^[a-zA-Z_][a-zA-Z_0-9]*$/.test(string)) { - return replaceEscapeSequences(string); - } - - return quoteString(string); - } - - // Surround a symbol's description in quotes when it is required (e.g the description has non printable characters). - function maybeQuoteSymbol(symbol) { - if (symbol.description === undefined) { - return symbol.toString(); - } - - if (/^[a-zA-Z_][a-zA-Z_.0-9]*$/.test(symbol.description)) { - return symbol.toString(); - } - - return `Symbol(${quoteString(symbol.description)})`; - } - - // Print strings when they are inside of arrays or objects with quotes - function inspectValueWithQuotes( - value, - ctx, - level, - inspectOptions, - ) { - const green = maybeColor(colors.green, inspectOptions); - switch (typeof value) { - case "string": { - const trunc = value.length > STR_ABBREVIATE_SIZE - ? value.slice(0, STR_ABBREVIATE_SIZE) + "..." - : value; - return green(quoteString(trunc)); // Quoted strings are green - } - default: - return inspectValue(value, ctx, level, inspectOptions); - } - } - - function inspectArray( - value, - ctx, - level, - inspectOptions, - ) { - const dim = maybeColor(colors.dim, inspectOptions); - const options = { - typeName: "Array", - displayName: "", - delims: ["[", "]"], - entryHandler: (entry, ctx, level, inspectOptions, next) => { - const [index, val] = entry; - let i = index; - if (!value.hasOwnProperty(i)) { - i++; - while (!value.hasOwnProperty(i) && i < value.length) { - next(); - i++; - } - const emptyItems = i - index; - const ending = emptyItems > 1 ? "s" : ""; - return dim(`<${emptyItems} empty item${ending}>`); - } else { - return inspectValueWithQuotes(val, ctx, level, inspectOptions); - } - }, - group: inspectOptions.compact, - sort: false, - }; - return inspectIterable(value, ctx, level, options, inspectOptions); - } - - function inspectTypedArray( - typedArrayName, - value, - ctx, - level, - inspectOptions, - ) { - const valueLength = value.length; - const options = { - typeName: typedArrayName, - displayName: `${typedArrayName}(${valueLength})`, - delims: ["[", "]"], - entryHandler: (entry, ctx, level, inspectOptions) => { - const val = entry[1]; - return inspectValueWithQuotes(val, ctx, level + 1, inspectOptions); - }, - group: inspectOptions.compact, - sort: false, - }; - return inspectIterable(value, ctx, level, options, inspectOptions); - } - - function inspectSet( - value, - ctx, - level, - inspectOptions, - ) { - const options = { - typeName: "Set", - displayName: "Set", - delims: ["{", "}"], - entryHandler: (entry, ctx, level, inspectOptions) => { - const val = entry[1]; - return inspectValueWithQuotes(val, ctx, level + 1, inspectOptions); - }, - group: false, - sort: inspectOptions.sorted, - }; - return inspectIterable(value, ctx, level, options, inspectOptions); - } - - function inspectMap( - value, - ctx, - level, - inspectOptions, - ) { - const options = { - typeName: "Map", - displayName: "Map", - delims: ["{", "}"], - entryHandler: (entry, ctx, level, inspectOptions) => { - const [key, val] = entry; - return `${ - inspectValueWithQuotes( - key, - ctx, - level + 1, - inspectOptions, - ) - } => ${inspectValueWithQuotes(val, ctx, level + 1, inspectOptions)}`; - }, - group: false, - sort: inspectOptions.sorted, - }; - return inspectIterable( - value, - ctx, - level, - options, - inspectOptions, - ); - } - - function inspectWeakSet(inspectOptions) { - const cyan = maybeColor(colors.cyan, inspectOptions); - return `WeakSet { ${cyan("[items unknown]")} }`; // as seen in Node, with cyan color - } - - function inspectWeakMap(inspectOptions) { - const cyan = maybeColor(colors.cyan, inspectOptions); - return `WeakMap { ${cyan("[items unknown]")} }`; // as seen in Node, with cyan color - } - - function inspectDate(value, inspectOptions) { - // without quotes, ISO format, in magenta like before - const magenta = maybeColor(colors.magenta, inspectOptions); - return magenta(isInvalidDate(value) ? "Invalid Date" : value.toISOString()); - } - - function inspectRegExp(value, inspectOptions) { - const red = maybeColor(colors.red, inspectOptions); - return red(value.toString()); // RegExps are red - } - - function inspectStringObject(value, inspectOptions) { - const cyan = maybeColor(colors.cyan, inspectOptions); - return cyan(`[String: "${value.toString()}"]`); // wrappers are in cyan - } - - function inspectBooleanObject(value, inspectOptions) { - const cyan = maybeColor(colors.cyan, inspectOptions); - return cyan(`[Boolean: ${value.toString()}]`); // wrappers are in cyan - } - - function inspectNumberObject(value, inspectOptions) { - const cyan = maybeColor(colors.cyan, inspectOptions); - return cyan(`[Number: ${value.toString()}]`); // wrappers are in cyan - } - - const PromiseState = { - Pending: 0, - Fulfilled: 1, - Rejected: 2, - }; - - function inspectPromise( - value, - ctx, - level, - inspectOptions, - ) { - const cyan = maybeColor(colors.cyan, inspectOptions); - const red = maybeColor(colors.red, inspectOptions); - - const [state, result] = core.getPromiseDetails(value); - - if (state === PromiseState.Pending) { - return `Promise { ${cyan("")} }`; - } - - const prefix = state === PromiseState.Fulfilled - ? "" - : `${red("")} `; - - const str = `${prefix}${ - inspectValueWithQuotes( - result, - ctx, - level + 1, - inspectOptions, - ) - }`; - - if (str.length + PROMISE_STRING_BASE_LENGTH > LINE_BREAKING_LENGTH) { - return `Promise {\n${DEFAULT_INDENT.repeat(level + 1)}${str}\n}`; - } - - return `Promise { ${str} }`; - } - - function inspectProxy( - targetAndHandler, - ctx, - level, - inspectOptions, - ) { - return `Proxy ${ - inspectArray(targetAndHandler, ctx, level, inspectOptions) - }`; - } - - function inspectRawObject( - value, - ctx, - level, - inspectOptions, - ) { - const cyan = maybeColor(colors.cyan, inspectOptions); - - if (level >= inspectOptions.depth) { - return cyan("[Object]"); // wrappers are in cyan - } - ctx.add(value); - - let baseString; - - let shouldShowDisplayName = false; - let displayName = value[ - Symbol.toStringTag - ]; - if (!displayName) { - displayName = getClassInstanceName(value); - } - if ( - displayName && displayName !== "Object" && displayName !== "anonymous" - ) { - shouldShowDisplayName = true; - } - - const entries = []; - const stringKeys = Object.keys(value); - const symbolKeys = Object.getOwnPropertySymbols(value); - if (inspectOptions.sorted) { - stringKeys.sort(); - symbolKeys.sort((s1, s2) => - (s1.description ?? "").localeCompare(s2.description ?? "") - ); - } - - const red = maybeColor(colors.red, inspectOptions); - - for (const key of stringKeys) { - if (inspectOptions.getters) { - let propertyValue; - let error = null; - try { - propertyValue = value[key]; - } catch (error_) { - error = error_; - } - const inspectedValue = error == null - ? inspectValueWithQuotes( - propertyValue, - ctx, - level + 1, - inspectOptions, - ) - : red(`[Thrown ${error.name}: ${error.message}]`); - entries.push(`${maybeQuoteString(key)}: ${inspectedValue}`); - } else { - const descriptor = Object.getOwnPropertyDescriptor(value, key); - if (descriptor.get !== undefined && descriptor.set !== undefined) { - entries.push(`${maybeQuoteString(key)}: [Getter/Setter]`); - } else if (descriptor.get !== undefined) { - entries.push(`${maybeQuoteString(key)}: [Getter]`); - } else { - entries.push( - `${maybeQuoteString(key)}: ${ - inspectValueWithQuotes(value[key], ctx, level + 1, inspectOptions) - }`, - ); - } - } - } - - for (const key of symbolKeys) { - if (inspectOptions.getters) { - let propertyValue; - let error; - try { - propertyValue = value[key]; - } catch (error_) { - error = error_; - } - const inspectedValue = error == null - ? inspectValueWithQuotes( - propertyValue, - ctx, - level + 1, - inspectOptions, - ) - : red(`Thrown ${error.name}: ${error.message}`); - entries.push(`[${maybeQuoteSymbol(key)}]: ${inspectedValue}`); - } else { - const descriptor = Object.getOwnPropertyDescriptor(value, key); - if (descriptor.get !== undefined && descriptor.set !== undefined) { - entries.push(`[${maybeQuoteSymbol(key)}]: [Getter/Setter]`); - } else if (descriptor.get !== undefined) { - entries.push(`[${maybeQuoteSymbol(key)}]: [Getter]`); - } else { - entries.push( - `[${maybeQuoteSymbol(key)}]: ${ - inspectValueWithQuotes(value[key], ctx, level + 1, inspectOptions) - }`, - ); - } - } - } - - // Making sure color codes are ignored when calculating the total length - const totalLength = entries.length + level + - colors.stripColor(entries.join("")).length; - - ctx.delete(value); - - if (entries.length === 0) { - baseString = "{}"; - } else if (totalLength > LINE_BREAKING_LENGTH || !inspectOptions.compact) { - const entryIndent = DEFAULT_INDENT.repeat(level + 1); - const closingIndent = DEFAULT_INDENT.repeat(level); - baseString = `{\n${entryIndent}${entries.join(`,\n${entryIndent}`)}${ - inspectOptions.trailingComma ? "," : "" - }\n${closingIndent}}`; - } else { - baseString = `{ ${entries.join(", ")} }`; - } - - if (shouldShowDisplayName) { - baseString = `${displayName} ${baseString}`; - } - - return baseString; - } - - function inspectObject( - value, - consoleContext, - level, - inspectOptions, - ) { - if (customInspect in value && typeof value[customInspect] === "function") { - try { - return String(value[customInspect]()); - } catch { - // pass - } - } - // This non-unique symbol is used to support op_crates, ie. - // in op_crates/web we don't want to depend on unique "Deno.customInspect" - // symbol defined in the public API. Internal only, shouldn't be used - // by users. - const nonUniqueCustomInspect = Symbol.for("Deno.customInspect"); - if ( - nonUniqueCustomInspect in value && - typeof value[nonUniqueCustomInspect] === "function" - ) { - try { - return String(value[nonUniqueCustomInspect]()); - } catch { - // pass - } - } - if (value instanceof Error) { - return String(value.stack); - } else if (Array.isArray(value)) { - return inspectArray(value, consoleContext, level, inspectOptions); - } else if (value instanceof Number) { - return inspectNumberObject(value, inspectOptions); - } else if (value instanceof Boolean) { - return inspectBooleanObject(value, inspectOptions); - } else if (value instanceof String) { - return inspectStringObject(value, inspectOptions); - } else if (value instanceof Promise) { - return inspectPromise(value, consoleContext, level, inspectOptions); - } else if (value instanceof RegExp) { - return inspectRegExp(value, inspectOptions); - } else if (value instanceof Date) { - return inspectDate(value, inspectOptions); - } else if (value instanceof Set) { - return inspectSet(value, consoleContext, level, inspectOptions); - } else if (value instanceof Map) { - return inspectMap(value, consoleContext, level, inspectOptions); - } else if (value instanceof WeakSet) { - return inspectWeakSet(inspectOptions); - } else if (value instanceof WeakMap) { - return inspectWeakMap(inspectOptions); - } else if (isTypedArray(value)) { - return inspectTypedArray( - Object.getPrototypeOf(value).constructor.name, - value, - consoleContext, - level, - inspectOptions, - ); - } else { - // Otherwise, default object formatting - return inspectRawObject(value, consoleContext, level, inspectOptions); - } - } - - const colorKeywords = new Map([ - ["black", "#000000"], - ["silver", "#c0c0c0"], - ["gray", "#808080"], - ["white", "#ffffff"], - ["maroon", "#800000"], - ["red", "#ff0000"], - ["purple", "#800080"], - ["fuchsia", "#ff00ff"], - ["green", "#008000"], - ["lime", "#00ff00"], - ["olive", "#808000"], - ["yellow", "#ffff00"], - ["navy", "#000080"], - ["blue", "#0000ff"], - ["teal", "#008080"], - ["aqua", "#00ffff"], - ["orange", "#ffa500"], - ["aliceblue", "#f0f8ff"], - ["antiquewhite", "#faebd7"], - ["aquamarine", "#7fffd4"], - ["azure", "#f0ffff"], - ["beige", "#f5f5dc"], - ["bisque", "#ffe4c4"], - ["blanchedalmond", "#ffebcd"], - ["blueviolet", "#8a2be2"], - ["brown", "#a52a2a"], - ["burlywood", "#deb887"], - ["cadetblue", "#5f9ea0"], - ["chartreuse", "#7fff00"], - ["chocolate", "#d2691e"], - ["coral", "#ff7f50"], - ["cornflowerblue", "#6495ed"], - ["cornsilk", "#fff8dc"], - ["crimson", "#dc143c"], - ["cyan", "#00ffff"], - ["darkblue", "#00008b"], - ["darkcyan", "#008b8b"], - ["darkgoldenrod", "#b8860b"], - ["darkgray", "#a9a9a9"], - ["darkgreen", "#006400"], - ["darkgrey", "#a9a9a9"], - ["darkkhaki", "#bdb76b"], - ["darkmagenta", "#8b008b"], - ["darkolivegreen", "#556b2f"], - ["darkorange", "#ff8c00"], - ["darkorchid", "#9932cc"], - ["darkred", "#8b0000"], - ["darksalmon", "#e9967a"], - ["darkseagreen", "#8fbc8f"], - ["darkslateblue", "#483d8b"], - ["darkslategray", "#2f4f4f"], - ["darkslategrey", "#2f4f4f"], - ["darkturquoise", "#00ced1"], - ["darkviolet", "#9400d3"], - ["deeppink", "#ff1493"], - ["deepskyblue", "#00bfff"], - ["dimgray", "#696969"], - ["dimgrey", "#696969"], - ["dodgerblue", "#1e90ff"], - ["firebrick", "#b22222"], - ["floralwhite", "#fffaf0"], - ["forestgreen", "#228b22"], - ["gainsboro", "#dcdcdc"], - ["ghostwhite", "#f8f8ff"], - ["gold", "#ffd700"], - ["goldenrod", "#daa520"], - ["greenyellow", "#adff2f"], - ["grey", "#808080"], - ["honeydew", "#f0fff0"], - ["hotpink", "#ff69b4"], - ["indianred", "#cd5c5c"], - ["indigo", "#4b0082"], - ["ivory", "#fffff0"], - ["khaki", "#f0e68c"], - ["lavender", "#e6e6fa"], - ["lavenderblush", "#fff0f5"], - ["lawngreen", "#7cfc00"], - ["lemonchiffon", "#fffacd"], - ["lightblue", "#add8e6"], - ["lightcoral", "#f08080"], - ["lightcyan", "#e0ffff"], - ["lightgoldenrodyellow", "#fafad2"], - ["lightgray", "#d3d3d3"], - ["lightgreen", "#90ee90"], - ["lightgrey", "#d3d3d3"], - ["lightpink", "#ffb6c1"], - ["lightsalmon", "#ffa07a"], - ["lightseagreen", "#20b2aa"], - ["lightskyblue", "#87cefa"], - ["lightslategray", "#778899"], - ["lightslategrey", "#778899"], - ["lightsteelblue", "#b0c4de"], - ["lightyellow", "#ffffe0"], - ["limegreen", "#32cd32"], - ["linen", "#faf0e6"], - ["magenta", "#ff00ff"], - ["mediumaquamarine", "#66cdaa"], - ["mediumblue", "#0000cd"], - ["mediumorchid", "#ba55d3"], - ["mediumpurple", "#9370db"], - ["mediumseagreen", "#3cb371"], - ["mediumslateblue", "#7b68ee"], - ["mediumspringgreen", "#00fa9a"], - ["mediumturquoise", "#48d1cc"], - ["mediumvioletred", "#c71585"], - ["midnightblue", "#191970"], - ["mintcream", "#f5fffa"], - ["mistyrose", "#ffe4e1"], - ["moccasin", "#ffe4b5"], - ["navajowhite", "#ffdead"], - ["oldlace", "#fdf5e6"], - ["olivedrab", "#6b8e23"], - ["orangered", "#ff4500"], - ["orchid", "#da70d6"], - ["palegoldenrod", "#eee8aa"], - ["palegreen", "#98fb98"], - ["paleturquoise", "#afeeee"], - ["palevioletred", "#db7093"], - ["papayawhip", "#ffefd5"], - ["peachpuff", "#ffdab9"], - ["peru", "#cd853f"], - ["pink", "#ffc0cb"], - ["plum", "#dda0dd"], - ["powderblue", "#b0e0e6"], - ["rosybrown", "#bc8f8f"], - ["royalblue", "#4169e1"], - ["saddlebrown", "#8b4513"], - ["salmon", "#fa8072"], - ["sandybrown", "#f4a460"], - ["seagreen", "#2e8b57"], - ["seashell", "#fff5ee"], - ["sienna", "#a0522d"], - ["skyblue", "#87ceeb"], - ["slateblue", "#6a5acd"], - ["slategray", "#708090"], - ["slategrey", "#708090"], - ["snow", "#fffafa"], - ["springgreen", "#00ff7f"], - ["steelblue", "#4682b4"], - ["tan", "#d2b48c"], - ["thistle", "#d8bfd8"], - ["tomato", "#ff6347"], - ["turquoise", "#40e0d0"], - ["violet", "#ee82ee"], - ["wheat", "#f5deb3"], - ["whitesmoke", "#f5f5f5"], - ["yellowgreen", "#9acd32"], - ["rebeccapurple", "#663399"], - ]); - - function parseCssColor(colorString) { - if (colorKeywords.has(colorString)) { - colorString = colorKeywords.get(colorString); - } - // deno-fmt-ignore - const hashMatch = colorString.match(/^#([\dA-Fa-f]{2})([\dA-Fa-f]{2})([\dA-Fa-f]{2})([\dA-Fa-f]{2})?$/); - if (hashMatch != null) { - return [ - Number(`0x${hashMatch[1]}`), - Number(`0x${hashMatch[2]}`), - Number(`0x${hashMatch[3]}`), - ]; - } - // deno-fmt-ignore - const smallHashMatch = colorString.match(/^#([\dA-Fa-f])([\dA-Fa-f])([\dA-Fa-f])([\dA-Fa-f])?$/); - if (smallHashMatch != null) { - return [ - Number(`0x${smallHashMatch[1]}0`), - Number(`0x${smallHashMatch[2]}0`), - Number(`0x${smallHashMatch[3]}0`), - ]; - } - // deno-fmt-ignore - const rgbMatch = colorString.match(/^rgba?\(\s*([+\-]?\d*\.?\d+)\s*,\s*([+\-]?\d*\.?\d+)\s*,\s*([+\-]?\d*\.?\d+)\s*(,\s*([+\-]?\d*\.?\d+)\s*)?\)$/); - if (rgbMatch != null) { - return [ - Math.round(Math.max(0, Math.min(255, Number(rgbMatch[1])))), - Math.round(Math.max(0, Math.min(255, Number(rgbMatch[2])))), - Math.round(Math.max(0, Math.min(255, Number(rgbMatch[3])))), - ]; - } - // deno-fmt-ignore - const hslMatch = colorString.match(/^hsla?\(\s*([+\-]?\d*\.?\d+)\s*,\s*([+\-]?\d*\.?\d+)%\s*,\s*([+\-]?\d*\.?\d+)%\s*(,\s*([+\-]?\d*\.?\d+)\s*)?\)$/); - if (hslMatch != null) { - // https://www.rapidtables.com/convert/color/hsl-to-rgb.html - let h = Number(hslMatch[1]) % 360; - if (h < 0) { - h += 360; - } - const s = Math.max(0, Math.min(100, Number(hslMatch[2]))) / 100; - const l = Math.max(0, Math.min(100, Number(hslMatch[3]))) / 100; - const c = (1 - Math.abs(2 * l - 1)) * s; - const x = c * (1 - Math.abs((h / 60) % 2 - 1)); - const m = l - c / 2; - let r_; - let g_; - let b_; - if (h < 60) { - [r_, g_, b_] = [c, x, 0]; - } else if (h < 120) { - [r_, g_, b_] = [x, c, 0]; - } else if (h < 180) { - [r_, g_, b_] = [0, c, x]; - } else if (h < 240) { - [r_, g_, b_] = [0, x, c]; - } else if (h < 300) { - [r_, g_, b_] = [x, 0, c]; - } else { - [r_, g_, b_] = [c, 0, x]; - } - return [ - Math.round((r_ + m) * 255), - Math.round((g_ + m) * 255), - Math.round((b_ + m) * 255), - ]; - } - return null; - } - - function getDefaultCss() { - return { - backgroundColor: null, - color: null, - fontWeight: null, - fontStyle: null, - textDecorationColor: null, - textDecorationLine: [], - }; - } - - function parseCss(cssString) { - const css = getDefaultCss(); - - const rawEntries = []; - let inValue = false; - let currentKey = null; - let parenthesesDepth = 0; - let currentPart = ""; - for (let i = 0; i < cssString.length; i++) { - const c = cssString[i]; - if (c == "(") { - parenthesesDepth++; - } else if (parenthesesDepth > 0) { - if (c == ")") { - parenthesesDepth--; - } - } else if (inValue) { - if (c == ";") { - const value = currentPart.trim(); - if (value != "") { - rawEntries.push([currentKey, value]); - } - currentKey = null; - currentPart = ""; - inValue = false; - continue; - } - } else if (c == ":") { - currentKey = currentPart.trim(); - currentPart = ""; - inValue = true; - continue; - } - currentPart += c; - } - if (inValue && parenthesesDepth == 0) { - const value = currentPart.trim(); - if (value != "") { - rawEntries.push([currentKey, value]); - } - currentKey = null; - currentPart = ""; - } - - for (const [key, value] of rawEntries) { - if (key == "background-color") { - const color = parseCssColor(value); - if (color != null) { - css.backgroundColor = color; - } - } else if (key == "color") { - const color = parseCssColor(value); - if (color != null) { - css.color = color; - } - } else if (key == "font-weight") { - if (value == "bold") { - css.fontWeight = value; - } - } else if (key == "font-style") { - if (["italic", "oblique", "oblique 14deg"].includes(value)) { - css.fontStyle = "italic"; - } - } else if (key == "text-decoration-line") { - css.textDecorationLine = []; - for (const lineType of value.split(/\s+/g)) { - if (["line-through", "overline", "underline"].includes(lineType)) { - css.textDecorationLine.push(lineType); - } - } - } else if (key == "text-decoration-color") { - const color = parseCssColor(value); - if (color != null) { - css.textDecorationColor = color; - } - } else if (key == "text-decoration") { - css.textDecorationColor = null; - css.textDecorationLine = []; - for (const arg of value.split(/\s+/g)) { - const maybeColor = parseCssColor(arg); - if (maybeColor != null) { - css.textDecorationColor = maybeColor; - } else if (["line-through", "overline", "underline"].includes(arg)) { - css.textDecorationLine.push(arg); - } - } - } - } - - return css; - } - - function colorEquals(color1, color2) { - return color1?.[0] == color2?.[0] && color1?.[1] == color2?.[1] && - color1?.[2] == color2?.[2]; - } - - function cssToAnsi(css, prevCss = null) { - prevCss = prevCss ?? getDefaultCss(); - let ansi = ""; - if (!colorEquals(css.backgroundColor, prevCss.backgroundColor)) { - if (css.backgroundColor != null) { - const [r, g, b] = css.backgroundColor; - ansi += `\x1b[48;2;${r};${g};${b}m`; - } else { - ansi += "\x1b[49m"; - } - } - if (!colorEquals(css.color, prevCss.color)) { - if (css.color != null) { - const [r, g, b] = css.color; - ansi += `\x1b[38;2;${r};${g};${b}m`; - } else { - ansi += "\x1b[39m"; - } - } - if (css.fontWeight != prevCss.fontWeight) { - if (css.fontWeight == "bold") { - ansi += `\x1b[1m`; - } else { - ansi += "\x1b[22m"; - } - } - if (css.fontStyle != prevCss.fontStyle) { - if (css.fontStyle == "italic") { - ansi += `\x1b[3m`; - } else { - ansi += "\x1b[23m"; - } - } - if (!colorEquals(css.textDecorationColor, prevCss.textDecorationColor)) { - if (css.textDecorationColor != null) { - const [r, g, b] = css.textDecorationColor; - ansi += `\x1b[58;2;${r};${g};${b}m`; - } else { - ansi += "\x1b[59m"; - } - } - if ( - css.textDecorationLine.includes("line-through") != - prevCss.textDecorationLine.includes("line-through") - ) { - if (css.textDecorationLine.includes("line-through")) { - ansi += "\x1b[9m"; - } else { - ansi += "\x1b[29m"; - } - } - if ( - css.textDecorationLine.includes("overline") != - prevCss.textDecorationLine.includes("overline") - ) { - if (css.textDecorationLine.includes("overline")) { - ansi += "\x1b[53m"; - } else { - ansi += "\x1b[55m"; - } - } - if ( - css.textDecorationLine.includes("underline") != - prevCss.textDecorationLine.includes("underline") - ) { - if (css.textDecorationLine.includes("underline")) { - ansi += "\x1b[4m"; - } else { - ansi += "\x1b[24m"; - } - } - return ansi; - } - - function inspectArgs(args, inspectOptions = {}) { - const noColor = globalThis.Deno?.noColor ?? true; - const rInspectOptions = { ...DEFAULT_INSPECT_OPTIONS, ...inspectOptions }; - const first = args[0]; - let a = 0; - let string = ""; - - if (typeof first == "string" && args.length > 1) { - a++; - // Index of the first not-yet-appended character. Use this so we only - // have to append to `string` when a substitution occurs / at the end. - let appendedChars = 0; - let usedStyle = false; - let prevCss = null; - for (let i = 0; i < first.length - 1; i++) { - if (first[i] == "%") { - const char = first[++i]; - if (a < args.length) { - let formattedArg = null; - if (char == "s") { - // Format as a string. - formattedArg = String(args[a++]); - } else if (["d", "i"].includes(char)) { - // Format as an integer. - const value = args[a++]; - if (typeof value == "bigint") { - formattedArg = `${value}n`; - } else if (typeof value == "number") { - formattedArg = `${parseInt(String(value))}`; - } else { - formattedArg = "NaN"; - } - } else if (char == "f") { - // Format as a floating point value. - const value = args[a++]; - if (typeof value == "number") { - formattedArg = `${value}`; - } else { - formattedArg = "NaN"; - } - } else if (["O", "o"].includes(char)) { - // Format as an object. - formattedArg = inspectValue( - args[a++], - new Set(), - 0, - rInspectOptions, - ); - } else if (char == "c") { - const value = args[a++]; - if (!noColor) { - const css = parseCss(value); - formattedArg = cssToAnsi(css, prevCss); - if (formattedArg != "") { - usedStyle = true; - prevCss = css; - } - } else { - formattedArg = ""; - } - } - - if (formattedArg != null) { - string += first.slice(appendedChars, i - 1) + formattedArg; - appendedChars = i + 1; - } - } - if (char == "%") { - string += first.slice(appendedChars, i - 1) + "%"; - appendedChars = i + 1; - } - } - } - string += first.slice(appendedChars); - if (usedStyle) { - string += "\x1b[0m"; - } - } - - for (; a < args.length; a++) { - if (a > 0) { - string += " "; - } - if (typeof args[a] == "string") { - string += args[a]; - } else { - // Use default maximum depth for null or undefined arguments. - string += inspectValue(args[a], new Set(), 0, rInspectOptions); - } - } - - if (rInspectOptions.indentLevel > 0) { - const groupIndent = DEFAULT_INDENT.repeat(rInspectOptions.indentLevel); - string = groupIndent + string.replaceAll("\n", `\n${groupIndent}`); - } - - return string; - } - - const countMap = new Map(); - const timerMap = new Map(); - const isConsoleInstance = Symbol("isConsoleInstance"); - - function getConsoleInspectOptions() { - return { - ...DEFAULT_INSPECT_OPTIONS, - colors: !(globalThis.Deno?.noColor ?? false), - }; - } - - class Console { - #printFunc = null; - [isConsoleInstance] = false; - - constructor(printFunc) { - this.#printFunc = printFunc; - this.indentLevel = 0; - this[isConsoleInstance] = true; - - // ref https://console.spec.whatwg.org/#console-namespace - // For historical web-compatibility reasons, the namespace object for - // console must have as its [[Prototype]] an empty object, created as if - // by ObjectCreate(%ObjectPrototype%), instead of %ObjectPrototype%. - const console = Object.create({}); - Object.assign(console, this); - return console; - } - - log = (...args) => { - this.#printFunc( - inspectArgs(args, { - ...getConsoleInspectOptions(), - indentLevel: this.indentLevel, - }) + "\n", - false, - ); - }; - - debug = this.log; - info = this.log; - - dir = (obj, options = {}) => { - this.#printFunc( - inspectArgs([obj], { ...getConsoleInspectOptions(), ...options }) + - "\n", - false, - ); - }; - - dirxml = this.dir; - - warn = (...args) => { - this.#printFunc( - inspectArgs(args, { - ...getConsoleInspectOptions(), - indentLevel: this.indentLevel, - }) + "\n", - true, - ); - }; - - error = this.warn; - - assert = (condition = false, ...args) => { - if (condition) { - return; - } - - if (args.length === 0) { - this.error("Assertion failed"); - return; - } - - const [first, ...rest] = args; - - if (typeof first === "string") { - this.error(`Assertion failed: ${first}`, ...rest); - return; - } - - this.error(`Assertion failed:`, ...args); - }; - - count = (label = "default") => { - label = String(label); - - if (countMap.has(label)) { - const current = countMap.get(label) || 0; - countMap.set(label, current + 1); - } else { - countMap.set(label, 1); - } - - this.info(`${label}: ${countMap.get(label)}`); - }; - - countReset = (label = "default") => { - label = String(label); - - if (countMap.has(label)) { - countMap.set(label, 0); - } else { - this.warn(`Count for '${label}' does not exist`); - } - }; - - table = (data, properties) => { - if (properties !== undefined && !Array.isArray(properties)) { - throw new Error( - "The 'properties' argument must be of type Array. " + - "Received type string", - ); - } - - if (data === null || typeof data !== "object") { - return this.log(data); - } - - const objectValues = {}; - const indexKeys = []; - const values = []; - - const stringifyValue = (value) => - inspectValueWithQuotes(value, new Set(), 0, { - ...DEFAULT_INSPECT_OPTIONS, - depth: 1, - }); - const toTable = (header, body) => this.log(cliTable(header, body)); - const createColumn = (value, shift) => [ - ...(shift ? [...new Array(shift)].map(() => "") : []), - stringifyValue(value), - ]; - - let resultData; - const isSet = data instanceof Set; - const isMap = data instanceof Map; - const valuesKey = "Values"; - const indexKey = isSet || isMap ? "(iter idx)" : "(idx)"; - - if (data instanceof Set) { - resultData = [...data]; - } else if (data instanceof Map) { - let idx = 0; - resultData = {}; - - data.forEach((v, k) => { - resultData[idx] = { Key: k, Values: v }; - idx++; - }); - } else { - resultData = data; - } - - let hasPrimitives = false; - Object.keys(resultData).forEach((k, idx) => { - const value = resultData[k]; - const primitive = value === null || - (typeof value !== "function" && typeof value !== "object"); - if (properties === undefined && primitive) { - hasPrimitives = true; - values.push(stringifyValue(value)); - } else { - const valueObj = value || {}; - const keys = properties || Object.keys(valueObj); - for (const k of keys) { - if (primitive || !valueObj.hasOwnProperty(k)) { - if (objectValues[k]) { - // fill with blanks for idx to avoid misplacing from later values - objectValues[k].push(""); - } - } else { - if (objectValues[k]) { - objectValues[k].push(stringifyValue(valueObj[k])); - } else { - objectValues[k] = createColumn(valueObj[k], idx); - } - } - } - values.push(""); - } - - indexKeys.push(k); - }); - - const headerKeys = Object.keys(objectValues); - const bodyValues = Object.values(objectValues); - const header = [ - indexKey, - ...(properties || - [...headerKeys, !isMap && hasPrimitives && valuesKey]), - ].filter(Boolean); - const body = [indexKeys, ...bodyValues, values]; - - toTable(header, body); - }; - - time = (label = "default") => { - label = String(label); - - if (timerMap.has(label)) { - this.warn(`Timer '${label}' already exists`); - return; - } - - timerMap.set(label, Date.now()); - }; - - timeLog = (label = "default", ...args) => { - label = String(label); - - if (!timerMap.has(label)) { - this.warn(`Timer '${label}' does not exists`); - return; - } - - const startTime = timerMap.get(label); - const duration = Date.now() - startTime; - - this.info(`${label}: ${duration}ms`, ...args); - }; - - timeEnd = (label = "default") => { - label = String(label); - - if (!timerMap.has(label)) { - this.warn(`Timer '${label}' does not exists`); - return; - } - - const startTime = timerMap.get(label); - timerMap.delete(label); - const duration = Date.now() - startTime; - - this.info(`${label}: ${duration}ms`); - }; - - group = (...label) => { - if (label.length > 0) { - this.log(...label); - } - this.indentLevel += 2; - }; - - groupCollapsed = this.group; - - groupEnd = () => { - if (this.indentLevel > 0) { - this.indentLevel -= 2; - } - }; - - clear = () => { - this.indentLevel = 0; - this.#printFunc(CSI.kClear, false); - this.#printFunc(CSI.kClearScreenDown, false); - }; - - trace = (...args) => { - const message = inspectArgs( - args, - { ...getConsoleInspectOptions(), indentLevel: 0 }, - ); - const err = { - name: "Trace", - message, - }; - Error.captureStackTrace(err, this.trace); - this.error(err.stack); - }; - - static [Symbol.hasInstance](instance) { - return instance[isConsoleInstance]; - } - } - - const customInspect = Symbol("Deno.customInspect"); - - function inspect( - value, - inspectOptions = {}, - ) { - return inspectValue(value, new Set(), 0, { - ...DEFAULT_INSPECT_OPTIONS, - ...inspectOptions, - // TODO(nayeemrmn): Indent level is not supported. - indentLevel: 0, - }); - } - - // Expose these fields to internalObject for tests. - exposeForTest("Console", Console); - exposeForTest("cssToAnsi", cssToAnsi); - exposeForTest("inspectArgs", inspectArgs); - exposeForTest("parseCss", parseCss); - exposeForTest("parseCssColor", parseCssColor); - - window.__bootstrap.console = { - CSI, - inspectArgs, - Console, - customInspect, - inspect, - }; -})(this); diff --git a/cli/rt/06_util.js b/cli/rt/06_util.js deleted file mode 100644 index f4804c519..000000000 --- a/cli/rt/06_util.js +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const { build } = window.__bootstrap.build; - const internals = window.__bootstrap.internals; - let logDebug = false; - let logSource = "JS"; - - function setLogDebug(debug, source) { - logDebug = debug; - if (source) { - logSource = source; - } - } - - function log(...args) { - if (logDebug) { - // if we destructure `console` off `globalThis` too early, we don't bind to - // the right console, therefore we don't log anything out. - globalThis.console.log(`DEBUG ${logSource} -`, ...args); - } - } - - class AssertionError extends Error { - constructor(msg) { - super(msg); - this.name = "AssertionError"; - } - } - - function assert(cond, msg = "Assertion failed.") { - if (!cond) { - throw new AssertionError(msg); - } - } - - function createResolvable() { - let resolve; - let reject; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - promise.resolve = resolve; - promise.reject = reject; - return promise; - } - - function immutableDefine( - o, - p, - value, - ) { - Object.defineProperty(o, p, { - value, - configurable: false, - writable: false, - }); - } - - // Keep in sync with `fromFileUrl()` in `std/path/win32.ts`. - function pathFromURLWin32(url) { - let path = decodeURIComponent( - url.pathname - .replace(/^\/*([A-Za-z]:)(\/|$)/, "$1/") - .replace(/\//g, "\\") - .replace(/%(?![0-9A-Fa-f]{2})/g, "%25"), - ); - if (url.hostname != "") { - // Note: The `URL` implementation guarantees that the drive letter and - // hostname are mutually exclusive. Otherwise it would not have been valid - // to append the hostname and path like this. - path = `\\\\${url.hostname}${path}`; - } - return path; - } - - // Keep in sync with `fromFileUrl()` in `std/path/posix.ts`. - function pathFromURLPosix(url) { - if (url.hostname !== "") { - throw new TypeError(`Host must be empty.`); - } - - return decodeURIComponent( - url.pathname.replace(/%(?![0-9A-Fa-f]{2})/g, "%25"), - ); - } - - function pathFromURL(pathOrUrl) { - if (pathOrUrl instanceof URL) { - if (pathOrUrl.protocol != "file:") { - throw new TypeError("Must be a file URL."); - } - - return build.os == "windows" - ? pathFromURLWin32(pathOrUrl) - : pathFromURLPosix(pathOrUrl); - } - return pathOrUrl; - } - - internals.exposeForTest("pathFromURL", pathFromURL); - - function writable(value) { - return { - value, - writable: true, - enumerable: true, - configurable: true, - }; - } - - function nonEnumerable(value) { - return { - value, - writable: true, - configurable: true, - }; - } - - function readOnly(value) { - return { - value, - enumerable: true, - }; - } - - function getterOnly(getter) { - return { - get: getter, - enumerable: true, - }; - } - - window.__bootstrap.util = { - log, - setLogDebug, - createResolvable, - assert, - AssertionError, - immutableDefine, - pathFromURL, - writable, - nonEnumerable, - readOnly, - getterOnly, - }; -})(this); diff --git a/cli/rt/10_dispatch_minimal.js b/cli/rt/10_dispatch_minimal.js deleted file mode 100644 index dceb23e5f..000000000 --- a/cli/rt/10_dispatch_minimal.js +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - const util = window.__bootstrap.util; - - // Using an object without a prototype because `Map` was causing GC problems. - const promiseTableMin = Object.create(null); - - const decoder = new TextDecoder(); - - // Note it's important that promiseId starts at 1 instead of 0, because sync - // messages are indicated with promiseId 0. If we ever add wrap around logic for - // overflows, this should be taken into account. - let _nextPromiseId = 1; - - function nextPromiseId() { - return _nextPromiseId++; - } - - function recordFromBufMinimal(ui8) { - const headerLen = 12; - const header = ui8.subarray(0, headerLen); - const buf32 = new Int32Array( - header.buffer, - header.byteOffset, - header.byteLength / 4, - ); - const promiseId = buf32[0]; - const arg = buf32[1]; - const result = buf32[2]; - let err; - - if (arg < 0) { - err = { - className: decoder.decode(ui8.subarray(headerLen, headerLen + result)), - message: decoder.decode(ui8.subarray(headerLen + result)), - }; - } else if (ui8.length != 12) { - throw new TypeError("Malformed response message"); - } - - return { - promiseId, - arg, - result, - err, - }; - } - - function unwrapResponse(res) { - if (res.err != null) { - const ErrorClass = core.getErrorClass(res.err.className); - if (!ErrorClass) { - throw new Error( - `Unregistered error class: "${res.err.className}"\n ${res.err.message}\n Classes of errors returned from ops should be registered via Deno.core.registerErrorClass().`, - ); - } - throw new ErrorClass(res.err.message); - } - return res.result; - } - - const scratch32 = new Int32Array(3); - const scratchBytes = new Uint8Array( - scratch32.buffer, - scratch32.byteOffset, - scratch32.byteLength, - ); - util.assert(scratchBytes.byteLength === scratch32.length * 4); - - function asyncMsgFromRust(ui8) { - const record = recordFromBufMinimal(ui8); - const { promiseId } = record; - const promise = promiseTableMin[promiseId]; - delete promiseTableMin[promiseId]; - util.assert(promise); - promise.resolve(record); - } - - async function sendAsync(opName, arg, zeroCopy) { - const promiseId = nextPromiseId(); // AKA cmdId - scratch32[0] = promiseId; - scratch32[1] = arg; - scratch32[2] = 0; // result - const promise = util.createResolvable(); - const buf = core.dispatchByName(opName, scratchBytes, zeroCopy); - if (buf != null) { - const record = recordFromBufMinimal(buf); - // Sync result. - promise.resolve(record); - } else { - // Async result. - promiseTableMin[promiseId] = promise; - } - - const res = await promise; - return unwrapResponse(res); - } - - function sendSync(opName, arg, zeroCopy) { - scratch32[0] = 0; // promiseId 0 indicates sync - scratch32[1] = arg; - const res = core.dispatchByName(opName, scratchBytes, zeroCopy); - const resRecord = recordFromBufMinimal(res); - return unwrapResponse(resRecord); - } - - window.__bootstrap.dispatchMinimal = { - asyncMsgFromRust, - sendSync, - sendAsync, - }; -})(this); diff --git a/cli/rt/11_timers.js b/cli/rt/11_timers.js deleted file mode 100644 index 5a59844a3..000000000 --- a/cli/rt/11_timers.js +++ /dev/null @@ -1,557 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const assert = window.__bootstrap.util.assert; - const core = window.Deno.core; - const { sendSync } = window.__bootstrap.dispatchMinimal; - - function opStopGlobalTimer() { - core.jsonOpSync("op_global_timer_stop"); - } - - function opStartGlobalTimer(timeout) { - return core.jsonOpSync("op_global_timer_start", { timeout }); - } - - async function opWaitGlobalTimer() { - await core.jsonOpAsync("op_global_timer"); - } - - const nowBytes = new Uint8Array(8); - function opNow() { - sendSync("op_now", 0, nowBytes); - return new DataView(nowBytes.buffer).getFloat64(); - } - - function sleepSync(millis = 0) { - return core.jsonOpSync("op_sleep_sync", { millis }); - } - - // Derived from https://github.com/vadimg/js_bintrees. MIT Licensed. - - class RBNode { - constructor(data) { - this.data = data; - this.left = null; - this.right = null; - this.red = true; - } - - getChild(dir) { - return dir ? this.right : this.left; - } - - setChild(dir, val) { - if (dir) { - this.right = val; - } else { - this.left = val; - } - } - } - - class RBTree { - #comparator = null; - #root = null; - - constructor(comparator) { - this.#comparator = comparator; - this.#root = null; - } - - /** Returns `null` if tree is empty. */ - min() { - let res = this.#root; - if (res === null) { - return null; - } - while (res.left !== null) { - res = res.left; - } - return res.data; - } - - /** Returns node `data` if found, `null` otherwise. */ - find(data) { - let res = this.#root; - while (res !== null) { - const c = this.#comparator(data, res.data); - if (c === 0) { - return res.data; - } else { - res = res.getChild(c > 0); - } - } - return null; - } - - /** returns `true` if inserted, `false` if duplicate. */ - insert(data) { - let ret = false; - - if (this.#root === null) { - // empty tree - this.#root = new RBNode(data); - ret = true; - } else { - const head = new RBNode(null); // fake tree root - - let dir = 0; - let last = 0; - - // setup - let gp = null; // grandparent - let ggp = head; // grand-grand-parent - let p = null; // parent - let node = this.#root; - ggp.right = this.#root; - - // search down - while (true) { - if (node === null) { - // insert new node at the bottom - node = new RBNode(data); - p.setChild(dir, node); - ret = true; - } else if (isRed(node.left) && isRed(node.right)) { - // color flip - node.red = true; - node.left.red = false; - node.right.red = false; - } - - // fix red violation - if (isRed(node) && isRed(p)) { - const dir2 = ggp.right === gp; - - assert(gp); - if (node === p.getChild(last)) { - ggp.setChild(dir2, singleRotate(gp, !last)); - } else { - ggp.setChild(dir2, doubleRotate(gp, !last)); - } - } - - const cmp = this.#comparator(node.data, data); - - // stop if found - if (cmp === 0) { - break; - } - - last = dir; - dir = Number(cmp < 0); // Fix type - - // update helpers - if (gp !== null) { - ggp = gp; - } - gp = p; - p = node; - node = node.getChild(dir); - } - - // update root - this.#root = head.right; - } - - // make root black - this.#root.red = false; - - return ret; - } - - /** Returns `true` if removed, `false` if not found. */ - remove(data) { - if (this.#root === null) { - return false; - } - - const head = new RBNode(null); // fake tree root - let node = head; - node.right = this.#root; - let p = null; // parent - let gp = null; // grand parent - let found = null; // found item - let dir = 1; - - while (node.getChild(dir) !== null) { - const last = dir; - - // update helpers - gp = p; - p = node; - node = node.getChild(dir); - - const cmp = this.#comparator(data, node.data); - - dir = cmp > 0; - - // save found node - if (cmp === 0) { - found = node; - } - - // push the red node down - if (!isRed(node) && !isRed(node.getChild(dir))) { - if (isRed(node.getChild(!dir))) { - const sr = singleRotate(node, dir); - p.setChild(last, sr); - p = sr; - } else if (!isRed(node.getChild(!dir))) { - const sibling = p.getChild(!last); - if (sibling !== null) { - if ( - !isRed(sibling.getChild(!last)) && - !isRed(sibling.getChild(last)) - ) { - // color flip - p.red = false; - sibling.red = true; - node.red = true; - } else { - assert(gp); - const dir2 = gp.right === p; - - if (isRed(sibling.getChild(last))) { - gp.setChild(dir2, doubleRotate(p, last)); - } else if (isRed(sibling.getChild(!last))) { - gp.setChild(dir2, singleRotate(p, last)); - } - - // ensure correct coloring - const gpc = gp.getChild(dir2); - assert(gpc); - gpc.red = true; - node.red = true; - assert(gpc.left); - gpc.left.red = false; - assert(gpc.right); - gpc.right.red = false; - } - } - } - } - } - - // replace and remove if found - if (found !== null) { - found.data = node.data; - assert(p); - p.setChild(p.right === node, node.getChild(node.left === null)); - } - - // update root and make it black - this.#root = head.right; - if (this.#root !== null) { - this.#root.red = false; - } - - return found !== null; - } - } - - function isRed(node) { - return node !== null && node.red; - } - - function singleRotate(root, dir) { - const save = root.getChild(!dir); - assert(save); - - root.setChild(!dir, save.getChild(dir)); - save.setChild(dir, root); - - root.red = true; - save.red = false; - - return save; - } - - function doubleRotate(root, dir) { - root.setChild(!dir, singleRotate(root.getChild(!dir), !dir)); - return singleRotate(root, dir); - } - - const { console } = globalThis; - const OriginalDate = Date; - - // Timeout values > TIMEOUT_MAX are set to 1. - const TIMEOUT_MAX = 2 ** 31 - 1; - - let globalTimeoutDue = null; - - let nextTimerId = 1; - const idMap = new Map(); - const dueTree = new RBTree((a, b) => a.due - b.due); - - function clearGlobalTimeout() { - globalTimeoutDue = null; - opStopGlobalTimer(); - } - - let pendingEvents = 0; - const pendingFireTimers = []; - - /** Process and run a single ready timer macrotask. - * This function should be registered through Deno.core.setMacrotaskCallback. - * Returns true when all ready macrotasks have been processed, false if more - * ready ones are available. The Isolate future would rely on the return value - * to repeatedly invoke this function until depletion. Multiple invocations - * of this function one at a time ensures newly ready microtasks are processed - * before next macrotask timer callback is invoked. */ - function handleTimerMacrotask() { - if (pendingFireTimers.length > 0) { - fire(pendingFireTimers.shift()); - return pendingFireTimers.length === 0; - } - return true; - } - - async function setGlobalTimeout(due, now) { - // Since JS and Rust don't use the same clock, pass the time to rust as a - // relative time value. On the Rust side we'll turn that into an absolute - // value again. - const timeout = due - now; - assert(timeout >= 0); - // Send message to the backend. - globalTimeoutDue = due; - pendingEvents++; - // FIXME(bartlomieju): this is problematic, because `clearGlobalTimeout` - // is synchronous. That means that timer is cancelled, but this promise is still pending - // until next turn of event loop. This leads to "leaking of async ops" in tests; - // because `clearTimeout/clearInterval` might be the last statement in test function - // `opSanitizer` will immediately complain that there is pending op going on, unless - // some timeout/defer is put in place to allow promise resolution. - // Ideally `clearGlobalTimeout` doesn't return until this op is resolved, but - // I'm not if that's possible. - opStartGlobalTimer(timeout); - await opWaitGlobalTimer(); - pendingEvents--; - // eslint-disable-next-line @typescript-eslint/no-use-before-define - prepareReadyTimers(); - } - - function prepareReadyTimers() { - const now = OriginalDate.now(); - // Bail out if we're not expecting the global timer to fire. - if (globalTimeoutDue === null || pendingEvents > 0) { - return; - } - // After firing the timers that are due now, this will hold the first timer - // list that hasn't fired yet. - let nextDueNode; - while ((nextDueNode = dueTree.min()) !== null && nextDueNode.due <= now) { - dueTree.remove(nextDueNode); - // Fire all the timers in the list. - for (const timer of nextDueNode.timers) { - // With the list dropped, the timer is no longer scheduled. - timer.scheduled = false; - // Place the callback to pending timers to fire. - pendingFireTimers.push(timer); - } - } - setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, now); - } - - function setOrClearGlobalTimeout(due, now) { - if (due == null) { - clearGlobalTimeout(); - } else { - setGlobalTimeout(due, now); - } - } - - function schedule(timer, now) { - assert(!timer.scheduled); - assert(now <= timer.due); - // Find or create the list of timers that will fire at point-in-time `due`. - const maybeNewDueNode = { due: timer.due, timers: [] }; - let dueNode = dueTree.find(maybeNewDueNode); - if (dueNode === null) { - dueTree.insert(maybeNewDueNode); - dueNode = maybeNewDueNode; - } - // Append the newly scheduled timer to the list and mark it as scheduled. - dueNode.timers.push(timer); - timer.scheduled = true; - // If the new timer is scheduled to fire before any timer that existed before, - // update the global timeout to reflect this. - if (globalTimeoutDue === null || globalTimeoutDue > timer.due) { - setOrClearGlobalTimeout(timer.due, now); - } - } - - function unschedule(timer) { - // Check if our timer is pending scheduling or pending firing. - // If either is true, they are not in tree, and their idMap entry - // will be deleted soon. Remove it from queue. - let index = -1; - if ((index = pendingFireTimers.indexOf(timer)) >= 0) { - pendingFireTimers.splice(index); - return; - } - // If timer is not in the 2 pending queues and is unscheduled, - // it is not in the tree. - if (!timer.scheduled) { - return; - } - const searchKey = { due: timer.due, timers: [] }; - // Find the list of timers that will fire at point-in-time `due`. - const list = dueTree.find(searchKey).timers; - if (list.length === 1) { - // Time timer is the only one in the list. Remove the entire list. - assert(list[0] === timer); - dueTree.remove(searchKey); - // If the unscheduled timer was 'next up', find when the next timer that - // still exists is due, and update the global alarm accordingly. - if (timer.due === globalTimeoutDue) { - const nextDueNode = dueTree.min(); - setOrClearGlobalTimeout( - nextDueNode && nextDueNode.due, - OriginalDate.now(), - ); - } - } else { - // Multiple timers that are due at the same point in time. - // Remove this timer from the list. - const index = list.indexOf(timer); - assert(index > -1); - list.splice(index, 1); - } - } - - function fire(timer) { - // If the timer isn't found in the ID map, that means it has been cancelled - // between the timer firing and the promise callback (this function). - if (!idMap.has(timer.id)) { - return; - } - // Reschedule the timer if it is a repeating one, otherwise drop it. - if (!timer.repeat) { - // One-shot timer: remove the timer from this id-to-timer map. - idMap.delete(timer.id); - } else { - // Interval timer: compute when timer was supposed to fire next. - // However make sure to never schedule the next interval in the past. - const now = OriginalDate.now(); - timer.due = Math.max(now, timer.due + timer.delay); - schedule(timer, now); - } - // Call the user callback. Intermediate assignment is to avoid leaking `this` - // to it, while also keeping the stack trace neat when it shows up in there. - const callback = timer.callback; - callback(); - } - - function checkThis(thisArg) { - if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis) { - throw new TypeError("Illegal invocation"); - } - } - - function checkBigInt(n) { - if (typeof n === "bigint") { - throw new TypeError("Cannot convert a BigInt value to a number"); - } - } - - function setTimer( - cb, - delay, - args, - repeat, - ) { - // Bind `args` to the callback and bind `this` to globalThis(global). - const callback = cb.bind(globalThis, ...args); - // In the browser, the delay value must be coercible to an integer between 0 - // and INT32_MAX. Any other value will cause the timer to fire immediately. - // We emulate this behavior. - const now = OriginalDate.now(); - if (delay > TIMEOUT_MAX) { - console.warn( - `${delay} does not fit into` + - " a 32-bit signed integer." + - "\nTimeout duration was set to 1.", - ); - delay = 1; - } - delay = Math.max(0, delay | 0); - - // Create a new, unscheduled timer object. - const timer = { - id: nextTimerId++, - callback, - args, - delay, - due: now + delay, - repeat, - scheduled: false, - }; - // Register the timer's existence in the id-to-timer map. - idMap.set(timer.id, timer); - // Schedule the timer in the due table. - schedule(timer, now); - return timer.id; - } - - function setTimeout( - cb, - delay = 0, - ...args - ) { - checkBigInt(delay); - checkThis(this); - return setTimer(cb, delay, args, false); - } - - function setInterval( - cb, - delay = 0, - ...args - ) { - checkBigInt(delay); - checkThis(this); - return setTimer(cb, delay, args, true); - } - - function clearTimer(id) { - id = Number(id); - const timer = idMap.get(id); - if (timer === undefined) { - // Timer doesn't exist any more or never existed. This is not an error. - return; - } - // Unschedule the timer if it is currently scheduled, and forget about it. - unschedule(timer); - idMap.delete(timer.id); - } - - function clearTimeout(id = 0) { - checkBigInt(id); - if (id === 0) { - return; - } - clearTimer(id); - } - - function clearInterval(id = 0) { - checkBigInt(id); - if (id === 0) { - return; - } - clearTimer(id); - } - - window.__bootstrap.timers = { - clearInterval, - setInterval, - clearTimeout, - setTimeout, - handleTimerMacrotask, - opStopGlobalTimer, - opStartGlobalTimer, - opNow, - sleepSync, - }; -})(this); diff --git a/cli/rt/11_workers.js b/cli/rt/11_workers.js deleted file mode 100644 index 62210dfae..000000000 --- a/cli/rt/11_workers.js +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - const { Window } = window.__bootstrap.globalInterfaces; - const { log } = window.__bootstrap.util; - const { defineEventHandler } = window.__bootstrap.webUtil; - - function createWorker( - specifier, - hasSourceCode, - sourceCode, - useDenoNamespace, - name, - ) { - return core.jsonOpSync("op_create_worker", { - specifier, - hasSourceCode, - sourceCode, - name, - useDenoNamespace, - }); - } - - function hostTerminateWorker(id) { - core.jsonOpSync("op_host_terminate_worker", { id }); - } - - function hostPostMessage(id, data) { - core.jsonOpSync("op_host_post_message", { id }, data); - } - - function hostGetMessage(id) { - return core.jsonOpAsync("op_host_get_message", { id }); - } - - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - - function encodeMessage(data) { - const dataJson = JSON.stringify(data); - return encoder.encode(dataJson); - } - - function decodeMessage(dataIntArray) { - const dataJson = decoder.decode(dataIntArray); - return JSON.parse(dataJson); - } - - class Worker extends EventTarget { - #id = 0; - #name = ""; - #terminated = false; - - constructor(specifier, options) { - super(); - const { type = "classic", name = "unknown" } = options ?? {}; - - if (type !== "module") { - throw new Error( - 'Not yet implemented: only "module" type workers are supported', - ); - } - - this.#name = name; - const hasSourceCode = false; - const sourceCode = decoder.decode(new Uint8Array()); - - const useDenoNamespace = options ? !!options.deno : false; - - const { id } = createWorker( - specifier, - hasSourceCode, - sourceCode, - useDenoNamespace, - options?.name, - ); - this.#id = id; - this.#poll(); - } - - #handleMessage = (msgData) => { - let data; - try { - data = decodeMessage(new Uint8Array(msgData)); - } catch (e) { - const msgErrorEvent = new MessageEvent("messageerror", { - cancelable: false, - data, - }); - return; - } - - const msgEvent = new MessageEvent("message", { - cancelable: false, - data, - }); - - this.dispatchEvent(msgEvent); - }; - - #handleError = (e) => { - const event = new ErrorEvent("error", { - cancelable: true, - message: e.message, - lineno: e.lineNumber ? e.lineNumber + 1 : undefined, - colno: e.columnNumber ? e.columnNumber + 1 : undefined, - filename: e.fileName, - error: null, - }); - - let handled = false; - - this.dispatchEvent(event); - if (event.defaultPrevented) { - handled = true; - } - - return handled; - }; - - #poll = async () => { - while (!this.#terminated) { - const event = await hostGetMessage(this.#id); - - // If terminate was called then we ignore all messages - if (this.#terminated) { - return; - } - - const type = event.type; - - if (type === "terminalError") { - this.#terminated = true; - if (!this.#handleError(event.error)) { - if (globalThis instanceof Window) { - throw new Error("Unhandled error event reached main worker."); - } else { - core.jsonOpSync( - "op_host_unhandled_error", - { message: event.error.message }, - ); - } - } - continue; - } - - if (type === "msg") { - this.#handleMessage(event.data); - continue; - } - - if (type === "error") { - if (!this.#handleError(event.error)) { - if (globalThis instanceof Window) { - throw new Error("Unhandled error event reached main worker."); - } else { - core.jsonOpSync( - "op_host_unhandled_error", - { message: event.error.message }, - ); - } - } - continue; - } - - if (type === "close") { - log(`Host got "close" message from worker: ${this.#name}`); - this.#terminated = true; - return; - } - - throw new Error(`Unknown worker event: "${type}"`); - } - }; - - postMessage(message, transferOrOptions) { - if (transferOrOptions) { - throw new Error( - "Not yet implemented: `transfer` and `options` are not supported.", - ); - } - - if (this.#terminated) { - return; - } - - hostPostMessage(this.#id, encodeMessage(message)); - } - - terminate() { - if (!this.#terminated) { - this.#terminated = true; - hostTerminateWorker(this.#id); - } - } - } - - defineEventHandler(Worker.prototype, "error"); - defineEventHandler(Worker.prototype, "message"); - defineEventHandler(Worker.prototype, "messageerror"); - - window.__bootstrap.worker = { - Worker, - }; -})(this); diff --git a/cli/rt/12_io.js b/cli/rt/12_io.js deleted file mode 100644 index 006d51cdd..000000000 --- a/cli/rt/12_io.js +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -// Interfaces 100% copied from Go. -// Documentation liberally lifted from them too. -// Thank you! We love Go! <3 - -((window) => { - const DEFAULT_BUFFER_SIZE = 32 * 1024; - const { sendSync, sendAsync } = window.__bootstrap.dispatchMinimal; - // Seek whence values. - // https://golang.org/pkg/io/#pkg-constants - const SeekMode = { - 0: "Start", - 1: "Current", - 2: "End", - - Start: 0, - Current: 1, - End: 2, - }; - - async function copy( - src, - dst, - options, - ) { - let n = 0; - const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE; - const b = new Uint8Array(bufSize); - let gotEOF = false; - while (gotEOF === false) { - const result = await src.read(b); - if (result === null) { - gotEOF = true; - } else { - let nwritten = 0; - while (nwritten < result) { - nwritten += await dst.write(b.subarray(nwritten, result)); - } - n += nwritten; - } - } - return n; - } - - async function* iter( - r, - options, - ) { - const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE; - const b = new Uint8Array(bufSize); - while (true) { - const result = await r.read(b); - if (result === null) { - break; - } - - yield b.subarray(0, result); - } - } - - function* iterSync( - r, - options, - ) { - const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE; - const b = new Uint8Array(bufSize); - while (true) { - const result = r.readSync(b); - if (result === null) { - break; - } - - yield b.subarray(0, result); - } - } - - function readSync(rid, buffer) { - if (buffer.length === 0) { - return 0; - } - - const nread = sendSync("op_read", rid, buffer); - if (nread < 0) { - throw new Error("read error"); - } - - return nread === 0 ? null : nread; - } - - async function read( - rid, - buffer, - ) { - if (buffer.length === 0) { - return 0; - } - - const nread = await sendAsync("op_read", rid, buffer); - if (nread < 0) { - throw new Error("read error"); - } - - return nread === 0 ? null : nread; - } - - function writeSync(rid, data) { - const result = sendSync("op_write", rid, data); - if (result < 0) { - throw new Error("write error"); - } - - return result; - } - - async function write(rid, data) { - const result = await sendAsync("op_write", rid, data); - if (result < 0) { - throw new Error("write error"); - } - - return result; - } - - window.__bootstrap.io = { - iterSync, - iter, - copy, - SeekMode, - read, - readSync, - write, - writeSync, - }; -})(this); diff --git a/cli/rt/13_buffer.js b/cli/rt/13_buffer.js deleted file mode 100644 index e06e2138b..000000000 --- a/cli/rt/13_buffer.js +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -// This code has been ported almost directly from Go's src/bytes/buffer.go -// Copyright 2009 The Go Authors. All rights reserved. BSD license. -// https://github.com/golang/go/blob/master/LICENSE - -((window) => { - const { assert } = window.__bootstrap.util; - - // MIN_READ is the minimum ArrayBuffer size passed to a read call by - // buffer.ReadFrom. As long as the Buffer has at least MIN_READ bytes beyond - // what is required to hold the contents of r, readFrom() will not grow the - // underlying buffer. - const MIN_READ = 32 * 1024; - const MAX_SIZE = 2 ** 32 - 2; - - // `off` is the offset into `dst` where it will at which to begin writing values - // from `src`. - // Returns the number of bytes copied. - function copyBytes(src, dst, off = 0) { - const r = dst.byteLength - off; - if (src.byteLength > r) { - src = src.subarray(0, r); - } - dst.set(src, off); - return src.byteLength; - } - - class Buffer { - #buf = null; // contents are the bytes buf[off : len(buf)] - #off = 0; // read at buf[off], write at buf[buf.byteLength] - - constructor(ab) { - if (ab == null) { - this.#buf = new Uint8Array(0); - return; - } - - this.#buf = new Uint8Array(ab); - } - - bytes(options = { copy: true }) { - if (options.copy === false) return this.#buf.subarray(this.#off); - return this.#buf.slice(this.#off); - } - - empty() { - return this.#buf.byteLength <= this.#off; - } - - get length() { - return this.#buf.byteLength - this.#off; - } - - get capacity() { - return this.#buf.buffer.byteLength; - } - - truncate(n) { - if (n === 0) { - this.reset(); - return; - } - if (n < 0 || n > this.length) { - throw Error("bytes.Buffer: truncation out of range"); - } - this.#reslice(this.#off + n); - } - - reset() { - this.#reslice(0); - this.#off = 0; - } - - #tryGrowByReslice = (n) => { - const l = this.#buf.byteLength; - if (n <= this.capacity - l) { - this.#reslice(l + n); - return l; - } - return -1; - }; - - #reslice = (len) => { - assert(len <= this.#buf.buffer.byteLength); - this.#buf = new Uint8Array(this.#buf.buffer, 0, len); - }; - - readSync(p) { - if (this.empty()) { - // Buffer is empty, reset to recover space. - this.reset(); - if (p.byteLength === 0) { - // this edge case is tested in 'bufferReadEmptyAtEOF' test - return 0; - } - return null; - } - const nread = copyBytes(this.#buf.subarray(this.#off), p); - this.#off += nread; - return nread; - } - - read(p) { - const rr = this.readSync(p); - return Promise.resolve(rr); - } - - writeSync(p) { - const m = this.#grow(p.byteLength); - return copyBytes(p, this.#buf, m); - } - - write(p) { - const n = this.writeSync(p); - return Promise.resolve(n); - } - - #grow = (n) => { - const m = this.length; - // If buffer is empty, reset to recover space. - if (m === 0 && this.#off !== 0) { - this.reset(); - } - // Fast: Try to grow by means of a reslice. - const i = this.#tryGrowByReslice(n); - if (i >= 0) { - return i; - } - const c = this.capacity; - if (n <= Math.floor(c / 2) - m) { - // We can slide things down instead of allocating a new - // ArrayBuffer. We only need m+n <= c to slide, but - // we instead let capacity get twice as large so we - // don't spend all our time copying. - copyBytes(this.#buf.subarray(this.#off), this.#buf); - } else if (c + n > MAX_SIZE) { - throw new Error("The buffer cannot be grown beyond the maximum size."); - } else { - // Not enough space anywhere, we need to allocate. - const buf = new Uint8Array(Math.min(2 * c + n, MAX_SIZE)); - copyBytes(this.#buf.subarray(this.#off), buf); - this.#buf = buf; - } - // Restore this.#off and len(this.#buf). - this.#off = 0; - this.#reslice(Math.min(m + n, MAX_SIZE)); - return m; - }; - - grow(n) { - if (n < 0) { - throw Error("Buffer.grow: negative count"); - } - const m = this.#grow(n); - this.#reslice(m); - } - - async readFrom(r) { - let n = 0; - const tmp = new Uint8Array(MIN_READ); - while (true) { - const shouldGrow = this.capacity - this.length < MIN_READ; - // read into tmp buffer if there's not enough room - // otherwise read directly into the internal buffer - const buf = shouldGrow - ? tmp - : new Uint8Array(this.#buf.buffer, this.length); - - const nread = await r.read(buf); - if (nread === null) { - return n; - } - - // write will grow if needed - if (shouldGrow) this.writeSync(buf.subarray(0, nread)); - else this.#reslice(this.length + nread); - - n += nread; - } - } - - readFromSync(r) { - let n = 0; - const tmp = new Uint8Array(MIN_READ); - while (true) { - const shouldGrow = this.capacity - this.length < MIN_READ; - // read into tmp buffer if there's not enough room - // otherwise read directly into the internal buffer - const buf = shouldGrow - ? tmp - : new Uint8Array(this.#buf.buffer, this.length); - - const nread = r.readSync(buf); - if (nread === null) { - return n; - } - - // write will grow if needed - if (shouldGrow) this.writeSync(buf.subarray(0, nread)); - else this.#reslice(this.length + nread); - - n += nread; - } - } - } - - async function readAll(r) { - const buf = new Buffer(); - await buf.readFrom(r); - return buf.bytes(); - } - - function readAllSync(r) { - const buf = new Buffer(); - buf.readFromSync(r); - return buf.bytes(); - } - - async function writeAll(w, arr) { - let nwritten = 0; - while (nwritten < arr.length) { - nwritten += await w.write(arr.subarray(nwritten)); - } - } - - function writeAllSync(w, arr) { - let nwritten = 0; - while (nwritten < arr.length) { - nwritten += w.writeSync(arr.subarray(nwritten)); - } - } - - window.__bootstrap.buffer = { - writeAll, - writeAllSync, - readAll, - readAllSync, - Buffer, - }; -})(this); diff --git a/cli/rt/27_websocket.js b/cli/rt/27_websocket.js deleted file mode 100644 index 60428c24d..000000000 --- a/cli/rt/27_websocket.js +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - const { requiredArguments, defineEventHandler } = window.__bootstrap.webUtil; - const CONNECTING = 0; - const OPEN = 1; - const CLOSING = 2; - const CLOSED = 3; - - class WebSocket extends EventTarget { - #readyState = CONNECTING; - - constructor(url, protocols = []) { - super(); - requiredArguments("WebSocket", arguments.length, 1); - - const wsURL = new URL(url); - - if (wsURL.protocol !== "ws:" && wsURL.protocol !== "wss:") { - throw new DOMException( - "Only ws & wss schemes are allowed in a WebSocket URL.", - "SyntaxError", - ); - } - - if (wsURL.hash !== "" || wsURL.href.endsWith("#")) { - throw new DOMException( - "Fragments are not allowed in a WebSocket URL.", - "SyntaxError", - ); - } - - this.#url = wsURL.href; - - core.jsonOpSync("op_ws_check_permission", { - url: this.#url, - }); - - if (protocols && typeof protocols === "string") { - protocols = [protocols]; - } - - if ( - protocols.some((x) => protocols.indexOf(x) !== protocols.lastIndexOf(x)) - ) { - throw new DOMException( - "Can't supply multiple times the same protocol.", - "SyntaxError", - ); - } - - core.jsonOpAsync("op_ws_create", { - url: wsURL.href, - protocols: protocols.join(", "), - }).then((create) => { - if (create.success) { - this.#rid = create.rid; - this.#extensions = create.extensions; - this.#protocol = create.protocol; - - if (this.#readyState === CLOSING) { - core.jsonOpAsync("op_ws_close", { - rid: this.#rid, - }).then(() => { - this.#readyState = CLOSED; - - const errEvent = new ErrorEvent("error"); - errEvent.target = this; - this.dispatchEvent(errEvent); - - const event = new CloseEvent("close"); - event.target = this; - this.dispatchEvent(event); - core.close(this.#rid); - }); - } else { - this.#readyState = OPEN; - const event = new Event("open"); - event.target = this; - this.dispatchEvent(event); - - this.#eventLoop(); - } - } else { - this.#readyState = CLOSED; - - const errEvent = new ErrorEvent("error"); - errEvent.target = this; - this.dispatchEvent(errEvent); - - const closeEvent = new CloseEvent("close"); - closeEvent.target = this; - this.dispatchEvent(closeEvent); - } - }).catch((err) => { - this.#readyState = CLOSED; - - const errorEv = new ErrorEvent( - "error", - { error: err, message: err.toString() }, - ); - errorEv.target = this; - this.dispatchEvent(errorEv); - - const closeEv = new CloseEvent("close"); - closeEv.target = this; - this.dispatchEvent(closeEv); - }); - } - - get CONNECTING() { - return CONNECTING; - } - get OPEN() { - return OPEN; - } - get CLOSING() { - return CLOSING; - } - get CLOSED() { - return CLOSED; - } - - get readyState() { - return this.#readyState; - } - - #extensions = ""; - #protocol = ""; - #url = ""; - #rid; - - get extensions() { - return this.#extensions; - } - get protocol() { - return this.#protocol; - } - - #binaryType = "blob"; - get binaryType() { - return this.#binaryType; - } - set binaryType(value) { - if (value === "blob" || value === "arraybuffer") { - this.#binaryType = value; - } - } - #bufferedAmount = 0; - get bufferedAmount() { - return this.#bufferedAmount; - } - - get url() { - return this.#url; - } - - send(data) { - requiredArguments("WebSocket.send", arguments.length, 1); - - if (this.#readyState != OPEN) { - throw Error("readyState not OPEN"); - } - - const sendTypedArray = (ta) => { - this.#bufferedAmount += ta.size; - core.jsonOpAsync("op_ws_send", { - rid: this.#rid, - }, ta).then(() => { - this.#bufferedAmount -= ta.size; - }); - }; - - if (data instanceof Blob) { - data.slice().arrayBuffer().then((ab) => - sendTypedArray(new DataView(ab)) - ); - } else if ( - data instanceof Int8Array || data instanceof Int16Array || - data instanceof Int32Array || data instanceof Uint8Array || - data instanceof Uint16Array || data instanceof Uint32Array || - data instanceof Uint8ClampedArray || data instanceof Float32Array || - data instanceof Float64Array || data instanceof DataView - ) { - sendTypedArray(data); - } else if (data instanceof ArrayBuffer) { - sendTypedArray(new DataView(data)); - } else { - const string = String(data); - const encoder = new TextEncoder(); - const d = encoder.encode(string); - this.#bufferedAmount += d.size; - core.jsonOpAsync("op_ws_send", { - rid: this.#rid, - text: string, - }).then(() => { - this.#bufferedAmount -= d.size; - }); - } - } - - close(code, reason) { - if (code && (code !== 1000 && !(3000 <= code > 5000))) { - throw new DOMException( - "The close code must be either 1000 or in the range of 3000 to 4999.", - "NotSupportedError", - ); - } - - const encoder = new TextEncoder(); - if (reason && encoder.encode(reason).byteLength > 123) { - throw new DOMException( - "The close reason may not be longer than 123 bytes.", - "SyntaxError", - ); - } - - if (this.#readyState === CONNECTING) { - this.#readyState = CLOSING; - } else if (this.#readyState === OPEN) { - this.#readyState = CLOSING; - - core.jsonOpAsync("op_ws_close", { - rid: this.#rid, - code, - reason, - }).then(() => { - this.#readyState = CLOSED; - const event = new CloseEvent("close", { - wasClean: true, - code, - reason, - }); - event.target = this; - this.dispatchEvent(event); - core.close(this.#rid); - }); - } - } - - async #eventLoop() { - if (this.#readyState === OPEN) { - const message = await core.jsonOpAsync( - "op_ws_next_event", - { rid: this.#rid }, - ); - if (message.type === "string" || message.type === "binary") { - let data; - - if (message.type === "string") { - data = message.data; - } else { - if (this.binaryType === "blob") { - data = new Blob([new Uint8Array(message.data)]); - } else { - data = new Uint8Array(message.data).buffer; - } - } - - const event = new MessageEvent("message", { - data, - origin: this.#url, - }); - event.target = this; - this.dispatchEvent(event); - - this.#eventLoop(); - } else if (message.type === "close") { - this.#readyState = CLOSED; - const event = new CloseEvent("close", { - wasClean: true, - code: message.code, - reason: message.reason, - }); - event.target = this; - this.dispatchEvent(event); - } else if (message.type === "error") { - this.#readyState = CLOSED; - - const errorEv = new ErrorEvent("error"); - errorEv.target = this; - this.dispatchEvent(errorEv); - - this.#readyState = CLOSED; - const closeEv = new CloseEvent("close"); - closeEv.target = this; - this.dispatchEvent(closeEv); - } - } - } - } - - Object.defineProperties(WebSocket, { - CONNECTING: { - value: 0, - }, - OPEN: { - value: 1, - }, - CLOSING: { - value: 2, - }, - CLOSED: { - value: 3, - }, - }); - - defineEventHandler(WebSocket.prototype, "message"); - defineEventHandler(WebSocket.prototype, "error"); - defineEventHandler(WebSocket.prototype, "close"); - defineEventHandler(WebSocket.prototype, "open"); - window.__bootstrap.webSocket = { - WebSocket, - }; -})(this); diff --git a/cli/rt/30_files.js b/cli/rt/30_files.js deleted file mode 100644 index 679b184fd..000000000 --- a/cli/rt/30_files.js +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - const { read, readSync, write, writeSync } = window.__bootstrap.io; - const { pathFromURL } = window.__bootstrap.util; - - function seekSync( - rid, - offset, - whence, - ) { - return core.jsonOpSync("op_seek_sync", { rid, offset, whence }); - } - - function seek( - rid, - offset, - whence, - ) { - return core.jsonOpAsync("op_seek_async", { rid, offset, whence }); - } - - function openSync( - path, - options = { read: true }, - ) { - checkOpenOptions(options); - const mode = options?.mode; - const rid = core.jsonOpSync( - "op_open_sync", - { path: pathFromURL(path), options, mode }, - ); - - return new File(rid); - } - - async function open( - path, - options = { read: true }, - ) { - checkOpenOptions(options); - const mode = options?.mode; - const rid = await core.jsonOpAsync( - "op_open_async", - { path: pathFromURL(path), options, mode }, - ); - - return new File(rid); - } - - function createSync(path) { - return openSync(path, { - read: true, - write: true, - truncate: true, - create: true, - }); - } - - function create(path) { - return open(path, { - read: true, - write: true, - truncate: true, - create: true, - }); - } - - class File { - #rid = 0; - - constructor(rid) { - this.#rid = rid; - } - - get rid() { - return this.#rid; - } - - write(p) { - return write(this.rid, p); - } - - writeSync(p) { - return writeSync(this.rid, p); - } - - read(p) { - return read(this.rid, p); - } - - readSync(p) { - return readSync(this.rid, p); - } - - seek(offset, whence) { - return seek(this.rid, offset, whence); - } - - seekSync(offset, whence) { - return seekSync(this.rid, offset, whence); - } - - close() { - core.close(this.rid); - } - } - - class Stdin { - constructor() { - } - - get rid() { - return 0; - } - - read(p) { - return read(this.rid, p); - } - - readSync(p) { - return readSync(this.rid, p); - } - - close() { - core.close(this.rid); - } - } - - class Stdout { - constructor() { - } - - get rid() { - return 1; - } - - write(p) { - return write(this.rid, p); - } - - writeSync(p) { - return writeSync(this.rid, p); - } - - close() { - core.close(this.rid); - } - } - - class Stderr { - constructor() { - } - - get rid() { - return 2; - } - - write(p) { - return write(this.rid, p); - } - - writeSync(p) { - return writeSync(this.rid, p); - } - - close() { - core.close(this.rid); - } - } - - const stdin = new Stdin(); - const stdout = new Stdout(); - const stderr = new Stderr(); - - function checkOpenOptions(options) { - if (Object.values(options).filter((val) => val === true).length === 0) { - throw new Error("OpenOptions requires at least one option to be true"); - } - - if (options.truncate && !options.write) { - throw new Error("'truncate' option requires 'write' option"); - } - - const createOrCreateNewWithoutWriteOrAppend = - (options.create || options.createNew) && - !(options.write || options.append); - - if (createOrCreateNewWithoutWriteOrAppend) { - throw new Error( - "'create' or 'createNew' options require 'write' or 'append' option", - ); - } - } - - window.__bootstrap.files = { - stdin, - stdout, - stderr, - File, - create, - createSync, - open, - openSync, - seek, - seekSync, - }; -})(this); diff --git a/cli/rt/30_fs.js b/cli/rt/30_fs.js deleted file mode 100644 index 33fab01e4..000000000 --- a/cli/rt/30_fs.js +++ /dev/null @@ -1,425 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - const { pathFromURL } = window.__bootstrap.util; - const build = window.__bootstrap.build.build; - - function chmodSync(path, mode) { - core.jsonOpSync("op_chmod_sync", { path: pathFromURL(path), mode }); - } - - async function chmod(path, mode) { - await core.jsonOpAsync("op_chmod_async", { path: pathFromURL(path), mode }); - } - - function chownSync( - path, - uid, - gid, - ) { - core.jsonOpSync("op_chown_sync", { path: pathFromURL(path), uid, gid }); - } - - async function chown( - path, - uid, - gid, - ) { - await core.jsonOpAsync( - "op_chown_async", - { path: pathFromURL(path), uid, gid }, - ); - } - - function copyFileSync( - fromPath, - toPath, - ) { - core.jsonOpSync("op_copy_file_sync", { - from: pathFromURL(fromPath), - to: pathFromURL(toPath), - }); - } - - async function copyFile( - fromPath, - toPath, - ) { - await core.jsonOpAsync("op_copy_file_async", { - from: pathFromURL(fromPath), - to: pathFromURL(toPath), - }); - } - - function cwd() { - return core.jsonOpSync("op_cwd"); - } - - function chdir(directory) { - core.jsonOpSync("op_chdir", { directory }); - } - - function makeTempDirSync(options = {}) { - return core.jsonOpSync("op_make_temp_dir_sync", options); - } - - function makeTempDir(options = {}) { - return core.jsonOpAsync("op_make_temp_dir_async", options); - } - - function makeTempFileSync(options = {}) { - return core.jsonOpSync("op_make_temp_file_sync", options); - } - - function makeTempFile(options = {}) { - return core.jsonOpAsync("op_make_temp_file_async", options); - } - - function mkdirArgs(path, options) { - const args = { path: pathFromURL(path), recursive: false }; - if (options != null) { - if (typeof options.recursive == "boolean") { - args.recursive = options.recursive; - } - if (options.mode) { - args.mode = options.mode; - } - } - return args; - } - - function mkdirSync(path, options) { - core.jsonOpSync("op_mkdir_sync", mkdirArgs(path, options)); - } - - async function mkdir( - path, - options, - ) { - await core.jsonOpAsync("op_mkdir_async", mkdirArgs(path, options)); - } - - function res(response) { - return response.entries; - } - - function readDirSync(path) { - return res( - core.jsonOpSync("op_read_dir_sync", { path: pathFromURL(path) }), - )[ - Symbol.iterator - ](); - } - - function readDir(path) { - const array = core.jsonOpAsync( - "op_read_dir_async", - { path: pathFromURL(path) }, - ) - .then( - res, - ); - return { - async *[Symbol.asyncIterator]() { - yield* await array; - }, - }; - } - - function readLinkSync(path) { - return core.jsonOpSync("op_read_link_sync", { path: pathFromURL(path) }); - } - - function readLink(path) { - return core.jsonOpAsync("op_read_link_async", { path: pathFromURL(path) }); - } - - function realPathSync(path) { - return core.jsonOpSync("op_realpath_sync", { path }); - } - - function realPath(path) { - return core.jsonOpAsync("op_realpath_async", { path }); - } - - function removeSync( - path, - options = {}, - ) { - core.jsonOpSync("op_remove_sync", { - path: pathFromURL(path), - recursive: !!options.recursive, - }); - } - - async function remove( - path, - options = {}, - ) { - await core.jsonOpAsync("op_remove_async", { - path: pathFromURL(path), - recursive: !!options.recursive, - }); - } - - function renameSync(oldpath, newpath) { - core.jsonOpSync("op_rename_sync", { oldpath, newpath }); - } - - async function rename(oldpath, newpath) { - await core.jsonOpAsync("op_rename_async", { oldpath, newpath }); - } - - function parseFileInfo(response) { - const unix = build.os === "darwin" || build.os === "linux"; - return { - isFile: response.isFile, - isDirectory: response.isDirectory, - isSymlink: response.isSymlink, - size: response.size, - mtime: response.mtime != null ? new Date(response.mtime) : null, - atime: response.atime != null ? new Date(response.atime) : null, - birthtime: response.birthtime != null - ? new Date(response.birthtime) - : null, - // Only non-null if on Unix - dev: unix ? response.dev : null, - ino: unix ? response.ino : null, - mode: unix ? response.mode : null, - nlink: unix ? response.nlink : null, - uid: unix ? response.uid : null, - gid: unix ? response.gid : null, - rdev: unix ? response.rdev : null, - blksize: unix ? response.blksize : null, - blocks: unix ? response.blocks : null, - }; - } - - function fstatSync(rid) { - return parseFileInfo(core.jsonOpSync("op_fstat_sync", { rid })); - } - - async function fstat(rid) { - return parseFileInfo(await core.jsonOpAsync("op_fstat_async", { rid })); - } - - async function lstat(path) { - const res = await core.jsonOpAsync("op_stat_async", { - path: pathFromURL(path), - lstat: true, - }); - return parseFileInfo(res); - } - - function lstatSync(path) { - const res = core.jsonOpSync("op_stat_sync", { - path: pathFromURL(path), - lstat: true, - }); - return parseFileInfo(res); - } - - async function stat(path) { - const res = await core.jsonOpAsync("op_stat_async", { - path: pathFromURL(path), - lstat: false, - }); - return parseFileInfo(res); - } - - function statSync(path) { - const res = core.jsonOpSync("op_stat_sync", { - path: pathFromURL(path), - lstat: false, - }); - return parseFileInfo(res); - } - - function coerceLen(len) { - if (len == null || len < 0) { - return 0; - } - - return len; - } - - function ftruncateSync(rid, len) { - core.jsonOpSync("op_ftruncate_sync", { rid, len: coerceLen(len) }); - } - - async function ftruncate(rid, len) { - await core.jsonOpAsync("op_ftruncate_async", { rid, len: coerceLen(len) }); - } - - function truncateSync(path, len) { - core.jsonOpSync("op_truncate_sync", { path, len: coerceLen(len) }); - } - - async function truncate(path, len) { - await core.jsonOpAsync("op_truncate_async", { path, len: coerceLen(len) }); - } - - function umask(mask) { - return core.jsonOpSync("op_umask", { mask }); - } - - function linkSync(oldpath, newpath) { - core.jsonOpSync("op_link_sync", { oldpath, newpath }); - } - - async function link(oldpath, newpath) { - await core.jsonOpAsync("op_link_async", { oldpath, newpath }); - } - - function toUnixTimeFromEpoch(value) { - if (value instanceof Date) { - const time = value.valueOf(); - const seconds = Math.trunc(time / 1e3); - const nanoseconds = Math.trunc(time - (seconds * 1e3)) * 1e6; - - return [ - seconds, - nanoseconds, - ]; - } - - const seconds = value; - const nanoseconds = 0; - - return [ - seconds, - nanoseconds, - ]; - } - - function futimeSync( - rid, - atime, - mtime, - ) { - core.jsonOpSync("op_futime_sync", { - rid, - atime: toUnixTimeFromEpoch(atime), - mtime: toUnixTimeFromEpoch(mtime), - }); - } - - async function futime( - rid, - atime, - mtime, - ) { - await core.jsonOpAsync("op_futime_async", { - rid, - atime: toUnixTimeFromEpoch(atime), - mtime: toUnixTimeFromEpoch(mtime), - }); - } - - function utimeSync( - path, - atime, - mtime, - ) { - core.jsonOpSync("op_utime_sync", { - path, - atime: toUnixTimeFromEpoch(atime), - mtime: toUnixTimeFromEpoch(mtime), - }); - } - - async function utime( - path, - atime, - mtime, - ) { - await core.jsonOpAsync("op_utime_async", { - path, - atime: toUnixTimeFromEpoch(atime), - mtime: toUnixTimeFromEpoch(mtime), - }); - } - - function symlinkSync( - oldpath, - newpath, - options, - ) { - core.jsonOpSync("op_symlink_sync", { oldpath, newpath, options }); - } - - async function symlink( - oldpath, - newpath, - options, - ) { - await core.jsonOpAsync("op_symlink_async", { oldpath, newpath, options }); - } - - function fdatasyncSync(rid) { - core.jsonOpSync("op_fdatasync_sync", { rid }); - } - - async function fdatasync(rid) { - await core.jsonOpAsync("op_fdatasync_async", { rid }); - } - - function fsyncSync(rid) { - core.jsonOpSync("op_fsync_sync", { rid }); - } - - async function fsync(rid) { - await core.jsonOpAsync("op_fsync_async", { rid }); - } - - window.__bootstrap.fs = { - cwd, - chdir, - chmodSync, - chmod, - chown, - chownSync, - copyFile, - copyFileSync, - makeTempFile, - makeTempDir, - makeTempFileSync, - makeTempDirSync, - mkdir, - mkdirSync, - readDir, - readDirSync, - readLinkSync, - readLink, - realPathSync, - realPath, - remove, - removeSync, - renameSync, - rename, - fstatSync, - fstat, - lstat, - lstatSync, - stat, - statSync, - ftruncate, - ftruncateSync, - truncate, - truncateSync, - umask, - link, - linkSync, - futime, - futimeSync, - utime, - utimeSync, - symlink, - symlinkSync, - fdatasync, - fdatasyncSync, - fsync, - fsyncSync, - }; -})(this); diff --git a/cli/rt/30_metrics.js b/cli/rt/30_metrics.js deleted file mode 100644 index d44a629cb..000000000 --- a/cli/rt/30_metrics.js +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - - function metrics() { - return core.jsonOpSync("op_metrics"); - } - - window.__bootstrap.metrics = { - metrics, - }; -})(this); diff --git a/cli/rt/30_net.js b/cli/rt/30_net.js deleted file mode 100644 index 9a71f0693..000000000 --- a/cli/rt/30_net.js +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - const { errors } = window.__bootstrap.errors; - const { read, write } = window.__bootstrap.io; - - const ShutdownMode = { - // See http://man7.org/linux/man-pages/man2/shutdown.2.html - // Corresponding to SHUT_RD, SHUT_WR, SHUT_RDWR - 0: "Read", - 1: "Write", - 2: "ReadWrite", - Read: 0, - Write: 1, - ReadWrite: 2, // unused - }; - - function shutdown(rid, how) { - core.jsonOpSync("op_shutdown", { rid, how }); - return Promise.resolve(); - } - - function opAccept( - rid, - transport, - ) { - return core.jsonOpAsync("op_accept", { rid, transport }); - } - - function opListen(args) { - return core.jsonOpSync("op_listen", args); - } - - function opConnect(args) { - return core.jsonOpAsync("op_connect", args); - } - - function opReceive( - rid, - transport, - zeroCopy, - ) { - return core.jsonOpAsync( - "op_datagram_receive", - { rid, transport }, - zeroCopy, - ); - } - - function opSend(args, zeroCopy) { - return core.jsonOpAsync("op_datagram_send", args, zeroCopy); - } - - class Conn { - #rid = 0; - #remoteAddr = null; - #localAddr = null; - constructor( - rid, - remoteAddr, - localAddr, - ) { - this.#rid = rid; - this.#remoteAddr = remoteAddr; - this.#localAddr = localAddr; - } - - get rid() { - return this.#rid; - } - - get remoteAddr() { - return this.#remoteAddr; - } - - get localAddr() { - return this.#localAddr; - } - - write(p) { - return write(this.rid, p); - } - - read(p) { - return read(this.rid, p); - } - - close() { - core.close(this.rid); - } - - // TODO(lucacasonato): make this unavailable in stable - closeWrite() { - shutdown(this.rid, ShutdownMode.Write); - } - } - - class Listener { - #rid = 0; - #addr = null; - - constructor(rid, addr) { - this.#rid = rid; - this.#addr = addr; - } - - get rid() { - return this.#rid; - } - - get addr() { - return this.#addr; - } - - async accept() { - const res = await opAccept(this.rid, this.addr.transport); - return new Conn(res.rid, res.remoteAddr, res.localAddr); - } - - async next() { - let conn; - try { - conn = await this.accept(); - } catch (error) { - if (error instanceof errors.BadResource) { - return { value: undefined, done: true }; - } - throw error; - } - return { value: conn, done: false }; - } - - return(value) { - this.close(); - return Promise.resolve({ value, done: true }); - } - - close() { - core.close(this.rid); - } - - [Symbol.asyncIterator]() { - return this; - } - } - - class Datagram { - #rid = 0; - #addr = null; - - constructor( - rid, - addr, - bufSize = 1024, - ) { - this.#rid = rid; - this.#addr = addr; - this.bufSize = bufSize; - } - - get rid() { - return this.#rid; - } - - get addr() { - return this.#addr; - } - - async receive(p) { - const buf = p || new Uint8Array(this.bufSize); - const { size, remoteAddr } = await opReceive( - this.rid, - this.addr.transport, - buf, - ); - const sub = buf.subarray(0, size); - return [sub, remoteAddr]; - } - - send(p, addr) { - const remote = { hostname: "127.0.0.1", ...addr }; - - const args = { ...remote, rid: this.rid }; - return opSend(args, p); - } - - close() { - core.close(this.rid); - } - - async *[Symbol.asyncIterator]() { - while (true) { - try { - yield await this.receive(); - } catch (err) { - if (err instanceof errors.BadResource) { - break; - } - throw err; - } - } - } - } - - function listen({ hostname, ...options }) { - const res = opListen({ - transport: "tcp", - hostname: typeof hostname === "undefined" ? "0.0.0.0" : hostname, - ...options, - }); - - return new Listener(res.rid, res.localAddr); - } - - async function connect( - options, - ) { - let res; - - if (options.transport === "unix") { - res = await opConnect(options); - } else { - res = await opConnect({ - transport: "tcp", - hostname: "127.0.0.1", - ...options, - }); - } - - return new Conn(res.rid, res.remoteAddr, res.localAddr); - } - - window.__bootstrap.net = { - connect, - Conn, - opConnect, - listen, - opListen, - Listener, - shutdown, - ShutdownMode, - Datagram, - }; -})(this); diff --git a/cli/rt/30_os.js b/cli/rt/30_os.js deleted file mode 100644 index ebc4e8916..000000000 --- a/cli/rt/30_os.js +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - - function loadavg() { - return core.jsonOpSync("op_loadavg"); - } - - function hostname() { - return core.jsonOpSync("op_hostname"); - } - - function osRelease() { - return core.jsonOpSync("op_os_release"); - } - - function systemMemoryInfo() { - return core.jsonOpSync("op_system_memory_info"); - } - - function systemCpuInfo() { - return core.jsonOpSync("op_system_cpu_info"); - } - - function exit(code = 0) { - core.jsonOpSync("op_exit", { code }); - throw new Error("Code not reachable"); - } - - function setEnv(key, value) { - core.jsonOpSync("op_set_env", { key, value }); - } - - function getEnv(key) { - return core.jsonOpSync("op_get_env", { key })[0]; - } - - function deleteEnv(key) { - core.jsonOpSync("op_delete_env", { key }); - } - - const env = { - get: getEnv, - toObject() { - return core.jsonOpSync("op_env"); - }, - set: setEnv, - delete: deleteEnv, - }; - - function execPath() { - return core.jsonOpSync("op_exec_path"); - } - - window.__bootstrap.os = { - env, - execPath, - exit, - osRelease, - systemMemoryInfo, - systemCpuInfo, - hostname, - loadavg, - }; -})(this); diff --git a/cli/rt/40_compiler_api.js b/cli/rt/40_compiler_api.js deleted file mode 100644 index ea963b67b..000000000 --- a/cli/rt/40_compiler_api.js +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -// This file contains the runtime APIs which will dispatch work to the internal -// compiler within Deno. -((window) => { - const core = window.Deno.core; - const util = window.__bootstrap.util; - - function opCompile(request) { - return core.jsonOpAsync("op_compile", request); - } - - function opTranspile( - request, - ) { - return core.jsonOpAsync("op_transpile", request); - } - - function checkRelative(specifier) { - return specifier.match(/^([\.\/\\]|https?:\/{2}|file:\/{2})/) - ? specifier - : `./${specifier}`; - } - - // TODO(bartlomieju): change return type to interface? - function transpileOnly( - sources, - options = {}, - ) { - util.log("Deno.transpileOnly", { sources: Object.keys(sources), options }); - const payload = { - sources, - options: JSON.stringify(options), - }; - return opTranspile(payload); - } - - // TODO(bartlomieju): change return type to interface? - async function compile( - rootName, - sources, - options = {}, - ) { - const payload = { - rootName: sources ? rootName : checkRelative(rootName), - sources, - options: JSON.stringify(options), - bundle: false, - }; - util.log("Deno.compile", { - rootName: payload.rootName, - sources: !!sources, - options, - }); - /** @type {{ emittedFiles: Record, diagnostics: any[] }} */ - const result = await opCompile(payload); - util.assert(result.emittedFiles); - const maybeDiagnostics = result.diagnostics.length === 0 - ? undefined - : result.diagnostics; - - return [maybeDiagnostics, result.emittedFiles]; - } - - // TODO(bartlomieju): change return type to interface? - async function bundle( - rootName, - sources, - options = {}, - ) { - const payload = { - rootName: sources ? rootName : checkRelative(rootName), - sources, - options: JSON.stringify(options), - bundle: true, - }; - util.log("Deno.bundle", { - rootName: payload.rootName, - sources: !!sources, - options, - }); - /** @type {{ emittedFiles: Record, diagnostics: any[] }} */ - const result = await opCompile(payload); - const output = result.emittedFiles["deno:///bundle.js"]; - util.assert(output); - const maybeDiagnostics = result.diagnostics.length === 0 - ? undefined - : result.diagnostics; - return [maybeDiagnostics, output]; - } - - window.__bootstrap.compilerApi = { - bundle, - compile, - transpileOnly, - }; -})(this); diff --git a/cli/rt/40_diagnostics.js b/cli/rt/40_diagnostics.js deleted file mode 100644 index 2b7457853..000000000 --- a/cli/rt/40_diagnostics.js +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -// Diagnostic provides an abstraction for advice/errors received from a -// compiler, which is strongly influenced by the format of TypeScript -// diagnostics. - -((window) => { - const DiagnosticCategory = { - 0: "Warning", - 1: "Error", - 2: "Suggestion", - 3: "Message", - - Warning: 0, - Error: 1, - Suggestion: 2, - Message: 3, - }; - - window.__bootstrap.diagnostics = { - DiagnosticCategory, - }; -})(this); diff --git a/cli/rt/40_error_stack.js b/cli/rt/40_error_stack.js deleted file mode 100644 index da2ee51f3..000000000 --- a/cli/rt/40_error_stack.js +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - - function opFormatDiagnostics(diagnostics) { - return core.jsonOpSync("op_format_diagnostic", diagnostics); - } - - function opApplySourceMap(location) { - const res = core.jsonOpSync("op_apply_source_map", location); - return { - fileName: res.fileName, - lineNumber: res.lineNumber, - columnNumber: res.columnNumber, - }; - } - - window.__bootstrap.errorStack = { - opApplySourceMap, - opFormatDiagnostics, - }; -})(this); diff --git a/cli/rt/40_fs_events.js b/cli/rt/40_fs_events.js deleted file mode 100644 index a36adecba..000000000 --- a/cli/rt/40_fs_events.js +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - const { errors } = window.__bootstrap.errors; - - class FsWatcher { - #rid = 0; - - constructor(paths, options) { - const { recursive } = options; - this.#rid = core.jsonOpSync("op_fs_events_open", { recursive, paths }); - } - - get rid() { - return this.#rid; - } - - async next() { - try { - return await core.jsonOpAsync("op_fs_events_poll", { - rid: this.rid, - }); - } catch (error) { - if (error instanceof errors.BadResource) { - return { value: undefined, done: true }; - } - throw error; - } - } - - return(value) { - core.close(this.rid); - return Promise.resolve({ value, done: true }); - } - - [Symbol.asyncIterator]() { - return this; - } - } - - function watchFs( - paths, - options = { recursive: true }, - ) { - return new FsWatcher(Array.isArray(paths) ? paths : [paths], options); - } - - window.__bootstrap.fsEvents = { - watchFs, - }; -})(this); diff --git a/cli/rt/40_net_unstable.js b/cli/rt/40_net_unstable.js deleted file mode 100644 index fcc899a30..000000000 --- a/cli/rt/40_net_unstable.js +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const net = window.__bootstrap.net; - - function listen(options) { - if (options.transport === "unix") { - const res = net.opListen(options); - return new net.Listener(res.rid, res.localAddr); - } else { - return net.listen(options); - } - } - - function listenDatagram( - options, - ) { - let res; - if (options.transport === "unixpacket") { - res = net.opListen(options); - } else { - res = net.opListen({ - transport: "udp", - hostname: "127.0.0.1", - ...options, - }); - } - - return new net.Datagram(res.rid, res.localAddr); - } - - async function connect( - options, - ) { - if (options.transport === "unix") { - const res = await net.opConnect(options); - return new net.Conn(res.rid, res.remoteAddr, res.localAddr); - } else { - return net.connect(options); - } - } - - window.__bootstrap.netUnstable = { - connect, - listenDatagram, - listen, - }; -})(this); diff --git a/cli/rt/40_performance.js b/cli/rt/40_performance.js deleted file mode 100644 index 0a63dc704..000000000 --- a/cli/rt/40_performance.js +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const { opNow } = window.__bootstrap.timers; - const { cloneValue, illegalConstructorKey } = window.__bootstrap.webUtil; - - const customInspect = Symbol.for("Deno.customInspect"); - let performanceEntries = []; - - function findMostRecent( - name, - type, - ) { - return performanceEntries - .slice() - .reverse() - .find((entry) => entry.name === name && entry.entryType === type); - } - - function convertMarkToTimestamp(mark) { - if (typeof mark === "string") { - const entry = findMostRecent(mark, "mark"); - if (!entry) { - throw new SyntaxError(`Cannot find mark: "${mark}".`); - } - return entry.startTime; - } - if (mark < 0) { - throw new TypeError("Mark cannot be negative."); - } - return mark; - } - - function filterByNameType( - name, - type, - ) { - return performanceEntries.filter( - (entry) => - (name ? entry.name === name : true) && - (type ? entry.entryType === type : true), - ); - } - - function now() { - return opNow(); - } - - class PerformanceEntry { - #name = ""; - #entryType = ""; - #startTime = 0; - #duration = 0; - - get name() { - return this.#name; - } - - get entryType() { - return this.#entryType; - } - - get startTime() { - return this.#startTime; - } - - get duration() { - return this.#duration; - } - - constructor( - name = null, - entryType = null, - startTime = null, - duration = null, - key = null, - ) { - if (key != illegalConstructorKey) { - throw new TypeError("Illegal constructor."); - } - this.#name = name; - this.#entryType = entryType; - this.#startTime = startTime; - this.#duration = duration; - } - - toJSON() { - return { - name: this.#name, - entryType: this.#entryType, - startTime: this.#startTime, - duration: this.#duration, - }; - } - - [customInspect]() { - return `${this.constructor.name} { name: "${this.name}", entryType: "${this.entryType}", startTime: ${this.startTime}, duration: ${this.duration} }`; - } - } - - class PerformanceMark extends PerformanceEntry { - #detail = null; - - get detail() { - return this.#detail; - } - - get entryType() { - return "mark"; - } - - constructor( - name, - { detail = null, startTime = now() } = {}, - ) { - super(name, "mark", startTime, 0, illegalConstructorKey); - if (startTime < 0) { - throw new TypeError("startTime cannot be negative"); - } - this.#detail = cloneValue(detail); - } - - toJSON() { - return { - name: this.name, - entryType: this.entryType, - startTime: this.startTime, - duration: this.duration, - detail: this.detail, - }; - } - - [customInspect]() { - return this.detail - ? `${this.constructor.name} {\n detail: ${ - JSON.stringify(this.detail, null, 2) - },\n name: "${this.name}",\n entryType: "${this.entryType}",\n startTime: ${this.startTime},\n duration: ${this.duration}\n}` - : `${this.constructor.name} { detail: ${this.detail}, name: "${this.name}", entryType: "${this.entryType}", startTime: ${this.startTime}, duration: ${this.duration} }`; - } - } - - class PerformanceMeasure extends PerformanceEntry { - #detail = null; - - get detail() { - return this.#detail; - } - - get entryType() { - return "measure"; - } - - constructor( - name, - startTime, - duration, - detail = null, - key, - ) { - if (key != illegalConstructorKey) { - throw new TypeError("Illegal constructor."); - } - super(name, "measure", startTime, duration, illegalConstructorKey); - this.#detail = cloneValue(detail); - } - - toJSON() { - return { - name: this.name, - entryType: this.entryType, - startTime: this.startTime, - duration: this.duration, - detail: this.detail, - }; - } - - [customInspect]() { - return this.detail - ? `${this.constructor.name} {\n detail: ${ - JSON.stringify(this.detail, null, 2) - },\n name: "${this.name}",\n entryType: "${this.entryType}",\n startTime: ${this.startTime},\n duration: ${this.duration}\n}` - : `${this.constructor.name} { detail: ${this.detail}, name: "${this.name}", entryType: "${this.entryType}", startTime: ${this.startTime}, duration: ${this.duration} }`; - } - } - - class Performance { - constructor(key = null) { - if (key != illegalConstructorKey) { - throw new TypeError("Illegal constructor."); - } - } - - clearMarks(markName) { - if (markName == null) { - performanceEntries = performanceEntries.filter( - (entry) => entry.entryType !== "mark", - ); - } else { - performanceEntries = performanceEntries.filter( - (entry) => !(entry.name === markName && entry.entryType === "mark"), - ); - } - } - - clearMeasures(measureName) { - if (measureName == null) { - performanceEntries = performanceEntries.filter( - (entry) => entry.entryType !== "measure", - ); - } else { - performanceEntries = performanceEntries.filter( - (entry) => - !(entry.name === measureName && entry.entryType === "measure"), - ); - } - } - - getEntries() { - return filterByNameType(); - } - - getEntriesByName( - name, - type, - ) { - return filterByNameType(name, type); - } - - getEntriesByType(type) { - return filterByNameType(undefined, type); - } - - mark( - markName, - options = {}, - ) { - // 3.1.1.1 If the global object is a Window object and markName uses the - // same name as a read only attribute in the PerformanceTiming interface, - // throw a SyntaxError. - not implemented - const entry = new PerformanceMark(markName, options); - // 3.1.1.7 Queue entry - not implemented - performanceEntries.push(entry); - return entry; - } - - measure( - measureName, - startOrMeasureOptions = {}, - endMark, - ) { - if ( - startOrMeasureOptions && typeof startOrMeasureOptions === "object" && - Object.keys(startOrMeasureOptions).length > 0 - ) { - if (endMark) { - throw new TypeError("Options cannot be passed with endMark."); - } - if ( - !("start" in startOrMeasureOptions) && - !("end" in startOrMeasureOptions) - ) { - throw new TypeError( - "A start or end mark must be supplied in options.", - ); - } - if ( - "start" in startOrMeasureOptions && - "duration" in startOrMeasureOptions && - "end" in startOrMeasureOptions - ) { - throw new TypeError( - "Cannot specify start, end, and duration together in options.", - ); - } - } - let endTime; - if (endMark) { - endTime = convertMarkToTimestamp(endMark); - } else if ( - typeof startOrMeasureOptions === "object" && - "end" in startOrMeasureOptions - ) { - endTime = convertMarkToTimestamp(startOrMeasureOptions.end); - } else if ( - typeof startOrMeasureOptions === "object" && - "start" in startOrMeasureOptions && - "duration" in startOrMeasureOptions - ) { - const start = convertMarkToTimestamp(startOrMeasureOptions.start); - const duration = convertMarkToTimestamp(startOrMeasureOptions.duration); - endTime = start + duration; - } else { - endTime = now(); - } - let startTime; - if ( - typeof startOrMeasureOptions === "object" && - "start" in startOrMeasureOptions - ) { - startTime = convertMarkToTimestamp(startOrMeasureOptions.start); - } else if ( - typeof startOrMeasureOptions === "object" && - "end" in startOrMeasureOptions && - "duration" in startOrMeasureOptions - ) { - const end = convertMarkToTimestamp(startOrMeasureOptions.end); - const duration = convertMarkToTimestamp(startOrMeasureOptions.duration); - startTime = end - duration; - } else if (typeof startOrMeasureOptions === "string") { - startTime = convertMarkToTimestamp(startOrMeasureOptions); - } else { - startTime = 0; - } - const entry = new PerformanceMeasure( - measureName, - startTime, - endTime - startTime, - typeof startOrMeasureOptions === "object" - ? startOrMeasureOptions.detail ?? null - : null, - illegalConstructorKey, - ); - performanceEntries.push(entry); - return entry; - } - - now() { - return now(); - } - } - - const performance = new Performance(illegalConstructorKey); - - window.__bootstrap.performance = { - PerformanceEntry, - PerformanceMark, - PerformanceMeasure, - Performance, - performance, - }; -})(this); diff --git a/cli/rt/40_permissions.js b/cli/rt/40_permissions.js deleted file mode 100644 index 50d471b6a..000000000 --- a/cli/rt/40_permissions.js +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - const { illegalConstructorKey } = window.__bootstrap.webUtil; - - function opQuery(desc) { - return core.jsonOpSync("op_query_permission", desc).state; - } - - function opRevoke(desc) { - return core.jsonOpSync("op_revoke_permission", desc).state; - } - - function opRequest(desc) { - return core.jsonOpSync("op_request_permission", desc).state; - } - - class PermissionStatus { - constructor(state = null, key = null) { - if (key != illegalConstructorKey) { - throw new TypeError("Illegal constructor."); - } - this.state = state; - } - // TODO(kt3k): implement onchange handler - } - - class Permissions { - constructor(key) { - if (key != illegalConstructorKey) { - throw new TypeError("Illegal constructor."); - } - } - - query(desc) { - const state = opQuery(desc); - return Promise.resolve( - new PermissionStatus(state, illegalConstructorKey), - ); - } - - revoke(desc) { - const state = opRevoke(desc); - return Promise.resolve( - new PermissionStatus(state, illegalConstructorKey), - ); - } - - request(desc) { - const state = opRequest(desc); - return Promise.resolve( - new PermissionStatus(state, illegalConstructorKey), - ); - } - } - - const permissions = new Permissions(illegalConstructorKey); - - window.__bootstrap.permissions = { - permissions, - Permissions, - PermissionStatus, - }; -})(this); diff --git a/cli/rt/40_plugins.js b/cli/rt/40_plugins.js deleted file mode 100644 index f5aefd400..000000000 --- a/cli/rt/40_plugins.js +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - - function openPlugin(filename) { - return core.jsonOpSync("op_open_plugin", { filename }); - } - - window.__bootstrap.plugins = { - openPlugin, - }; -})(this); diff --git a/cli/rt/40_process.js b/cli/rt/40_process.js deleted file mode 100644 index b46a1aead..000000000 --- a/cli/rt/40_process.js +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - const { File } = window.__bootstrap.files; - const { readAll } = window.__bootstrap.buffer; - const { assert, pathFromURL } = window.__bootstrap.util; - - function opKill(pid, signo) { - core.jsonOpSync("op_kill", { pid, signo }); - } - - function opRunStatus(rid) { - return core.jsonOpAsync("op_run_status", { rid }); - } - - function opRun(request) { - assert(request.cmd.length > 0); - return core.jsonOpSync("op_run", request); - } - - async function runStatus(rid) { - const res = await opRunStatus(rid); - - if (res.gotSignal) { - const signal = res.exitSignal; - return { success: false, code: 128 + signal, signal }; - } else if (res.exitCode != 0) { - return { success: false, code: res.exitCode }; - } else { - return { success: true, code: 0 }; - } - } - - class Process { - constructor(res) { - this.rid = res.rid; - this.pid = res.pid; - - if (res.stdinRid && res.stdinRid > 0) { - this.stdin = new File(res.stdinRid); - } - - if (res.stdoutRid && res.stdoutRid > 0) { - this.stdout = new File(res.stdoutRid); - } - - if (res.stderrRid && res.stderrRid > 0) { - this.stderr = new File(res.stderrRid); - } - } - - status() { - return runStatus(this.rid); - } - - async output() { - if (!this.stdout) { - throw new TypeError("stdout was not piped"); - } - try { - return await readAll(this.stdout); - } finally { - this.stdout.close(); - } - } - - async stderrOutput() { - if (!this.stderr) { - throw new TypeError("stderr was not piped"); - } - try { - return await readAll(this.stderr); - } finally { - this.stderr.close(); - } - } - - close() { - core.close(this.rid); - } - - kill(signo) { - opKill(this.pid, signo); - } - } - - function isRid(arg) { - return !isNaN(arg); - } - - function run({ - cmd, - cwd = undefined, - env = {}, - stdout = "inherit", - stderr = "inherit", - stdin = "inherit", - }) { - if (cmd[0] != null) { - cmd[0] = pathFromURL(cmd[0]); - } - const res = opRun({ - cmd: cmd.map(String), - cwd, - env: Object.entries(env), - stdin: isRid(stdin) ? "" : stdin, - stdout: isRid(stdout) ? "" : stdout, - stderr: isRid(stderr) ? "" : stderr, - stdinRid: isRid(stdin) ? stdin : 0, - stdoutRid: isRid(stdout) ? stdout : 0, - stderrRid: isRid(stderr) ? stderr : 0, - }); - return new Process(res); - } - - window.__bootstrap.process = { - run, - Process, - kill: opKill, - }; -})(this); diff --git a/cli/rt/40_read_file.js b/cli/rt/40_read_file.js deleted file mode 100644 index 9a36f335b..000000000 --- a/cli/rt/40_read_file.js +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const { open, openSync } = window.__bootstrap.files; - const { readAll, readAllSync } = window.__bootstrap.buffer; - - function readFileSync(path) { - const file = openSync(path); - const contents = readAllSync(file); - file.close(); - return contents; - } - - async function readFile(path) { - const file = await open(path); - const contents = await readAll(file); - file.close(); - return contents; - } - - function readTextFileSync(path) { - const file = openSync(path); - const contents = readAllSync(file); - file.close(); - const decoder = new TextDecoder(); - return decoder.decode(contents); - } - - async function readTextFile(path) { - const file = await open(path); - const contents = await readAll(file); - file.close(); - const decoder = new TextDecoder(); - return decoder.decode(contents); - } - - window.__bootstrap.readFile = { - readFile, - readFileSync, - readTextFileSync, - readTextFile, - }; -})(this); diff --git a/cli/rt/40_signals.js b/cli/rt/40_signals.js deleted file mode 100644 index 739c963fd..000000000 --- a/cli/rt/40_signals.js +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - const { build } = window.__bootstrap.build; - - function bindSignal(signo) { - return core.jsonOpSync("op_signal_bind", { signo }); - } - - function pollSignal(rid) { - return core.jsonOpAsync("op_signal_poll", { rid }); - } - - function unbindSignal(rid) { - core.jsonOpSync("op_signal_unbind", { rid }); - } - - // From `kill -l` - const LinuxSignal = { - 1: "SIGHUP", - 2: "SIGINT", - 3: "SIGQUIT", - 4: "SIGILL", - 5: "SIGTRAP", - 6: "SIGABRT", - 7: "SIGBUS", - 8: "SIGFPE", - 9: "SIGKILL", - 10: "SIGUSR1", - 11: "SIGSEGV", - 12: "SIGUSR2", - 13: "SIGPIPE", - 14: "SIGALRM", - 15: "SIGTERM", - 16: "SIGSTKFLT", - 17: "SIGCHLD", - 18: "SIGCONT", - 19: "SIGSTOP", - 20: "SIGTSTP", - 21: "SIGTTIN", - 22: "SIGTTOU", - 23: "SIGURG", - 24: "SIGXCPU", - 25: "SIGXFSZ", - 26: "SIGVTALRM", - 27: "SIGPROF", - 28: "SIGWINCH", - 29: "SIGIO", - 30: "SIGPWR", - 31: "SIGSYS", - SIGHUP: 1, - SIGINT: 2, - SIGQUIT: 3, - SIGILL: 4, - SIGTRAP: 5, - SIGABRT: 6, - SIGBUS: 7, - SIGFPE: 8, - SIGKILL: 9, - SIGUSR1: 10, - SIGSEGV: 11, - SIGUSR2: 12, - SIGPIPE: 13, - SIGALRM: 14, - SIGTERM: 15, - SIGSTKFLT: 16, - SIGCHLD: 17, - SIGCONT: 18, - SIGSTOP: 19, - SIGTSTP: 20, - SIGTTIN: 21, - SIGTTOU: 22, - SIGURG: 23, - SIGXCPU: 24, - SIGXFSZ: 25, - SIGVTALRM: 26, - SIGPROF: 27, - SIGWINCH: 28, - SIGIO: 29, - SIGPWR: 30, - SIGSYS: 31, - }; - - // From `kill -l` - const MacOSSignal = { - 1: "SIGHUP", - 2: "SIGINT", - 3: "SIGQUIT", - 4: "SIGILL", - 5: "SIGTRAP", - 6: "SIGABRT", - 7: "SIGEMT", - 8: "SIGFPE", - 9: "SIGKILL", - 10: "SIGBUS", - 11: "SIGSEGV", - 12: "SIGSYS", - 13: "SIGPIPE", - 14: "SIGALRM", - 15: "SIGTERM", - 16: "SIGURG", - 17: "SIGSTOP", - 18: "SIGTSTP", - 19: "SIGCONT", - 20: "SIGCHLD", - 21: "SIGTTIN", - 22: "SIGTTOU", - 23: "SIGIO", - 24: "SIGXCPU", - 25: "SIGXFSZ", - 26: "SIGVTALRM", - 27: "SIGPROF", - 28: "SIGWINCH", - 29: "SIGINFO", - 30: "SIGUSR1", - 31: "SIGUSR2", - SIGHUP: 1, - SIGINT: 2, - SIGQUIT: 3, - SIGILL: 4, - SIGTRAP: 5, - SIGABRT: 6, - SIGEMT: 7, - SIGFPE: 8, - SIGKILL: 9, - SIGBUS: 10, - SIGSEGV: 11, - SIGSYS: 12, - SIGPIPE: 13, - SIGALRM: 14, - SIGTERM: 15, - SIGURG: 16, - SIGSTOP: 17, - SIGTSTP: 18, - SIGCONT: 19, - SIGCHLD: 20, - SIGTTIN: 21, - SIGTTOU: 22, - SIGIO: 23, - SIGXCPU: 24, - SIGXFSZ: 25, - SIGVTALRM: 26, - SIGPROF: 27, - SIGWINCH: 28, - SIGINFO: 29, - SIGUSR1: 30, - SIGUSR2: 31, - }; - - const Signal = {}; - - function setSignals() { - if (build.os === "darwin") { - Object.assign(Signal, MacOSSignal); - } else { - Object.assign(Signal, LinuxSignal); - } - } - - function signal(signo) { - if (build.os === "windows") { - throw new Error("not implemented!"); - } - return new SignalStream(signo); - } - - const signals = { - alarm() { - return signal(Signal.SIGALRM); - }, - child() { - return signal(Signal.SIGCHLD); - }, - hungup() { - return signal(Signal.SIGHUP); - }, - interrupt() { - return signal(Signal.SIGINT); - }, - io() { - return signal(Signal.SIGIO); - }, - pipe() { - return signal(Signal.SIGPIPE); - }, - quit() { - return signal(Signal.SIGQUIT); - }, - terminate() { - return signal(Signal.SIGTERM); - }, - userDefined1() { - return signal(Signal.SIGUSR1); - }, - userDefined2() { - return signal(Signal.SIGUSR2); - }, - windowChange() { - return signal(Signal.SIGWINCH); - }, - }; - - class SignalStream { - #disposed = false; - #pollingPromise = Promise.resolve(false); - #rid = 0; - - constructor(signo) { - this.#rid = bindSignal(signo).rid; - this.#loop(); - } - - #pollSignal = async () => { - const res = await pollSignal(this.#rid); - return res.done; - }; - - #loop = async () => { - do { - this.#pollingPromise = this.#pollSignal(); - } while (!(await this.#pollingPromise) && !this.#disposed); - }; - - then( - f, - g, - ) { - return this.#pollingPromise.then(() => {}).then(f, g); - } - - async next() { - return { done: await this.#pollingPromise, value: undefined }; - } - - [Symbol.asyncIterator]() { - return this; - } - - dispose() { - if (this.#disposed) { - throw new Error("The stream has already been disposed."); - } - this.#disposed = true; - unbindSignal(this.#rid); - } - } - - window.__bootstrap.signals = { - signal, - signals, - Signal, - SignalStream, - setSignals, - }; -})(this); diff --git a/cli/rt/40_testing.js b/cli/rt/40_testing.js deleted file mode 100644 index 082d17fe0..000000000 --- a/cli/rt/40_testing.js +++ /dev/null @@ -1,350 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - const colors = window.__bootstrap.colors; - const { exit } = window.__bootstrap.os; - const { Console, inspectArgs } = window.__bootstrap.console; - const { stdout } = window.__bootstrap.files; - const { exposeForTest } = window.__bootstrap.internals; - const { metrics } = window.__bootstrap.metrics; - const { assert } = window.__bootstrap.util; - - const disabledConsole = new Console(() => {}); - - function delay(ms) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - } - - function formatDuration(time = 0) { - const gray = colors.maybeColor(colors.gray); - const italic = colors.maybeColor(colors.italic); - const timeStr = `(${time}ms)`; - return gray(italic(timeStr)); - } - - // Wrap test function in additional assertion that makes sure - // the test case does not leak async "ops" - ie. number of async - // completed ops after the test is the same as number of dispatched - // ops. Note that "unref" ops are ignored since in nature that are - // optional. - function assertOps(fn) { - return async function asyncOpSanitizer() { - const pre = metrics(); - await fn(); - // Defer until next event loop turn - that way timeouts and intervals - // cleared can actually be removed from resource table, otherwise - // false positives may occur (https://github.com/denoland/deno/issues/4591) - await delay(0); - const post = metrics(); - // We're checking diff because one might spawn HTTP server in the background - // that will be a pending async op before test starts. - const dispatchedDiff = post.opsDispatchedAsync - pre.opsDispatchedAsync; - const completedDiff = post.opsCompletedAsync - pre.opsCompletedAsync; - assert( - dispatchedDiff === completedDiff, - `Test case is leaking async ops. -Before: - - dispatched: ${pre.opsDispatchedAsync} - - completed: ${pre.opsCompletedAsync} -After: - - dispatched: ${post.opsDispatchedAsync} - - completed: ${post.opsCompletedAsync} - -Make sure to await all promises returned from Deno APIs before -finishing test case.`, - ); - }; - } - - // Wrap test function in additional assertion that makes sure - // the test case does not "leak" resources - ie. resource table after - // the test has exactly the same contents as before the test. - function assertResources( - fn, - ) { - return async function resourceSanitizer() { - const pre = core.resources(); - await fn(); - const post = core.resources(); - - const preStr = JSON.stringify(pre, null, 2); - const postStr = JSON.stringify(post, null, 2); - const msg = `Test case is leaking resources. -Before: ${preStr} -After: ${postStr} - -Make sure to close all open resource handles returned from Deno APIs before -finishing test case.`; - assert(preStr === postStr, msg); - }; - } - - const TEST_REGISTRY = []; - - // Main test function provided by Deno, as you can see it merely - // creates a new object with "name" and "fn" fields. - function test( - t, - fn, - ) { - let testDef; - const defaults = { - ignore: false, - only: false, - sanitizeOps: true, - sanitizeResources: true, - }; - - if (typeof t === "string") { - if (!fn || typeof fn != "function") { - throw new TypeError("Missing test function"); - } - if (!t) { - throw new TypeError("The test name can't be empty"); - } - testDef = { fn: fn, name: t, ...defaults }; - } else { - if (!t.fn) { - throw new TypeError("Missing test function"); - } - if (!t.name) { - throw new TypeError("The test name can't be empty"); - } - testDef = { ...defaults, ...t }; - } - - if (testDef.sanitizeOps) { - testDef.fn = assertOps(testDef.fn); - } - - if (testDef.sanitizeResources) { - testDef.fn = assertResources(testDef.fn); - } - - TEST_REGISTRY.push(testDef); - } - - const encoder = new TextEncoder(); - - function log(msg, noNewLine = false) { - if (!noNewLine) { - msg += "\n"; - } - - // Using `stdout` here because it doesn't force new lines - // compared to `console.log`; `core.print` on the other hand - // is line-buffered and doesn't output message without newline - stdout.writeSync(encoder.encode(msg)); - } - - function reportToConsole(message) { - const green = colors.maybeColor(colors.green); - const red = colors.maybeColor(colors.red); - const yellow = colors.maybeColor(colors.yellow); - const redFailed = red("FAILED"); - const greenOk = green("ok"); - const yellowIgnored = yellow("ignored"); - if (message.start != null) { - log(`running ${message.start.tests.length} tests`); - } else if (message.testStart != null) { - const { name } = message.testStart; - - log(`test ${name} ... `, true); - return; - } else if (message.testEnd != null) { - switch (message.testEnd.status) { - case "passed": - log(`${greenOk} ${formatDuration(message.testEnd.duration)}`); - break; - case "failed": - log(`${redFailed} ${formatDuration(message.testEnd.duration)}`); - break; - case "ignored": - log(`${yellowIgnored} ${formatDuration(message.testEnd.duration)}`); - break; - } - } else if (message.end != null) { - const failures = message.end.results.filter((m) => m.error != null); - if (failures.length > 0) { - log(`\nfailures:\n`); - - for (const { name, error } of failures) { - log(name); - log(inspectArgs([error])); - log(""); - } - - log(`failures:\n`); - - for (const { name } of failures) { - log(`\t${name}`); - } - } - log( - `\ntest result: ${message.end.failed ? redFailed : greenOk}. ` + - `${message.end.passed} passed; ${message.end.failed} failed; ` + - `${message.end.ignored} ignored; ${message.end.measured} measured; ` + - `${message.end.filtered} filtered out ` + - `${formatDuration(message.end.duration)}\n`, - ); - - if (message.end.usedOnly && message.end.failed == 0) { - log(`${redFailed} because the "only" option was used\n`); - } - } - } - - exposeForTest("reportToConsole", reportToConsole); - - // TODO: already implements AsyncGenerator, but add as "implements to class" - // TODO: implements PromiseLike - class TestRunner { - #usedOnly = false; - - constructor( - tests, - filterFn, - failFast, - ) { - this.stats = { - filtered: 0, - ignored: 0, - measured: 0, - passed: 0, - failed: 0, - }; - this.filterFn = filterFn; - this.failFast = failFast; - const onlyTests = tests.filter(({ only }) => only); - this.#usedOnly = onlyTests.length > 0; - const unfilteredTests = this.#usedOnly ? onlyTests : tests; - this.testsToRun = unfilteredTests.filter(filterFn); - this.stats.filtered = unfilteredTests.length - this.testsToRun.length; - } - - async *[Symbol.asyncIterator]() { - yield { start: { tests: this.testsToRun } }; - - const results = []; - const suiteStart = +new Date(); - for (const test of this.testsToRun) { - const endMessage = { - name: test.name, - duration: 0, - }; - yield { testStart: { ...test } }; - if (test.ignore) { - endMessage.status = "ignored"; - this.stats.ignored++; - } else { - const start = +new Date(); - try { - await test.fn(); - endMessage.status = "passed"; - this.stats.passed++; - } catch (err) { - endMessage.status = "failed"; - endMessage.error = err; - this.stats.failed++; - } - endMessage.duration = +new Date() - start; - } - results.push(endMessage); - yield { testEnd: endMessage }; - if (this.failFast && endMessage.error != null) { - break; - } - } - - const duration = +new Date() - suiteStart; - - yield { - end: { ...this.stats, usedOnly: this.#usedOnly, duration, results }, - }; - } - } - - function createFilterFn( - filter, - skip, - ) { - return (def) => { - let passes = true; - - if (filter) { - if (filter instanceof RegExp) { - passes = passes && filter.test(def.name); - } else if (filter.startsWith("/") && filter.endsWith("/")) { - const filterAsRegex = new RegExp(filter.slice(1, filter.length - 1)); - passes = passes && filterAsRegex.test(def.name); - } else { - passes = passes && def.name.includes(filter); - } - } - - if (skip) { - if (skip instanceof RegExp) { - passes = passes && !skip.test(def.name); - } else { - passes = passes && !def.name.includes(skip); - } - } - - return passes; - }; - } - - exposeForTest("createFilterFn", createFilterFn); - - async function runTests({ - exitOnFail = true, - failFast = false, - filter = undefined, - skip = undefined, - disableLog = false, - reportToConsole: reportToConsole_ = true, - onMessage = undefined, - } = {}) { - const filterFn = createFilterFn(filter, skip); - const testRunner = new TestRunner(TEST_REGISTRY, filterFn, failFast); - - const originalConsole = globalThis.console; - - if (disableLog) { - globalThis.console = disabledConsole; - } - - let endMsg; - - for await (const message of testRunner) { - if (onMessage != null) { - await onMessage(message); - } - if (reportToConsole_) { - reportToConsole(message); - } - if (message.end != null) { - endMsg = message.end; - } - } - - if (disableLog) { - globalThis.console = originalConsole; - } - - if ((endMsg.failed > 0 || endMsg?.usedOnly) && exitOnFail) { - exit(1); - } - - return endMsg; - } - - exposeForTest("runTests", runTests); - - window.__bootstrap.testing = { - test, - }; -})(this); diff --git a/cli/rt/40_tls.js b/cli/rt/40_tls.js deleted file mode 100644 index d66e0bd01..000000000 --- a/cli/rt/40_tls.js +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - const { Listener, Conn } = window.__bootstrap.net; - - function opConnectTls( - args, - ) { - return core.jsonOpAsync("op_connect_tls", args); - } - - function opAcceptTLS(rid) { - return core.jsonOpAsync("op_accept_tls", { rid }); - } - - function opListenTls(args) { - return core.jsonOpSync("op_listen_tls", args); - } - - function opStartTls(args) { - return core.jsonOpAsync("op_start_tls", args); - } - - async function connectTls({ - port, - hostname = "127.0.0.1", - transport = "tcp", - certFile = undefined, - }) { - const res = await opConnectTls({ - port, - hostname, - transport, - certFile, - }); - return new Conn(res.rid, res.remoteAddr, res.localAddr); - } - - class TLSListener extends Listener { - async accept() { - const res = await opAcceptTLS(this.rid); - return new Conn(res.rid, res.remoteAddr, res.localAddr); - } - } - - function listenTls({ - port, - certFile, - keyFile, - hostname = "0.0.0.0", - transport = "tcp", - }) { - const res = opListenTls({ - port, - certFile, - keyFile, - hostname, - transport, - }); - return new TLSListener(res.rid, res.localAddr); - } - - async function startTls( - conn, - { hostname = "127.0.0.1", certFile } = {}, - ) { - const res = await opStartTls({ - rid: conn.rid, - hostname, - certFile, - }); - return new Conn(res.rid, res.remoteAddr, res.localAddr); - } - - window.__bootstrap.tls = { - startTls, - listenTls, - connectTls, - TLSListener, - }; -})(this); diff --git a/cli/rt/40_tty.js b/cli/rt/40_tty.js deleted file mode 100644 index 598d33237..000000000 --- a/cli/rt/40_tty.js +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - - function consoleSize(rid) { - return core.jsonOpSync("op_console_size", { rid }); - } - - function isatty(rid) { - return core.jsonOpSync("op_isatty", { rid }); - } - - const DEFAULT_SET_RAW_OPTIONS = { - cbreak: false, - }; - - function setRaw(rid, mode, options = {}) { - const rOptions = { ...DEFAULT_SET_RAW_OPTIONS, ...options }; - core.jsonOpSync("op_set_raw", { rid, mode, options: rOptions }); - } - - window.__bootstrap.tty = { - consoleSize, - isatty, - setRaw, - }; -})(this); diff --git a/cli/rt/40_write_file.js b/cli/rt/40_write_file.js deleted file mode 100644 index 7a9cb1f40..000000000 --- a/cli/rt/40_write_file.js +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -((window) => { - const { stat, statSync, chmod, chmodSync } = window.__bootstrap.fs; - const { open, openSync } = window.__bootstrap.files; - const { writeAll, writeAllSync } = window.__bootstrap.buffer; - const { build } = window.__bootstrap.build; - - function writeFileSync( - path, - data, - options = {}, - ) { - if (options.create !== undefined) { - const create = !!options.create; - if (!create) { - // verify that file exists - statSync(path); - } - } - - const openOptions = options.append - ? { write: true, create: true, append: true } - : { write: true, create: true, truncate: true }; - const file = openSync(path, openOptions); - - if ( - options.mode !== undefined && - options.mode !== null && - build.os !== "windows" - ) { - chmodSync(path, options.mode); - } - - writeAllSync(file, data); - file.close(); - } - - async function writeFile( - path, - data, - options = {}, - ) { - if (options.create !== undefined) { - const create = !!options.create; - if (!create) { - // verify that file exists - await stat(path); - } - } - - const openOptions = options.append - ? { write: true, create: true, append: true } - : { write: true, create: true, truncate: true }; - const file = await open(path, openOptions); - - if ( - options.mode !== undefined && - options.mode !== null && - build.os !== "windows" - ) { - await chmod(path, options.mode); - } - - await writeAll(file, data); - file.close(); - } - - function writeTextFileSync( - path, - data, - options = {}, - ) { - const encoder = new TextEncoder(); - return writeFileSync(path, encoder.encode(data), options); - } - - function writeTextFile( - path, - data, - options = {}, - ) { - const encoder = new TextEncoder(); - return writeFile(path, encoder.encode(data), options); - } - - window.__bootstrap.writeFile = { - writeTextFile, - writeTextFileSync, - writeFile, - writeFileSync, - }; -})(this); diff --git a/cli/rt/41_prompt.js b/cli/rt/41_prompt.js deleted file mode 100644 index ec294668b..000000000 --- a/cli/rt/41_prompt.js +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -((window) => { - const { stdin } = window.__bootstrap.files; - const { isatty } = window.__bootstrap.tty; - const LF = "\n".charCodeAt(0); - const CR = "\r".charCodeAt(0); - const decoder = new TextDecoder(); - const core = window.Deno.core; - - function alert(message = "Alert") { - if (!isatty(stdin.rid)) { - return; - } - - core.print(`${message} [Enter] `, false); - - readLineFromStdinSync(); - } - - function confirm(message = "Confirm") { - if (!isatty(stdin.rid)) { - return false; - } - - core.print(`${message} [y/N] `, false); - - const answer = readLineFromStdinSync(); - - return answer === "Y" || answer === "y"; - } - - function prompt(message = "Prompt", defaultValue) { - defaultValue ??= null; - - if (!isatty(stdin.rid)) { - return null; - } - - core.print(`${message} `, false); - - if (defaultValue) { - core.print(`[${defaultValue}] `, false); - } - - return readLineFromStdinSync() || defaultValue; - } - - function readLineFromStdinSync() { - const c = new Uint8Array(1); - const buf = []; - - while (true) { - const n = stdin.readSync(c); - if (n === null || n === 0) { - break; - } - if (c[0] === CR) { - const n = stdin.readSync(c); - if (c[0] === LF) { - break; - } - buf.push(CR); - if (n === null || n === 0) { - break; - } - } - if (c[0] === LF) { - break; - } - buf.push(c[0]); - } - return decoder.decode(new Uint8Array(buf)); - } - - window.__bootstrap.prompt = { - alert, - confirm, - prompt, - }; -})(this); diff --git a/cli/rt/90_deno_ns.js b/cli/rt/90_deno_ns.js deleted file mode 100644 index 9188788ec..000000000 --- a/cli/rt/90_deno_ns.js +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const __bootstrap = window.__bootstrap; - __bootstrap.denoNs = { - test: __bootstrap.testing.test, - metrics: __bootstrap.metrics.metrics, - Process: __bootstrap.process.Process, - run: __bootstrap.process.run, - isatty: __bootstrap.tty.isatty, - writeFileSync: __bootstrap.writeFile.writeFileSync, - writeFile: __bootstrap.writeFile.writeFile, - writeTextFileSync: __bootstrap.writeFile.writeTextFileSync, - writeTextFile: __bootstrap.writeFile.writeTextFile, - readTextFile: __bootstrap.readFile.readTextFile, - readTextFileSync: __bootstrap.readFile.readTextFileSync, - readFile: __bootstrap.readFile.readFile, - readFileSync: __bootstrap.readFile.readFileSync, - watchFs: __bootstrap.fsEvents.watchFs, - chmodSync: __bootstrap.fs.chmodSync, - chmod: __bootstrap.fs.chmod, - chown: __bootstrap.fs.chown, - chownSync: __bootstrap.fs.chownSync, - copyFileSync: __bootstrap.fs.copyFileSync, - cwd: __bootstrap.fs.cwd, - makeTempDirSync: __bootstrap.fs.makeTempDirSync, - makeTempDir: __bootstrap.fs.makeTempDir, - makeTempFileSync: __bootstrap.fs.makeTempFileSync, - makeTempFile: __bootstrap.fs.makeTempFile, - mkdirSync: __bootstrap.fs.mkdirSync, - mkdir: __bootstrap.fs.mkdir, - chdir: __bootstrap.fs.chdir, - copyFile: __bootstrap.fs.copyFile, - readDirSync: __bootstrap.fs.readDirSync, - readDir: __bootstrap.fs.readDir, - readLinkSync: __bootstrap.fs.readLinkSync, - readLink: __bootstrap.fs.readLink, - realPathSync: __bootstrap.fs.realPathSync, - realPath: __bootstrap.fs.realPath, - removeSync: __bootstrap.fs.removeSync, - remove: __bootstrap.fs.remove, - renameSync: __bootstrap.fs.renameSync, - rename: __bootstrap.fs.rename, - version: __bootstrap.version.version, - build: __bootstrap.build.build, - statSync: __bootstrap.fs.statSync, - lstatSync: __bootstrap.fs.lstatSync, - stat: __bootstrap.fs.stat, - lstat: __bootstrap.fs.lstat, - truncateSync: __bootstrap.fs.truncateSync, - truncate: __bootstrap.fs.truncate, - errors: __bootstrap.errors.errors, - customInspect: __bootstrap.console.customInspect, - inspect: __bootstrap.console.inspect, - env: __bootstrap.os.env, - exit: __bootstrap.os.exit, - execPath: __bootstrap.os.execPath, - Buffer: __bootstrap.buffer.Buffer, - readAll: __bootstrap.buffer.readAll, - readAllSync: __bootstrap.buffer.readAllSync, - writeAll: __bootstrap.buffer.writeAll, - writeAllSync: __bootstrap.buffer.writeAllSync, - copy: __bootstrap.io.copy, - iter: __bootstrap.io.iter, - iterSync: __bootstrap.io.iterSync, - SeekMode: __bootstrap.io.SeekMode, - read: __bootstrap.io.read, - readSync: __bootstrap.io.readSync, - write: __bootstrap.io.write, - writeSync: __bootstrap.io.writeSync, - File: __bootstrap.files.File, - open: __bootstrap.files.open, - openSync: __bootstrap.files.openSync, - create: __bootstrap.files.create, - createSync: __bootstrap.files.createSync, - stdin: __bootstrap.files.stdin, - stdout: __bootstrap.files.stdout, - stderr: __bootstrap.files.stderr, - seek: __bootstrap.files.seek, - seekSync: __bootstrap.files.seekSync, - connect: __bootstrap.net.connect, - listen: __bootstrap.net.listen, - connectTls: __bootstrap.tls.connectTls, - listenTls: __bootstrap.tls.listenTls, - sleepSync: __bootstrap.timers.sleepSync, - fsyncSync: __bootstrap.fs.fsyncSync, - fsync: __bootstrap.fs.fsync, - fdatasyncSync: __bootstrap.fs.fdatasyncSync, - fdatasync: __bootstrap.fs.fdatasync, - }; - - __bootstrap.denoNsUnstable = { - signal: __bootstrap.signals.signal, - signals: __bootstrap.signals.signals, - Signal: __bootstrap.signals.Signal, - SignalStream: __bootstrap.signals.SignalStream, - transpileOnly: __bootstrap.compilerApi.transpileOnly, - compile: __bootstrap.compilerApi.compile, - bundle: __bootstrap.compilerApi.bundle, - permissions: __bootstrap.permissions.permissions, - Permissions: __bootstrap.permissions.Permissions, - PermissionStatus: __bootstrap.permissions.PermissionStatus, - openPlugin: __bootstrap.plugins.openPlugin, - kill: __bootstrap.process.kill, - setRaw: __bootstrap.tty.setRaw, - consoleSize: __bootstrap.tty.consoleSize, - DiagnosticCategory: __bootstrap.diagnostics.DiagnosticCategory, - loadavg: __bootstrap.os.loadavg, - hostname: __bootstrap.os.hostname, - osRelease: __bootstrap.os.osRelease, - systemMemoryInfo: __bootstrap.os.systemMemoryInfo, - systemCpuInfo: __bootstrap.os.systemCpuInfo, - applySourceMap: __bootstrap.errorStack.opApplySourceMap, - formatDiagnostics: __bootstrap.errorStack.opFormatDiagnostics, - shutdown: __bootstrap.net.shutdown, - ShutdownMode: __bootstrap.net.ShutdownMode, - listen: __bootstrap.netUnstable.listen, - connect: __bootstrap.netUnstable.connect, - listenDatagram: __bootstrap.netUnstable.listenDatagram, - startTls: __bootstrap.tls.startTls, - fstatSync: __bootstrap.fs.fstatSync, - fstat: __bootstrap.fs.fstat, - ftruncateSync: __bootstrap.fs.ftruncateSync, - ftruncate: __bootstrap.fs.ftruncate, - umask: __bootstrap.fs.umask, - link: __bootstrap.fs.link, - linkSync: __bootstrap.fs.linkSync, - futime: __bootstrap.fs.futime, - futimeSync: __bootstrap.fs.futimeSync, - utime: __bootstrap.fs.utime, - utimeSync: __bootstrap.fs.utimeSync, - symlink: __bootstrap.fs.symlink, - symlinkSync: __bootstrap.fs.symlinkSync, - HttpClient: __bootstrap.fetch.HttpClient, - createHttpClient: __bootstrap.fetch.createHttpClient, - }; -})(this); diff --git a/cli/rt/99_main.js b/cli/rt/99_main.js deleted file mode 100644 index f38d51936..000000000 --- a/cli/rt/99_main.js +++ /dev/null @@ -1,395 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -// Removes the `__proto__` for security reasons. This intentionally makes -// Deno non compliant with ECMA-262 Annex B.2.2.1 -// -delete Object.prototype.__proto__; - -((window) => { - const core = Deno.core; - const util = window.__bootstrap.util; - const eventTarget = window.__bootstrap.eventTarget; - const globalInterfaces = window.__bootstrap.globalInterfaces; - const dispatchMinimal = window.__bootstrap.dispatchMinimal; - const build = window.__bootstrap.build; - const version = window.__bootstrap.version; - const errorStack = window.__bootstrap.errorStack; - const os = window.__bootstrap.os; - const timers = window.__bootstrap.timers; - const Console = window.__bootstrap.console.Console; - const worker = window.__bootstrap.worker; - const signals = window.__bootstrap.signals; - const { internalSymbol, internalObject } = window.__bootstrap.internals; - const performance = window.__bootstrap.performance; - const crypto = window.__bootstrap.crypto; - const url = window.__bootstrap.url; - const headers = window.__bootstrap.headers; - const streams = window.__bootstrap.streams; - const fileReader = window.__bootstrap.fileReader; - const webSocket = window.__bootstrap.webSocket; - const fetch = window.__bootstrap.fetch; - const prompt = window.__bootstrap.prompt; - const denoNs = window.__bootstrap.denoNs; - const denoNsUnstable = window.__bootstrap.denoNsUnstable; - const errors = window.__bootstrap.errors.errors; - const { defineEventHandler } = window.__bootstrap.webUtil; - - let windowIsClosing = false; - - function windowClose() { - if (!windowIsClosing) { - windowIsClosing = true; - // Push a macrotask to exit after a promise resolve. - // This is not perfect, but should be fine for first pass. - Promise.resolve().then(() => - timers.setTimeout.call( - null, - () => { - // This should be fine, since only Window/MainWorker has .close() - os.exit(0); - }, - 0, - ) - ); - } - } - - const encoder = new TextEncoder(); - - function workerClose() { - if (isClosing) { - return; - } - - isClosing = true; - opCloseWorker(); - } - - // TODO(bartlomieju): remove these functions - // Stuff for workers - const onmessage = () => {}; - const onerror = () => {}; - - function postMessage(data) { - const dataJson = JSON.stringify(data); - const dataIntArray = encoder.encode(dataJson); - opPostMessage(dataIntArray); - } - - let isClosing = false; - async function workerMessageRecvCallback(data) { - const msgEvent = new MessageEvent("message", { - cancelable: false, - data, - }); - - try { - if (globalThis["onmessage"]) { - const result = globalThis.onmessage(msgEvent); - if (result && "then" in result) { - await result; - } - } - globalThis.dispatchEvent(msgEvent); - } catch (e) { - let handled = false; - - const errorEvent = new ErrorEvent("error", { - cancelable: true, - message: e.message, - lineno: e.lineNumber ? e.lineNumber + 1 : undefined, - colno: e.columnNumber ? e.columnNumber + 1 : undefined, - filename: e.fileName, - error: null, - }); - - if (globalThis["onerror"]) { - const ret = globalThis.onerror( - e.message, - e.fileName, - e.lineNumber, - e.columnNumber, - e, - ); - handled = ret === true; - } - - globalThis.dispatchEvent(errorEvent); - if (errorEvent.defaultPrevented) { - handled = true; - } - - if (!handled) { - throw e; - } - } - } - - function opPostMessage(data) { - core.jsonOpSync("op_worker_post_message", {}, data); - } - - function opCloseWorker() { - core.jsonOpSync("op_worker_close"); - } - - function opMainModule() { - return core.jsonOpSync("op_main_module"); - } - - function runtimeStart(runtimeOptions, source) { - const opsMap = core.ops(); - for (const [name, opId] of Object.entries(opsMap)) { - if (name === "op_write" || name === "op_read") { - core.setAsyncHandler(opId, dispatchMinimal.asyncMsgFromRust); - } - } - - core.setMacrotaskCallback(timers.handleTimerMacrotask); - version.setVersions( - runtimeOptions.denoVersion, - runtimeOptions.v8Version, - runtimeOptions.tsVersion, - ); - build.setBuildInfo(runtimeOptions.target); - util.setLogDebug(runtimeOptions.debugFlag, source); - // TODO(bartlomieju): a very crude way to disable - // source mapping of errors. This condition is true - // only for compiled standalone binaries. - let prepareStackTrace; - if (runtimeOptions.applySourceMaps) { - prepareStackTrace = core.createPrepareStackTrace( - errorStack.opApplySourceMap, - ); - } else { - prepareStackTrace = core.createPrepareStackTrace(); - } - Error.prepareStackTrace = prepareStackTrace; - } - - function registerErrors() { - core.registerErrorClass("NotFound", errors.NotFound); - core.registerErrorClass("PermissionDenied", errors.PermissionDenied); - core.registerErrorClass("ConnectionRefused", errors.ConnectionRefused); - core.registerErrorClass("ConnectionReset", errors.ConnectionReset); - core.registerErrorClass("ConnectionAborted", errors.ConnectionAborted); - core.registerErrorClass("NotConnected", errors.NotConnected); - core.registerErrorClass("AddrInUse", errors.AddrInUse); - core.registerErrorClass("AddrNotAvailable", errors.AddrNotAvailable); - core.registerErrorClass("BrokenPipe", errors.BrokenPipe); - core.registerErrorClass("AlreadyExists", errors.AlreadyExists); - core.registerErrorClass("InvalidData", errors.InvalidData); - core.registerErrorClass("TimedOut", errors.TimedOut); - core.registerErrorClass("Interrupted", errors.Interrupted); - core.registerErrorClass("WriteZero", errors.WriteZero); - core.registerErrorClass("UnexpectedEof", errors.UnexpectedEof); - core.registerErrorClass("BadResource", errors.BadResource); - core.registerErrorClass("Http", errors.Http); - core.registerErrorClass("Busy", errors.Busy); - core.registerErrorClass("NotSupported", errors.NotSupported); - core.registerErrorClass("Error", Error); - core.registerErrorClass("RangeError", RangeError); - core.registerErrorClass("ReferenceError", ReferenceError); - core.registerErrorClass("SyntaxError", SyntaxError); - core.registerErrorClass("TypeError", TypeError); - core.registerErrorClass("URIError", URIError); - } - - // https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope - const windowOrWorkerGlobalScope = { - Blob: util.nonEnumerable(fetch.Blob), - ByteLengthQueuingStrategy: util.nonEnumerable( - streams.ByteLengthQueuingStrategy, - ), - CloseEvent: util.nonEnumerable(CloseEvent), - CountQueuingStrategy: util.nonEnumerable( - streams.CountQueuingStrategy, - ), - CustomEvent: util.nonEnumerable(CustomEvent), - DOMException: util.nonEnumerable(DOMException), - ErrorEvent: util.nonEnumerable(ErrorEvent), - Event: util.nonEnumerable(Event), - EventTarget: util.nonEnumerable(EventTarget), - File: util.nonEnumerable(fetch.DomFile), - FileReader: util.nonEnumerable(fileReader.FileReader), - FormData: util.nonEnumerable(fetch.FormData), - Headers: util.nonEnumerable(headers.Headers), - MessageEvent: util.nonEnumerable(MessageEvent), - Performance: util.nonEnumerable(performance.Performance), - PerformanceEntry: util.nonEnumerable(performance.PerformanceEntry), - PerformanceMark: util.nonEnumerable(performance.PerformanceMark), - PerformanceMeasure: util.nonEnumerable(performance.PerformanceMeasure), - ProgressEvent: util.nonEnumerable(ProgressEvent), - ReadableStream: util.nonEnumerable(streams.ReadableStream), - Request: util.nonEnumerable(fetch.Request), - Response: util.nonEnumerable(fetch.Response), - TextDecoder: util.nonEnumerable(TextDecoder), - TextEncoder: util.nonEnumerable(TextEncoder), - TransformStream: util.nonEnumerable(streams.TransformStream), - URL: util.nonEnumerable(url.URL), - URLSearchParams: util.nonEnumerable(url.URLSearchParams), - WebSocket: util.nonEnumerable(webSocket.WebSocket), - Worker: util.nonEnumerable(worker.Worker), - WritableStream: util.nonEnumerable(streams.WritableStream), - atob: util.writable(atob), - btoa: util.writable(btoa), - clearInterval: util.writable(timers.clearInterval), - clearTimeout: util.writable(timers.clearTimeout), - console: util.writable(new Console(core.print)), - crypto: util.readOnly(crypto), - fetch: util.writable(fetch.fetch), - performance: util.writable(performance.performance), - setInterval: util.writable(timers.setInterval), - setTimeout: util.writable(timers.setTimeout), - }; - - const mainRuntimeGlobalProperties = { - Window: globalInterfaces.windowConstructorDescriptor, - window: util.readOnly(globalThis), - self: util.readOnly(globalThis), - // TODO(bartlomieju): from MDN docs (https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope) - // it seems those two properties should be available to workers as well - onload: util.writable(null), - onunload: util.writable(null), - close: util.writable(windowClose), - closed: util.getterOnly(() => windowIsClosing), - alert: util.writable(prompt.alert), - confirm: util.writable(prompt.confirm), - prompt: util.writable(prompt.prompt), - }; - - const workerRuntimeGlobalProperties = { - WorkerGlobalScope: globalInterfaces.workerGlobalScopeConstructorDescriptor, - DedicatedWorkerGlobalScope: - globalInterfaces.dedicatedWorkerGlobalScopeConstructorDescriptor, - self: util.readOnly(globalThis), - onmessage: util.writable(onmessage), - onerror: util.writable(onerror), - // TODO: should be readonly? - close: util.nonEnumerable(workerClose), - postMessage: util.writable(postMessage), - workerMessageRecvCallback: util.nonEnumerable(workerMessageRecvCallback), - }; - - let hasBootstrapped = false; - - function bootstrapMainRuntime(runtimeOptions) { - if (hasBootstrapped) { - throw new Error("Worker runtime already bootstrapped"); - } - // Remove bootstrapping data from the global scope - delete globalThis.__bootstrap; - delete globalThis.bootstrap; - util.log("bootstrapMainRuntime"); - hasBootstrapped = true; - Object.defineProperties(globalThis, windowOrWorkerGlobalScope); - Object.defineProperties(globalThis, mainRuntimeGlobalProperties); - Object.setPrototypeOf(globalThis, Window.prototype); - eventTarget.setEventTargetData(globalThis); - - defineEventHandler(window, "load", null); - defineEventHandler(window, "unload", null); - - runtimeStart(runtimeOptions); - const { args, noColor, pid, ppid, unstableFlag } = runtimeOptions; - - registerErrors(); - - const finalDenoNs = { - core, - internal: internalSymbol, - [internalSymbol]: internalObject, - resources: core.resources, - close: core.close, - ...denoNs, - }; - Object.defineProperties(finalDenoNs, { - pid: util.readOnly(pid), - ppid: util.readOnly(ppid), - noColor: util.readOnly(noColor), - args: util.readOnly(Object.freeze(args)), - mainModule: util.getterOnly(opMainModule), - }); - - if (unstableFlag) { - Object.assign(finalDenoNs, denoNsUnstable); - } - - // Setup `Deno` global - we're actually overriding already - // existing global `Deno` with `Deno` namespace from "./deno.ts". - util.immutableDefine(globalThis, "Deno", finalDenoNs); - Object.freeze(globalThis.Deno); - Object.freeze(globalThis.Deno.core); - Object.freeze(globalThis.Deno.core.sharedQueue); - signals.setSignals(); - - util.log("args", args); - } - - function bootstrapWorkerRuntime( - runtimeOptions, - name, - useDenoNamespace, - internalName, - ) { - if (hasBootstrapped) { - throw new Error("Worker runtime already bootstrapped"); - } - // Remove bootstrapping data from the global scope - delete globalThis.__bootstrap; - delete globalThis.bootstrap; - util.log("bootstrapWorkerRuntime"); - hasBootstrapped = true; - Object.defineProperties(globalThis, windowOrWorkerGlobalScope); - Object.defineProperties(globalThis, workerRuntimeGlobalProperties); - Object.defineProperties(globalThis, { name: util.readOnly(name) }); - Object.setPrototypeOf(globalThis, DedicatedWorkerGlobalScope.prototype); - eventTarget.setEventTargetData(globalThis); - - runtimeStart( - runtimeOptions, - internalName ?? name, - ); - const { unstableFlag, pid, noColor, args } = runtimeOptions; - - registerErrors(); - - const finalDenoNs = { - core, - internal: internalSymbol, - [internalSymbol]: internalObject, - resources: core.resources, - close: core.close, - ...denoNs, - }; - if (useDenoNamespace) { - if (unstableFlag) { - Object.assign(finalDenoNs, denoNsUnstable); - } - Object.defineProperties(finalDenoNs, { - pid: util.readOnly(pid), - noColor: util.readOnly(noColor), - args: util.readOnly(Object.freeze(args)), - }); - // Setup `Deno` global - we're actually overriding already - // existing global `Deno` with `Deno` namespace from "./deno.ts". - util.immutableDefine(globalThis, "Deno", finalDenoNs); - Object.freeze(globalThis.Deno); - Object.freeze(globalThis.Deno.core); - Object.freeze(globalThis.Deno.core.sharedQueue); - signals.setSignals(); - } else { - delete globalThis.Deno; - util.assert(globalThis.Deno === undefined); - } - } - - Object.defineProperties(globalThis, { - bootstrap: { - value: { - mainRuntime: bootstrapMainRuntime, - workerRuntime: bootstrapWorkerRuntime, - }, - configurable: true, - }, - }); -})(this); diff --git a/cli/rt/README.md b/cli/rt/README.md deleted file mode 100644 index b17fa22e5..000000000 --- a/cli/rt/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# Runtime JavaScript Code - -This directory contains Deno runtime code written in plain JavaScript. - -Each file is a plain, old **script**, not ES modules. The reason is that -snapshotting ES modules is much harder, especially if one needs to manipulate -global scope (like in case of Deno). - -Each file is prefixed with a number, telling in which order scripts should be -loaded into V8 isolate. This is temporary solution and we're striving not to -require specific order (though it's not 100% obvious if that's feasible). - -## Deno Web APIs - -This directory facilities Web APIs that are available in Deno. - -Please note, that some implementations might not be completely aligned with -specification. - -Some Web APIs are using ops under the hood, eg. `console`, `performance`. - -## Implemented Web APIs - -- [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob): for - representing opaque binary data. -- [Console](https://developer.mozilla.org/en-US/docs/Web/API/Console): for - logging purposes. -- [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent), - [EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) - and - [EventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventListener): - to work with DOM events. - - **Implementation notes:** There is no DOM hierarchy in Deno, so there is no - tree for Events to bubble/capture through. -- [fetch](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch), - [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request), - [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response), - [Body](https://developer.mozilla.org/en-US/docs/Web/API/Body) and - [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers): modern - Promise-based HTTP Request API. -- [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData): access - to a `multipart/form-data` serialization. -- [Performance](https://developer.mozilla.org/en-US/docs/Web/API/Performance): - retrieving current time with a high precision. -- [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout), - [setInterval](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval), - [clearTimeout](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearTimeout): - scheduling callbacks in future and - [clearInterval](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearInterval). -- [Stream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) for - creating, composing, and consuming streams of data. -- [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) and - [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams): - to construct and parse URLSs. -- [Worker](https://developer.mozilla.org/en-US/docs/Web/API/Worker): executing - additional code in a separate thread. - - **Implementation notes:** Blob URLs are not supported, object ownership - cannot be transferred, posted data is serialized to JSON instead of - [structured cloning](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). diff --git a/cli/signal.rs b/cli/signal.rs deleted file mode 100644 index b597714f4..000000000 --- a/cli/signal.rs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use deno_core::error::AnyError; - -#[cfg(not(unix))] -use deno_core::error::last_os_error; -#[cfg(not(unix))] -use deno_core::error::type_error; - -#[cfg(not(unix))] -const SIGINT: i32 = 2; -#[cfg(not(unix))] -const SIGKILL: i32 = 9; -#[cfg(not(unix))] -const SIGTERM: i32 = 15; - -#[cfg(not(unix))] -use winapi::{ - shared::minwindef::DWORD, - um::{ - handleapi::CloseHandle, - processthreadsapi::{OpenProcess, TerminateProcess}, - winnt::PROCESS_TERMINATE, - }, -}; - -#[cfg(unix)] -pub fn kill(pid: i32, signo: i32) -> Result<(), AnyError> { - use nix::sys::signal::{kill as unix_kill, Signal}; - use nix::unistd::Pid; - use std::convert::TryFrom; - let sig = Signal::try_from(signo)?; - unix_kill(Pid::from_raw(pid), Option::Some(sig)).map_err(AnyError::from) -} - -#[cfg(not(unix))] -pub fn kill(pid: i32, signal: i32) -> Result<(), AnyError> { - match signal { - SIGINT | SIGKILL | SIGTERM => { - if pid <= 0 { - return Err(type_error("unsupported pid")); - } - unsafe { - let handle = OpenProcess(PROCESS_TERMINATE, 0, pid as DWORD); - if handle.is_null() { - return Err(last_os_error()); - } - if TerminateProcess(handle, 1) == 0 { - CloseHandle(handle); - return Err(last_os_error()); - } - if CloseHandle(handle) == 0 { - return Err(last_os_error()); - } - } - } - _ => { - return Err(type_error("unsupported signal")); - } - } - Ok(()) -} diff --git a/cli/specifier_handler.rs b/cli/specifier_handler.rs index 083316740..02a1196d3 100644 --- a/cli/specifier_handler.rs +++ b/cli/specifier_handler.rs @@ -5,8 +5,8 @@ use crate::deno_dir::DenoDir; use crate::disk_cache::DiskCache; use crate::file_fetcher::FileFetcher; use crate::media_type::MediaType; -use crate::permissions::Permissions; use crate::program_state::ProgramState; +use deno_runtime::permissions::Permissions; use deno_core::error::custom_error; use deno_core::error::AnyError; diff --git a/cli/standalone.rs b/cli/standalone.rs index 6559242bd..fea42fc96 100644 --- a/cli/standalone.rs +++ b/cli/standalone.rs @@ -1,10 +1,7 @@ use crate::colors; use crate::flags::Flags; -use crate::permissions::Permissions; use crate::tokio_util; use crate::version; -use crate::worker::MainWorker; -use crate::worker::WorkerOptions; use deno_core::error::bail; use deno_core::error::type_error; use deno_core::error::AnyError; @@ -12,6 +9,9 @@ use deno_core::futures::FutureExt; use deno_core::ModuleLoader; use deno_core::ModuleSpecifier; use deno_core::OpState; +use deno_runtime::permissions::Permissions; +use deno_runtime::worker::MainWorker; +use deno_runtime::worker::WorkerOptions; use std::cell::RefCell; use std::convert::TryInto; use std::env::current_exe; @@ -135,6 +135,7 @@ async fn run(source_code: String, args: Vec) -> Result<(), AnyError> { runtime_version: version::deno(), ts_version: version::TYPESCRIPT.to_string(), no_color: !colors::use_color(), + get_error_class_fn: Some(&crate::errors::get_error_class_name), }; let mut worker = MainWorker::from_options(main_module.clone(), permissions, &options); diff --git a/cli/tools/coverage.rs b/cli/tools/coverage.rs index 726ce9749..229cb8020 100644 --- a/cli/tools/coverage.rs +++ b/cli/tools/coverage.rs @@ -1,11 +1,11 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use crate::colors; -use crate::inspector::InspectorSession; use deno_core::error::AnyError; use deno_core::serde_json; use deno_core::serde_json::json; use deno_core::url::Url; +use deno_runtime::inspector::InspectorSession; use serde::Deserialize; pub struct CoverageCollector { diff --git a/cli/tools/repl.rs b/cli/tools/repl.rs index 9b4d94a23..61d7809c3 100644 --- a/cli/tools/repl.rs +++ b/cli/tools/repl.rs @@ -3,13 +3,13 @@ use crate::ast; use crate::ast::TokenOrComment; use crate::colors; -use crate::inspector::InspectorSession; use crate::media_type::MediaType; use crate::program_state::ProgramState; -use crate::worker::MainWorker; use deno_core::error::AnyError; use deno_core::serde_json::json; use deno_core::serde_json::Value; +use deno_runtime::inspector::InspectorSession; +use deno_runtime::worker::MainWorker; use rustyline::completion::Completer; use rustyline::error::ReadlineError; use rustyline::highlight::Highlighter; diff --git a/cli/tools/upgrade.rs b/cli/tools/upgrade.rs index b76850dde..da26b3159 100644 --- a/cli/tools/upgrade.rs +++ b/cli/tools/upgrade.rs @@ -3,8 +3,8 @@ //! This module provides feature to upgrade deno executable use crate::AnyError; -use deno_fetch::reqwest; -use deno_fetch::reqwest::Client; +use deno_runtime::deno_fetch::reqwest; +use deno_runtime::deno_fetch::reqwest::Client; use semver_parser::version::parse as semver_parse; use std::fs; use std::path::Path; diff --git a/cli/web_worker.rs b/cli/web_worker.rs deleted file mode 100644 index 18d391580..000000000 --- a/cli/web_worker.rs +++ /dev/null @@ -1,588 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::colors; -use crate::inspector::DenoInspector; -use crate::inspector::InspectorServer; -use crate::js; -use crate::metrics::Metrics; -use crate::ops; -use crate::permissions::Permissions; -use crate::tokio_util::create_basic_runtime; -use deno_core::error::AnyError; -use deno_core::futures::channel::mpsc; -use deno_core::futures::future::poll_fn; -use deno_core::futures::future::FutureExt; -use deno_core::futures::stream::StreamExt; -use deno_core::futures::task::AtomicWaker; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::url::Url; -use deno_core::v8; -use deno_core::JsErrorCreateFn; -use deno_core::JsRuntime; -use deno_core::ModuleLoader; -use deno_core::ModuleSpecifier; -use deno_core::RuntimeOptions; -use std::env; -use std::rc::Rc; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::task::Context; -use std::task::Poll; -use tokio::sync::Mutex as AsyncMutex; - -/// Events that are sent to host from child -/// worker. -pub enum WorkerEvent { - Message(Box<[u8]>), - Error(AnyError), - TerminalError(AnyError), -} - -pub struct WorkerChannelsInternal { - pub sender: mpsc::Sender, - pub receiver: mpsc::Receiver>, -} - -/// Wrapper for `WorkerHandle` that adds functionality -/// for terminating workers. -/// -/// This struct is used by host as well as worker itself. -/// -/// Host uses it to communicate with worker and terminate it, -/// while worker uses it only to finish execution on `self.close()`. -#[derive(Clone)] -pub struct WebWorkerHandle { - pub sender: mpsc::Sender>, - pub receiver: Arc>>, - terminate_tx: mpsc::Sender<()>, - terminated: Arc, - isolate_handle: v8::IsolateHandle, -} - -impl WebWorkerHandle { - /// Post message to worker as a host. - pub fn post_message(&self, buf: Box<[u8]>) -> Result<(), AnyError> { - let mut sender = self.sender.clone(); - sender.try_send(buf)?; - Ok(()) - } - - /// Get the event with lock. - /// Return error if more than one listener tries to get event - pub async fn get_event(&self) -> Result, AnyError> { - let mut receiver = self.receiver.try_lock()?; - Ok(receiver.next().await) - } - - pub fn terminate(&self) { - // This function can be called multiple times by whomever holds - // the handle. However only a single "termination" should occur so - // we need a guard here. - let already_terminated = self.terminated.swap(true, Ordering::SeqCst); - - if !already_terminated { - self.isolate_handle.terminate_execution(); - let mut sender = self.terminate_tx.clone(); - // This call should be infallible hence the `expect`. - // This might change in the future. - sender.try_send(()).expect("Failed to terminate"); - } - } -} - -fn create_channels( - isolate_handle: v8::IsolateHandle, - terminate_tx: mpsc::Sender<()>, -) -> (WorkerChannelsInternal, WebWorkerHandle) { - let (in_tx, in_rx) = mpsc::channel::>(1); - let (out_tx, out_rx) = mpsc::channel::(1); - let internal_channels = WorkerChannelsInternal { - sender: out_tx, - receiver: in_rx, - }; - let external_channels = WebWorkerHandle { - sender: in_tx, - receiver: Arc::new(AsyncMutex::new(out_rx)), - terminated: Arc::new(AtomicBool::new(false)), - terminate_tx, - isolate_handle, - }; - (internal_channels, external_channels) -} - -/// This struct is an implementation of `Worker` Web API -/// -/// Each `WebWorker` is either a child of `MainWorker` or other -/// `WebWorker`. -pub struct WebWorker { - id: u32, - inspector: Option>, - // Following fields are pub because they are accessed - // when creating a new WebWorker instance. - pub(crate) internal_channels: WorkerChannelsInternal, - pub(crate) js_runtime: JsRuntime, - pub(crate) name: String, - waker: AtomicWaker, - event_loop_idle: bool, - terminate_rx: mpsc::Receiver<()>, - handle: WebWorkerHandle, - pub use_deno_namespace: bool, -} - -pub struct WebWorkerOptions { - /// Sets `Deno.args` in JS runtime. - pub args: Vec, - pub debug_flag: bool, - pub unstable: bool, - pub ca_filepath: Option, - pub user_agent: String, - pub seed: Option, - pub module_loader: Rc, - pub create_web_worker_cb: Arc, - pub js_error_create_fn: Option>, - pub use_deno_namespace: bool, - pub attach_inspector: bool, - pub maybe_inspector_server: Option>, - pub apply_source_maps: bool, - /// Sets `Deno.version.deno` in JS runtime. - pub runtime_version: String, - /// Sets `Deno.version.typescript` in JS runtime. - pub ts_version: String, - /// Sets `Deno.noColor` in JS runtime. - pub no_color: bool, -} - -impl WebWorker { - pub fn from_options( - name: String, - permissions: Permissions, - main_module: ModuleSpecifier, - worker_id: u32, - options: &WebWorkerOptions, - ) -> Self { - let mut js_runtime = JsRuntime::new(RuntimeOptions { - module_loader: Some(options.module_loader.clone()), - startup_snapshot: Some(js::deno_isolate_init()), - js_error_create_fn: options.js_error_create_fn.clone(), - get_error_class_fn: Some(&crate::errors::get_error_class_name), - ..Default::default() - }); - - let inspector = if options.attach_inspector { - Some(DenoInspector::new( - &mut js_runtime, - options.maybe_inspector_server.clone(), - )) - } else { - None - }; - - let (terminate_tx, terminate_rx) = mpsc::channel::<()>(1); - let isolate_handle = js_runtime.v8_isolate().thread_safe_handle(); - let (internal_channels, handle) = - create_channels(isolate_handle, terminate_tx); - - let mut worker = Self { - id: worker_id, - inspector, - internal_channels, - js_runtime, - name, - waker: AtomicWaker::new(), - event_loop_idle: false, - terminate_rx, - handle, - use_deno_namespace: options.use_deno_namespace, - }; - - { - let handle = worker.thread_safe_handle(); - let sender = worker.internal_channels.sender.clone(); - let js_runtime = &mut worker.js_runtime; - // All ops registered in this function depend on these - { - let op_state = js_runtime.op_state(); - let mut op_state = op_state.borrow_mut(); - op_state.put::(Default::default()); - op_state.put::(permissions); - op_state.put::(ops::UnstableChecker { - unstable: options.unstable, - }); - } - - ops::web_worker::init(js_runtime, sender.clone(), handle); - ops::runtime::init(js_runtime, main_module); - ops::fetch::init(js_runtime, options.ca_filepath.as_deref()); - ops::timers::init(js_runtime); - ops::worker_host::init( - js_runtime, - Some(sender), - options.create_web_worker_cb.clone(), - ); - ops::reg_json_sync(js_runtime, "op_close", deno_core::op_close); - ops::reg_json_sync(js_runtime, "op_resources", deno_core::op_resources); - ops::reg_json_sync( - js_runtime, - "op_domain_to_ascii", - deno_web::op_domain_to_ascii, - ); - ops::io::init(js_runtime); - ops::websocket::init( - js_runtime, - options.ca_filepath.as_deref(), - options.user_agent.clone(), - ); - - if options.use_deno_namespace { - ops::fs_events::init(js_runtime); - ops::fs::init(js_runtime); - ops::net::init(js_runtime); - ops::os::init(js_runtime); - ops::permissions::init(js_runtime); - ops::plugin::init(js_runtime); - ops::process::init(js_runtime); - ops::crypto::init(js_runtime, options.seed); - ops::signal::init(js_runtime); - ops::tls::init(js_runtime); - ops::tty::init(js_runtime); - - let op_state = js_runtime.op_state(); - let mut op_state = op_state.borrow_mut(); - let (stdin, stdout, stderr) = ops::io::get_stdio(); - if let Some(stream) = stdin { - op_state.resource_table.add("stdin", Box::new(stream)); - } - if let Some(stream) = stdout { - op_state.resource_table.add("stdout", Box::new(stream)); - } - if let Some(stream) = stderr { - op_state.resource_table.add("stderr", Box::new(stream)); - } - } - - worker - } - } - - pub fn bootstrap(&mut self, options: &WebWorkerOptions) { - let runtime_options = json!({ - "args": options.args, - "applySourceMaps": options.apply_source_maps, - "debugFlag": options.debug_flag, - "denoVersion": options.runtime_version, - "noColor": options.no_color, - "pid": std::process::id(), - "ppid": ops::runtime::ppid(), - "target": env!("TARGET"), - "tsVersion": options.ts_version, - "unstableFlag": options.unstable, - "v8Version": deno_core::v8_version(), - }); - - let runtime_options_str = - serde_json::to_string_pretty(&runtime_options).unwrap(); - - // Instead of using name for log we use `worker-${id}` because - // WebWorkers can have empty string as name. - let script = format!( - "bootstrap.workerRuntime({}, \"{}\", {}, \"worker-{}\")", - runtime_options_str, self.name, options.use_deno_namespace, self.id - ); - self - .execute(&script) - .expect("Failed to execute worker bootstrap script"); - } - - /// Same as execute2() but the filename defaults to "$CWD/__anonymous__". - pub fn execute(&mut self, js_source: &str) -> Result<(), AnyError> { - let path = env::current_dir().unwrap().join("__anonymous__"); - let url = Url::from_file_path(path).unwrap(); - self.js_runtime.execute(url.as_str(), js_source) - } - - /// Loads, instantiates and executes specified JavaScript module. - pub async fn execute_module( - &mut self, - module_specifier: &ModuleSpecifier, - ) -> Result<(), AnyError> { - let id = self.js_runtime.load_module(module_specifier, None).await?; - self.js_runtime.mod_evaluate(id).await - } - - /// Returns a way to communicate with the Worker from other threads. - pub fn thread_safe_handle(&self) -> WebWorkerHandle { - self.handle.clone() - } - - pub fn has_been_terminated(&self) -> bool { - self.handle.terminated.load(Ordering::SeqCst) - } - - pub fn poll_event_loop( - &mut self, - cx: &mut Context, - ) -> Poll> { - if self.has_been_terminated() { - return Poll::Ready(Ok(())); - } - - if !self.event_loop_idle { - let poll_result = { - // We always poll the inspector if it exists. - let _ = self.inspector.as_mut().map(|i| i.poll_unpin(cx)); - self.waker.register(cx.waker()); - self.js_runtime.poll_event_loop(cx) - }; - - if let Poll::Ready(r) = poll_result { - if self.has_been_terminated() { - return Poll::Ready(Ok(())); - } - - if let Err(e) = r { - print_worker_error(e.to_string(), &self.name); - let mut sender = self.internal_channels.sender.clone(); - sender - .try_send(WorkerEvent::Error(e)) - .expect("Failed to post message to host"); - } - self.event_loop_idle = true; - } - } - - if let Poll::Ready(r) = self.terminate_rx.poll_next_unpin(cx) { - // terminate_rx should never be closed - assert!(r.is_some()); - return Poll::Ready(Ok(())); - } - - let maybe_msg_poll_result = - self.internal_channels.receiver.poll_next_unpin(cx); - - if let Poll::Ready(maybe_msg) = maybe_msg_poll_result { - let msg = - maybe_msg.expect("Received `None` instead of message in worker"); - let msg = String::from_utf8(msg.to_vec()).unwrap(); - let script = format!("workerMessageRecvCallback({})", msg); - - if let Err(e) = self.execute(&script) { - // If execution was terminated during message callback then - // just ignore it - if self.has_been_terminated() { - return Poll::Ready(Ok(())); - } - - // Otherwise forward error to host - let mut sender = self.internal_channels.sender.clone(); - sender - .try_send(WorkerEvent::Error(e)) - .expect("Failed to post message to host"); - } - - // Let event loop be polled again - self.event_loop_idle = false; - self.waker.wake(); - } - - Poll::Pending - } - - pub async fn run_event_loop(&mut self) -> Result<(), AnyError> { - poll_fn(|cx| self.poll_event_loop(cx)).await - } -} - -impl Drop for WebWorker { - fn drop(&mut self) { - // The Isolate object must outlive the Inspector object, but this is - // currently not enforced by the type system. - self.inspector.take(); - } -} - -fn print_worker_error(error_str: String, name: &str) { - eprintln!( - "{}: Uncaught (in worker \"{}\") {}", - colors::red_bold("error"), - name, - error_str.trim_start_matches("Uncaught "), - ); -} - -/// This function should be called from a thread dedicated to this worker. -// TODO(bartlomieju): check if order of actions is aligned to Worker spec -pub fn run_web_worker( - mut worker: WebWorker, - specifier: ModuleSpecifier, - maybe_source_code: Option, -) -> Result<(), AnyError> { - let name = worker.name.to_string(); - - let mut rt = create_basic_runtime(); - - // TODO(bartlomieju): run following block using "select!" - // with terminate - - // Execute provided source code immediately - let result = if let Some(source_code) = maybe_source_code { - worker.execute(&source_code) - } else { - // TODO(bartlomieju): add "type": "classic", ie. ability to load - // script instead of module - let load_future = worker.execute_module(&specifier).boxed_local(); - - rt.block_on(load_future) - }; - - let mut sender = worker.internal_channels.sender.clone(); - - // If sender is closed it means that worker has already been closed from - // within using "globalThis.close()" - if sender.is_closed() { - return Ok(()); - } - - if let Err(e) = result { - print_worker_error(e.to_string(), &name); - sender - .try_send(WorkerEvent::TerminalError(e)) - .expect("Failed to post message to host"); - - // Failure to execute script is a terminal error, bye, bye. - return Ok(()); - } - - let result = rt.block_on(worker.run_event_loop()); - debug!("Worker thread shuts down {}", &name); - result -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tokio_util; - use deno_core::serde_json::json; - - fn create_test_web_worker() -> WebWorker { - let main_module = - ModuleSpecifier::resolve_url_or_path("./hello.js").unwrap(); - let module_loader = Rc::new(deno_core::NoopModuleLoader); - let create_web_worker_cb = Arc::new(|_| unreachable!()); - - let options = WebWorkerOptions { - args: vec![], - apply_source_maps: false, - debug_flag: false, - unstable: false, - ca_filepath: None, - user_agent: "x".to_string(), - seed: None, - module_loader, - create_web_worker_cb, - js_error_create_fn: None, - use_deno_namespace: false, - attach_inspector: false, - maybe_inspector_server: None, - runtime_version: "x".to_string(), - ts_version: "x".to_string(), - no_color: true, - }; - - let mut worker = WebWorker::from_options( - "TEST".to_string(), - Permissions::allow_all(), - main_module, - 1, - &options, - ); - worker.bootstrap(&options); - worker - } - - #[tokio::test] - async fn test_worker_messages() { - let (handle_sender, handle_receiver) = - std::sync::mpsc::sync_channel::(1); - - let join_handle = std::thread::spawn(move || { - let mut worker = create_test_web_worker(); - let source = r#" - onmessage = function(e) { - console.log("msg from main script", e.data); - if (e.data == "exit") { - return close(); - } else { - console.assert(e.data === "hi"); - } - postMessage([1, 2, 3]); - console.log("after postMessage"); - } - "#; - worker.execute(source).unwrap(); - let handle = worker.thread_safe_handle(); - handle_sender.send(handle).unwrap(); - let r = tokio_util::run_basic(worker.run_event_loop()); - assert!(r.is_ok()) - }); - - let mut handle = handle_receiver.recv().unwrap(); - - let msg = json!("hi").to_string().into_boxed_str().into_boxed_bytes(); - let r = handle.post_message(msg.clone()); - assert!(r.is_ok()); - - let maybe_msg = handle.get_event().await.unwrap(); - assert!(maybe_msg.is_some()); - - let r = handle.post_message(msg.clone()); - assert!(r.is_ok()); - - let maybe_msg = handle.get_event().await.unwrap(); - assert!(maybe_msg.is_some()); - match maybe_msg { - Some(WorkerEvent::Message(buf)) => { - assert_eq!(*buf, *b"[1,2,3]"); - } - _ => unreachable!(), - } - - let msg = json!("exit") - .to_string() - .into_boxed_str() - .into_boxed_bytes(); - let r = handle.post_message(msg); - assert!(r.is_ok()); - let event = handle.get_event().await.unwrap(); - assert!(event.is_none()); - handle.sender.close_channel(); - join_handle.join().expect("Failed to join worker thread"); - } - - #[tokio::test] - async fn removed_from_resource_table_on_close() { - let (handle_sender, handle_receiver) = - std::sync::mpsc::sync_channel::(1); - - let join_handle = std::thread::spawn(move || { - let mut worker = create_test_web_worker(); - worker.execute("onmessage = () => { close(); }").unwrap(); - let handle = worker.thread_safe_handle(); - handle_sender.send(handle).unwrap(); - let r = tokio_util::run_basic(worker.run_event_loop()); - assert!(r.is_ok()) - }); - - let mut handle = handle_receiver.recv().unwrap(); - - let msg = json!("hi").to_string().into_boxed_str().into_boxed_bytes(); - let r = handle.post_message(msg.clone()); - assert!(r.is_ok()); - let event = handle.get_event().await.unwrap(); - assert!(event.is_none()); - handle.sender.close_channel(); - - join_handle.join().expect("Failed to join worker thread"); - } -} diff --git a/cli/worker.rs b/cli/worker.rs deleted file mode 100644 index d1238df41..000000000 --- a/cli/worker.rs +++ /dev/null @@ -1,342 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::inspector::DenoInspector; -use crate::inspector::InspectorServer; -use crate::inspector::InspectorSession; -use crate::js; -use crate::metrics::Metrics; -use crate::ops; -use crate::permissions::Permissions; -use deno_core::error::AnyError; -use deno_core::futures::future::poll_fn; -use deno_core::futures::future::FutureExt; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::url::Url; -use deno_core::JsErrorCreateFn; -use deno_core::JsRuntime; -use deno_core::ModuleId; -use deno_core::ModuleLoader; -use deno_core::ModuleSpecifier; -use deno_core::RuntimeOptions; -use std::env; -use std::rc::Rc; -use std::sync::Arc; -use std::task::Context; -use std::task::Poll; - -/// This worker is created and used by almost all -/// subcommands in Deno executable. -/// -/// It provides ops available in the `Deno` namespace. -/// -/// All `WebWorker`s created during program execution -/// are descendants of this worker. -pub struct MainWorker { - inspector: Option>, - pub js_runtime: JsRuntime, - should_break_on_first_statement: bool, -} - -pub struct WorkerOptions { - pub apply_source_maps: bool, - /// Sets `Deno.args` in JS runtime. - pub args: Vec, - pub debug_flag: bool, - pub unstable: bool, - pub ca_filepath: Option, - pub user_agent: String, - pub seed: Option, - pub module_loader: Rc, - // Callback that will be invoked when creating new instance - // of WebWorker - pub create_web_worker_cb: Arc, - pub js_error_create_fn: Option>, - pub attach_inspector: bool, - pub maybe_inspector_server: Option>, - pub should_break_on_first_statement: bool, - /// Sets `Deno.version.deno` in JS runtime. - pub runtime_version: String, - /// Sets `Deno.version.typescript` in JS runtime. - pub ts_version: String, - /// Sets `Deno.noColor` in JS runtime. - pub no_color: bool, -} - -impl MainWorker { - pub fn from_options( - main_module: ModuleSpecifier, - permissions: Permissions, - options: &WorkerOptions, - ) -> Self { - let mut js_runtime = JsRuntime::new(RuntimeOptions { - module_loader: Some(options.module_loader.clone()), - startup_snapshot: Some(js::deno_isolate_init()), - js_error_create_fn: options.js_error_create_fn.clone(), - get_error_class_fn: Some(&crate::errors::get_error_class_name), - ..Default::default() - }); - - let inspector = if options.attach_inspector { - Some(DenoInspector::new( - &mut js_runtime, - options.maybe_inspector_server.clone(), - )) - } else { - None - }; - let should_break_on_first_statement = - inspector.is_some() && options.should_break_on_first_statement; - - let mut worker = Self { - inspector, - js_runtime, - should_break_on_first_statement, - }; - - let js_runtime = &mut worker.js_runtime; - { - // All ops registered in this function depend on these - { - let op_state = js_runtime.op_state(); - let mut op_state = op_state.borrow_mut(); - op_state.put::(Default::default()); - op_state.put::(permissions); - op_state.put::(ops::UnstableChecker { - unstable: options.unstable, - }); - } - - ops::runtime::init(js_runtime, main_module); - ops::fetch::init(js_runtime, options.ca_filepath.as_deref()); - ops::timers::init(js_runtime); - ops::worker_host::init( - js_runtime, - None, - options.create_web_worker_cb.clone(), - ); - ops::crypto::init(js_runtime, options.seed); - ops::reg_json_sync(js_runtime, "op_close", deno_core::op_close); - ops::reg_json_sync(js_runtime, "op_resources", deno_core::op_resources); - ops::reg_json_sync( - js_runtime, - "op_domain_to_ascii", - deno_web::op_domain_to_ascii, - ); - ops::fs_events::init(js_runtime); - ops::fs::init(js_runtime); - ops::io::init(js_runtime); - ops::net::init(js_runtime); - ops::os::init(js_runtime); - ops::permissions::init(js_runtime); - ops::plugin::init(js_runtime); - ops::process::init(js_runtime); - ops::signal::init(js_runtime); - ops::tls::init(js_runtime); - ops::tty::init(js_runtime); - ops::websocket::init( - js_runtime, - options.ca_filepath.as_deref(), - options.user_agent.clone(), - ); - } - { - let op_state = js_runtime.op_state(); - let mut op_state = op_state.borrow_mut(); - let t = &mut op_state.resource_table; - let (stdin, stdout, stderr) = ops::io::get_stdio(); - if let Some(stream) = stdin { - t.add("stdin", Box::new(stream)); - } - if let Some(stream) = stdout { - t.add("stdout", Box::new(stream)); - } - if let Some(stream) = stderr { - t.add("stderr", Box::new(stream)); - } - } - - worker - } - - pub fn bootstrap(&mut self, options: &WorkerOptions) { - let runtime_options = json!({ - "args": options.args, - "applySourceMaps": options.apply_source_maps, - "debugFlag": options.debug_flag, - "denoVersion": options.runtime_version, - "noColor": options.no_color, - "pid": std::process::id(), - "ppid": ops::runtime::ppid(), - "target": env!("TARGET"), - "tsVersion": options.ts_version, - "unstableFlag": options.unstable, - "v8Version": deno_core::v8_version(), - }); - - let script = format!( - "bootstrap.mainRuntime({})", - serde_json::to_string_pretty(&runtime_options).unwrap() - ); - self - .execute(&script) - .expect("Failed to execute bootstrap script"); - } - - /// Same as execute2() but the filename defaults to "$CWD/__anonymous__". - pub fn execute(&mut self, js_source: &str) -> Result<(), AnyError> { - let path = env::current_dir().unwrap().join("__anonymous__"); - let url = Url::from_file_path(path).unwrap(); - self.js_runtime.execute(url.as_str(), js_source) - } - - /// Loads and instantiates specified JavaScript module. - pub async fn preload_module( - &mut self, - module_specifier: &ModuleSpecifier, - ) -> Result { - self.js_runtime.load_module(module_specifier, None).await - } - - /// Loads, instantiates and executes specified JavaScript module. - pub async fn execute_module( - &mut self, - module_specifier: &ModuleSpecifier, - ) -> Result<(), AnyError> { - let id = self.preload_module(module_specifier).await?; - self.wait_for_inspector_session(); - self.js_runtime.mod_evaluate(id).await - } - - fn wait_for_inspector_session(&mut self) { - if self.should_break_on_first_statement { - self - .inspector - .as_mut() - .unwrap() - .wait_for_session_and_break_on_next_statement() - } - } - - /// Create new inspector session. This function panics if Worker - /// was not configured to create inspector. - pub fn create_inspector_session(&mut self) -> Box { - let inspector = self.inspector.as_mut().unwrap(); - - InspectorSession::new(&mut **inspector) - } - - pub fn poll_event_loop( - &mut self, - cx: &mut Context, - ) -> Poll> { - // We always poll the inspector if it exists. - let _ = self.inspector.as_mut().map(|i| i.poll_unpin(cx)); - self.js_runtime.poll_event_loop(cx) - } - - pub async fn run_event_loop(&mut self) -> Result<(), AnyError> { - poll_fn(|cx| self.poll_event_loop(cx)).await - } -} - -impl Drop for MainWorker { - fn drop(&mut self) { - // The Isolate object must outlive the Inspector object, but this is - // currently not enforced by the type system. - self.inspector.take(); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn create_test_worker() -> MainWorker { - let main_module = - ModuleSpecifier::resolve_url_or_path("./hello.js").unwrap(); - let permissions = Permissions::default(); - - let options = WorkerOptions { - apply_source_maps: false, - user_agent: "x".to_string(), - args: vec![], - debug_flag: false, - unstable: false, - ca_filepath: None, - seed: None, - js_error_create_fn: None, - create_web_worker_cb: Arc::new(|_| unreachable!()), - attach_inspector: false, - maybe_inspector_server: None, - should_break_on_first_statement: false, - module_loader: Rc::new(deno_core::FsModuleLoader), - runtime_version: "x".to_string(), - ts_version: "x".to_string(), - no_color: true, - }; - - MainWorker::from_options(main_module, permissions, &options) - } - - #[tokio::test] - async fn execute_mod_esm_imports_a() { - let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .join("cli/tests/esm_imports_a.js"); - let module_specifier = - ModuleSpecifier::resolve_url_or_path(&p.to_string_lossy()).unwrap(); - let mut worker = create_test_worker(); - let result = worker.execute_module(&module_specifier).await; - if let Err(err) = result { - eprintln!("execute_mod err {:?}", err); - } - if let Err(e) = worker.run_event_loop().await { - panic!("Future got unexpected error: {:?}", e); - } - } - - #[tokio::test] - async fn execute_mod_circular() { - let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .join("tests/circular1.js"); - let module_specifier = - ModuleSpecifier::resolve_url_or_path(&p.to_string_lossy()).unwrap(); - let mut worker = create_test_worker(); - let result = worker.execute_module(&module_specifier).await; - if let Err(err) = result { - eprintln!("execute_mod err {:?}", err); - } - if let Err(e) = worker.run_event_loop().await { - panic!("Future got unexpected error: {:?}", e); - } - } - - #[tokio::test] - async fn execute_mod_resolve_error() { - // "foo" is not a valid module specifier so this should return an error. - let mut worker = create_test_worker(); - let module_specifier = - ModuleSpecifier::resolve_url_or_path("does-not-exist").unwrap(); - let result = worker.execute_module(&module_specifier).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn execute_mod_002_hello() { - // This assumes cwd is project root (an assumption made throughout the - // tests). - let mut worker = create_test_worker(); - let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .join("cli/tests/001_hello.js"); - let module_specifier = - ModuleSpecifier::resolve_url_or_path(&p.to_string_lossy()).unwrap(); - let result = worker.execute_module(&module_specifier).await; - assert!(result.is_ok()); - } -} diff --git a/core/error.rs b/core/error.rs index 1d31df5ed..8e4138889 100644 --- a/core/error.rs +++ b/core/error.rs @@ -12,7 +12,6 @@ use std::fmt; use std::fmt::Debug; use std::fmt::Display; use std::fmt::Formatter; -use std::io; /// A generic wrapper that can encapsulate any concrete error type. pub type AnyError = anyhow::Error; @@ -41,10 +40,6 @@ pub fn uri_error(message: impl Into>) -> AnyError { custom_error("URIError", message) } -pub fn last_os_error() -> AnyError { - io::Error::last_os_error().into() -} - pub fn bad_resource(message: impl Into>) -> AnyError { custom_error("BadResource", message) } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml new file mode 100644 index 000000000..fa097064e --- /dev/null +++ b/runtime/Cargo.toml @@ -0,0 +1,74 @@ +# Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +[package] +name = "deno_runtime" +version = "0.1.0" +license = "MIT" +authors = ["the Deno authors"] +edition = "2018" +description = "Provides the deno runtime library" +repository = "https://github.com/denoland/deno" + +[lib] +name = "deno_runtime" +path = "lib.rs" + +[[example]] +name = "hello_runtime" +path = "examples/hello_runtime.rs" + +[build-dependencies] +deno_crypto = { path = "../op_crates/crypto", version = "0.4.0" } +deno_core = { path = "../core", version = "0.70.0" } +deno_web = { path = "../op_crates/web", version = "0.21.0" } +deno_fetch = { path = "../op_crates/fetch", version = "0.13.0" } + +[target.'cfg(windows)'.build-dependencies] +winres = "0.1.11" +winapi = "0.3.9" + +[dependencies] +deno_core = { path = "../core", version = "0.70.0" } +deno_crypto = { path = "../op_crates/crypto", version = "0.4.0" } +deno_fetch = { path = "../op_crates/fetch", version = "0.13.0" } +deno_web = { path = "../op_crates/web", version = "0.21.0" } + +atty = "0.2.14" +dlopen = "0.1.8" +encoding_rs = "0.8.24" +env_logger = "0.7.1" +filetime = "0.2.12" +http = "0.2.1" +indexmap = "1.6.0" +lazy_static = "1.4.0" +libc = "0.2.77" +log = "0.4.11" +notify = "5.0.0-pre.3" +percent-encoding = "2.1.0" +regex = "1.3.9" +ring = "0.16.19" +rustyline = { version = "7.0.0", default-features = false } +rustyline-derive = "0.4.0" +serde = { version = "1.0.116", features = ["derive"] } +shell-escape = "0.1.5" +sys-info = "0.7.0" +termcolor = "1.1.0" +tokio = { version = "0.2.22", features = ["full"] } +tokio-rustls = "0.14.1" +# Keep in-sync with warp. +tokio-tungstenite = "0.11.0" +uuid = { version = "0.8.1", features = ["v4"] } +warp = { version = "0.2.5", features = ["tls"] } +webpki = "0.21.3" +webpki-roots = "=0.19.0" # Pinned to v0.19.0 to match 'reqwest'. + +[target.'cfg(windows)'.dependencies] +winapi = { version = "0.3.9", features = ["knownfolders", "mswsock", "objbase", "shlobj", "tlhelp32", "winbase", "winerror", "winsock2"] } +fwdansi = "1.1.0" + +[target.'cfg(unix)'.dependencies] +nix = "0.19.0" + +[dev-dependencies] +# Used in benchmark +test_util = { path = "../test_util" } diff --git a/runtime/README.md b/runtime/README.md new file mode 100644 index 000000000..1056ac5b6 --- /dev/null +++ b/runtime/README.md @@ -0,0 +1,44 @@ +# `deno_runtime` crate + +[![crates](https://img.shields.io/crates/v/deno_runtime.svg)](https://crates.io/crates/deno_runtime) +[![docs](https://docs.rs/deno_runtime/badge.svg)](https://docs.rs/deno_runtime) + +This is a slim version of the Deno CLI which removes typescript integration and +various tooling (like lint and doc). Basically only JavaScript execution with +Deno's operating system bindings (ops). + +## Stability + +This crate is built using battle-tested modules that were originally in `deno` +crate, however the API of this crate is subject to rapid and breaking changes. + +## `MainWorker` + +The main API of this crate is `MainWorker`. `MainWorker` is a structure +encapsulating `deno_core::JsRuntime` with a set of ops used to implement `Deno` +namespace. + +When creating a `MainWorker` implementors must call `MainWorker::bootstrap` to +prepare JS runtime for use. + +`MainWorker` is highly configurable and allows to customize many of the +runtime's properties: + +- module loading implementation +- error formatting +- support for source maps +- support for V8 inspector and Chrome Devtools debugger +- HTTP client user agent, CA certificate +- random number generator seed + +## `Worker` Web API + +`deno_runtime` comes with support for `Worker` Web API. The `Worker` API is +implemented using `WebWorker` structure. + +When creating a new instance of `MainWorker` implementors must provide a +callback function that is used when creating a new instance of `Worker`. + +All `WebWorker` instances are decendents of `MainWorker` which is responsible +for setting up communication with child worker. Each `WebWorker` spawns a new OS +thread that is dedicated solely to that worker. diff --git a/runtime/build.rs b/runtime/build.rs new file mode 100644 index 000000000..7c74c9793 --- /dev/null +++ b/runtime/build.rs @@ -0,0 +1,81 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use deno_core::JsRuntime; +use deno_core::RuntimeOptions; +use std::env; +use std::path::Path; +use std::path::PathBuf; + +// TODO(bartlomieju): this module contains a lot of duplicated +// logic with `cli/build.rs`, factor out to `deno_core`. +fn create_snapshot( + mut js_runtime: JsRuntime, + snapshot_path: &Path, + files: Vec, +) { + deno_web::init(&mut js_runtime); + deno_fetch::init(&mut js_runtime); + deno_crypto::init(&mut js_runtime); + // TODO(nayeemrmn): https://github.com/rust-lang/cargo/issues/3946 to get the + // workspace root. + let display_root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(); + for file in files { + println!("cargo:rerun-if-changed={}", file.display()); + let display_path = file.strip_prefix(display_root).unwrap(); + let display_path_str = display_path.display().to_string(); + js_runtime + .execute( + &("deno:".to_string() + &display_path_str.replace('\\', "/")), + &std::fs::read_to_string(&file).unwrap(), + ) + .unwrap(); + } + + let snapshot = js_runtime.snapshot(); + let snapshot_slice: &[u8] = &*snapshot; + println!("Snapshot size: {}", snapshot_slice.len()); + std::fs::write(&snapshot_path, snapshot_slice).unwrap(); + println!("Snapshot written to: {} ", snapshot_path.display()); +} + +fn create_runtime_snapshot(snapshot_path: &Path, files: Vec) { + let js_runtime = JsRuntime::new(RuntimeOptions { + will_snapshot: true, + ..Default::default() + }); + create_snapshot(js_runtime, snapshot_path, files); +} + +fn main() { + // Don't build V8 if "cargo doc" is being run. This is to support docs.rs. + if env::var_os("RUSTDOCFLAGS").is_some() { + return; + } + + // To debug snapshot issues uncomment: + // op_fetch_asset::trace_serializer(); + + println!("cargo:rustc-env=TARGET={}", env::var("TARGET").unwrap()); + println!("cargo:rustc-env=PROFILE={}", env::var("PROFILE").unwrap()); + let o = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + + // Main snapshot + let runtime_snapshot_path = o.join("CLI_SNAPSHOT.bin"); + + let js_files = get_js_files("rt"); + create_runtime_snapshot(&runtime_snapshot_path, js_files); +} + +fn get_js_files(d: &str) -> Vec { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let mut js_files = std::fs::read_dir(d) + .unwrap() + .map(|dir_entry| { + let file = dir_entry.unwrap(); + manifest_dir.join(file.path()) + }) + .filter(|path| path.extension().unwrap_or_default() == "js") + .collect::>(); + js_files.sort(); + js_files +} diff --git a/runtime/colors.rs b/runtime/colors.rs new file mode 100644 index 000000000..93f252716 --- /dev/null +++ b/runtime/colors.rs @@ -0,0 +1,130 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use regex::Regex; +use std::env; +use std::fmt; +use std::io::Write; +use termcolor::Color::{Ansi256, Black, Blue, Cyan, Green, Red, White, Yellow}; +use termcolor::{Ansi, ColorSpec, WriteColor}; + +#[cfg(windows)] +use termcolor::{BufferWriter, ColorChoice}; + +lazy_static! { + // STRIP_ANSI_RE and strip_ansi_codes are lifted from the "console" crate. + // Copyright 2017 Armin Ronacher . MIT License. + static ref STRIP_ANSI_RE: Regex = Regex::new( + r"[\x1b\x9b][\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]" + ).unwrap(); + static ref NO_COLOR: bool = { + env::var_os("NO_COLOR").is_some() + }; +} + +/// Helper function to strip ansi codes. +#[cfg(test)] +pub fn strip_ansi_codes(s: &str) -> std::borrow::Cow { + STRIP_ANSI_RE.replace_all(s, "") +} + +pub fn use_color() -> bool { + !(*NO_COLOR) +} + +#[cfg(windows)] +pub fn enable_ansi() { + BufferWriter::stdout(ColorChoice::AlwaysAnsi); +} + +fn style(s: &str, colorspec: ColorSpec) -> impl fmt::Display { + if !use_color() { + return String::from(s); + } + let mut v = Vec::new(); + let mut ansi_writer = Ansi::new(&mut v); + ansi_writer.set_color(&colorspec).unwrap(); + ansi_writer.write_all(s.as_bytes()).unwrap(); + ansi_writer.reset().unwrap(); + String::from_utf8_lossy(&v).into_owned() +} + +pub fn red_bold(s: &str) -> impl fmt::Display { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Red)).set_bold(true); + style(&s, style_spec) +} + +pub fn green_bold(s: &str) -> impl fmt::Display { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Green)).set_bold(true); + style(&s, style_spec) +} + +pub fn italic_bold(s: &str) -> impl fmt::Display { + let mut style_spec = ColorSpec::new(); + style_spec.set_bold(true).set_italic(true); + style(&s, style_spec) +} + +pub fn white_on_red(s: &str) -> impl fmt::Display { + let mut style_spec = ColorSpec::new(); + style_spec.set_bg(Some(Red)).set_fg(Some(White)); + style(&s, style_spec) +} + +pub fn black_on_green(s: &str) -> impl fmt::Display { + let mut style_spec = ColorSpec::new(); + style_spec.set_bg(Some(Green)).set_fg(Some(Black)); + style(&s, style_spec) +} + +pub fn yellow(s: &str) -> impl fmt::Display { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Yellow)); + style(&s, style_spec) +} + +pub fn cyan(s: &str) -> impl fmt::Display { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Cyan)); + style(&s, style_spec) +} + +pub fn red(s: &str) -> impl fmt::Display { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Red)); + style(&s, style_spec) +} + +pub fn green(s: &str) -> impl fmt::Display { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Green)); + style(&s, style_spec) +} + +pub fn bold(s: &str) -> impl fmt::Display { + let mut style_spec = ColorSpec::new(); + style_spec.set_bold(true); + style(&s, style_spec) +} + +pub fn gray(s: &str) -> impl fmt::Display { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Ansi256(8))); + style(&s, style_spec) +} + +pub fn italic_bold_gray(s: &str) -> impl fmt::Display { + let mut style_spec = ColorSpec::new(); + style_spec + .set_fg(Some(Ansi256(8))) + .set_bold(true) + .set_italic(true); + style(&s, style_spec) +} + +pub fn intense_blue(s: &str) -> impl fmt::Display { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Blue)).set_intense(true); + style(&s, style_spec) +} diff --git a/runtime/errors.rs b/runtime/errors.rs new file mode 100644 index 000000000..f8f71a859 --- /dev/null +++ b/runtime/errors.rs @@ -0,0 +1,209 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +//! There are many types of errors in Deno: +//! - AnyError: a generic wrapper that can encapsulate any type of error. +//! - JsError: a container for the error message and stack trace for exceptions +//! thrown in JavaScript code. We use this to pretty-print stack traces. +//! - Diagnostic: these are errors that originate in TypeScript's compiler. +//! They're similar to JsError, in that they have line numbers. But +//! Diagnostics are compile-time type errors, whereas JsErrors are runtime +//! exceptions. + +use deno_core::error::AnyError; +use deno_core::serde_json; +use deno_core::url; +use deno_core::ModuleResolutionError; +use deno_fetch::reqwest; +use rustyline::error::ReadlineError; +use std::env; +use std::error::Error; +use std::io; + +fn get_dlopen_error_class(error: &dlopen::Error) -> &'static str { + use dlopen::Error::*; + match error { + NullCharacter(_) => "InvalidData", + OpeningLibraryError(ref e) => get_io_error_class(e), + SymbolGettingError(ref e) => get_io_error_class(e), + AddrNotMatchingDll(ref e) => get_io_error_class(e), + NullSymbol => "NotFound", + } +} + +fn get_env_var_error_class(error: &env::VarError) -> &'static str { + use env::VarError::*; + match error { + NotPresent => "NotFound", + NotUnicode(..) => "InvalidData", + } +} + +fn get_io_error_class(error: &io::Error) -> &'static str { + use io::ErrorKind::*; + match error.kind() { + NotFound => "NotFound", + PermissionDenied => "PermissionDenied", + ConnectionRefused => "ConnectionRefused", + ConnectionReset => "ConnectionReset", + ConnectionAborted => "ConnectionAborted", + NotConnected => "NotConnected", + AddrInUse => "AddrInUse", + AddrNotAvailable => "AddrNotAvailable", + BrokenPipe => "BrokenPipe", + AlreadyExists => "AlreadyExists", + InvalidInput => "TypeError", + InvalidData => "InvalidData", + TimedOut => "TimedOut", + Interrupted => "Interrupted", + WriteZero => "WriteZero", + UnexpectedEof => "UnexpectedEof", + Other => "Error", + WouldBlock => unreachable!(), + // Non-exhaustive enum - might add new variants + // in the future + _ => unreachable!(), + } +} + +fn get_module_resolution_error_class( + _: &ModuleResolutionError, +) -> &'static str { + "URIError" +} + +fn get_notify_error_class(error: ¬ify::Error) -> &'static str { + use notify::ErrorKind::*; + match error.kind { + Generic(_) => "Error", + Io(ref e) => get_io_error_class(e), + PathNotFound => "NotFound", + WatchNotFound => "NotFound", + InvalidConfig(_) => "InvalidData", + } +} + +fn get_readline_error_class(error: &ReadlineError) -> &'static str { + use ReadlineError::*; + match error { + Io(err) => get_io_error_class(err), + Eof => "UnexpectedEof", + Interrupted => "Interrupted", + #[cfg(unix)] + Errno(err) => get_nix_error_class(err), + _ => unimplemented!(), + } +} + +fn get_regex_error_class(error: ®ex::Error) -> &'static str { + use regex::Error::*; + match error { + Syntax(_) => "SyntaxError", + CompiledTooBig(_) => "RangeError", + _ => "Error", + } +} + +fn get_request_error_class(error: &reqwest::Error) -> &'static str { + error + .source() + .and_then(|inner_err| { + (inner_err + .downcast_ref::() + .map(get_io_error_class)) + .or_else(|| { + inner_err + .downcast_ref::() + .map(get_serde_json_error_class) + }) + .or_else(|| { + inner_err + .downcast_ref::() + .map(get_url_parse_error_class) + }) + }) + .unwrap_or("Http") +} + +fn get_serde_json_error_class( + error: &serde_json::error::Error, +) -> &'static str { + use deno_core::serde_json::error::*; + match error.classify() { + Category::Io => error + .source() + .and_then(|e| e.downcast_ref::()) + .map(get_io_error_class) + .unwrap(), + Category::Syntax => "SyntaxError", + Category::Data => "InvalidData", + Category::Eof => "UnexpectedEof", + } +} + +fn get_url_parse_error_class(_error: &url::ParseError) -> &'static str { + "URIError" +} + +#[cfg(unix)] +fn get_nix_error_class(error: &nix::Error) -> &'static str { + use nix::errno::Errno::*; + match error { + nix::Error::Sys(ECHILD) => "NotFound", + nix::Error::Sys(EINVAL) => "TypeError", + nix::Error::Sys(ENOENT) => "NotFound", + nix::Error::Sys(ENOTTY) => "BadResource", + nix::Error::Sys(EPERM) => "PermissionDenied", + nix::Error::Sys(ESRCH) => "NotFound", + nix::Error::Sys(UnknownErrno) => "Error", + nix::Error::Sys(_) => "Error", + nix::Error::InvalidPath => "TypeError", + nix::Error::InvalidUtf8 => "InvalidData", + nix::Error::UnsupportedOperation => unreachable!(), + } +} + +pub fn get_error_class_name(e: &AnyError) -> Option<&'static str> { + deno_core::error::get_custom_error_class(e) + .or_else(|| { + e.downcast_ref::() + .map(get_dlopen_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_env_var_error_class) + }) + .or_else(|| e.downcast_ref::().map(get_io_error_class)) + .or_else(|| { + e.downcast_ref::() + .map(get_module_resolution_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_notify_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_readline_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_request_error_class) + }) + .or_else(|| e.downcast_ref::().map(get_regex_error_class)) + .or_else(|| { + e.downcast_ref::() + .map(get_serde_json_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_url_parse_error_class) + }) + .or_else(|| { + #[cfg(unix)] + let maybe_get_nix_error_class = + || e.downcast_ref::().map(get_nix_error_class); + #[cfg(not(unix))] + let maybe_get_nix_error_class = || Option::<&'static str>::None; + (maybe_get_nix_error_class)() + }) +} diff --git a/runtime/examples/hello_runtime.js b/runtime/examples/hello_runtime.js new file mode 100644 index 000000000..46609c7a0 --- /dev/null +++ b/runtime/examples/hello_runtime.js @@ -0,0 +1,2 @@ +console.log("Hello world!"); +console.log(Deno); diff --git a/runtime/examples/hello_runtime.rs b/runtime/examples/hello_runtime.rs new file mode 100644 index 000000000..dbe539281 --- /dev/null +++ b/runtime/examples/hello_runtime.rs @@ -0,0 +1,55 @@ +// Copyright 2020 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::AnyError; +use deno_core::FsModuleLoader; +use deno_core::ModuleSpecifier; +use deno_runtime::permissions::Permissions; +use deno_runtime::worker::MainWorker; +use deno_runtime::worker::WorkerOptions; +use std::path::Path; +use std::rc::Rc; +use std::sync::Arc; + +fn get_error_class_name(e: &AnyError) -> &'static str { + deno_runtime::errors::get_error_class_name(e).unwrap_or("Error") +} + +#[tokio::main] +async fn main() -> Result<(), AnyError> { + let module_loader = Rc::new(FsModuleLoader); + let create_web_worker_cb = Arc::new(|_| { + todo!("Web workers are not supported in the example"); + }); + + let options = WorkerOptions { + apply_source_maps: false, + args: vec![], + debug_flag: false, + unstable: false, + ca_filepath: None, + user_agent: "hello_runtime".to_string(), + seed: None, + js_error_create_fn: None, + create_web_worker_cb, + attach_inspector: false, + maybe_inspector_server: None, + should_break_on_first_statement: false, + module_loader, + runtime_version: "x".to_string(), + ts_version: "x".to_string(), + no_color: false, + get_error_class_fn: Some(&get_error_class_name), + }; + + let js_path = + Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/hello_runtime.js"); + let main_module = ModuleSpecifier::resolve_path(&js_path.to_string_lossy())?; + let permissions = Permissions::allow_all(); + + let mut worker = + MainWorker::from_options(main_module.clone(), permissions, &options); + worker.bootstrap(&options); + worker.execute_module(&main_module).await?; + worker.run_event_loop().await?; + Ok(()) +} diff --git a/runtime/fs_util.rs b/runtime/fs_util.rs new file mode 100644 index 000000000..028538d4f --- /dev/null +++ b/runtime/fs_util.rs @@ -0,0 +1,80 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::AnyError; +pub use deno_core::normalize_path; +use std::env::current_dir; +use std::io::Error; +use std::path::{Path, PathBuf}; + +/// Similar to `std::fs::canonicalize()` but strips UNC prefixes on Windows. +pub fn canonicalize_path(path: &Path) -> Result { + let mut canonicalized_path = path.canonicalize()?; + if cfg!(windows) { + canonicalized_path = PathBuf::from( + canonicalized_path + .display() + .to_string() + .trim_start_matches("\\\\?\\"), + ); + } + Ok(canonicalized_path) +} + +pub fn resolve_from_cwd(path: &Path) -> Result { + let resolved_path = if path.is_absolute() { + path.to_owned() + } else { + let cwd = current_dir().unwrap(); + cwd.join(path) + }; + + Ok(normalize_path(&resolved_path)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resolve_from_cwd_child() { + let cwd = current_dir().unwrap(); + assert_eq!(resolve_from_cwd(Path::new("a")).unwrap(), cwd.join("a")); + } + + #[test] + fn resolve_from_cwd_dot() { + let cwd = current_dir().unwrap(); + assert_eq!(resolve_from_cwd(Path::new(".")).unwrap(), cwd); + } + + #[test] + fn resolve_from_cwd_parent() { + let cwd = current_dir().unwrap(); + assert_eq!(resolve_from_cwd(Path::new("a/..")).unwrap(), cwd); + } + + #[test] + fn test_normalize_path() { + assert_eq!(normalize_path(Path::new("a/../b")), PathBuf::from("b")); + assert_eq!(normalize_path(Path::new("a/./b/")), PathBuf::from("a/b/")); + assert_eq!( + normalize_path(Path::new("a/./b/../c")), + PathBuf::from("a/c") + ); + + if cfg!(windows) { + assert_eq!( + normalize_path(Path::new("C:\\a\\.\\b\\..\\c")), + PathBuf::from("C:\\a\\c") + ); + } + } + + // TODO: Get a good expected value here for Windows. + #[cfg(not(windows))] + #[test] + fn resolve_from_cwd_absolute() { + let expected = Path::new("/a"); + assert_eq!(resolve_from_cwd(expected).unwrap(), expected); + } +} diff --git a/runtime/http_util.rs b/runtime/http_util.rs new file mode 100644 index 000000000..67703c214 --- /dev/null +++ b/runtime/http_util.rs @@ -0,0 +1,46 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::generic_error; +use deno_core::error::AnyError; +use deno_fetch::reqwest; +use deno_fetch::reqwest::header::HeaderMap; +use deno_fetch::reqwest::header::USER_AGENT; +use deno_fetch::reqwest::redirect::Policy; +use deno_fetch::reqwest::Client; +use std::fs::File; +use std::io::Read; + +/// Create new instance of async reqwest::Client. This client supports +/// proxies and doesn't follow redirects. +pub fn create_http_client( + user_agent: String, + ca_file: Option<&str>, +) -> Result { + let mut headers = HeaderMap::new(); + headers.insert(USER_AGENT, user_agent.parse().unwrap()); + let mut builder = Client::builder() + .redirect(Policy::none()) + .default_headers(headers) + .use_rustls_tls(); + + if let Some(ca_file) = ca_file { + let mut buf = Vec::new(); + File::open(ca_file)?.read_to_end(&mut buf)?; + let cert = reqwest::Certificate::from_pem(&buf)?; + builder = builder.add_root_certificate(cert); + } + + builder + .build() + .map_err(|_| generic_error("Unable to build http client")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn create_test_client() { + create_http_client("test_client".to_string(), None).unwrap(); + } +} diff --git a/runtime/inspector.rs b/runtime/inspector.rs new file mode 100644 index 000000000..89fd5bf57 --- /dev/null +++ b/runtime/inspector.rs @@ -0,0 +1,952 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +//! The documentation for the inspector API is sparse, but these are helpful: +//! https://chromedevtools.github.io/devtools-protocol/ +//! https://hyperandroid.com/2020/02/12/v8-inspector-from-an-embedder-standpoint/ + +use core::convert::Infallible as Never; // Alias for the future `!` type. +use deno_core::error::generic_error; +use deno_core::error::AnyError; +use deno_core::futures::channel::mpsc; +use deno_core::futures::channel::mpsc::UnboundedReceiver; +use deno_core::futures::channel::mpsc::UnboundedSender; +use deno_core::futures::channel::oneshot; +use deno_core::futures::future::Future; +use deno_core::futures::pin_mut; +use deno_core::futures::prelude::*; +use deno_core::futures::select; +use deno_core::futures::stream::FuturesUnordered; +use deno_core::futures::task; +use deno_core::futures::task::Context; +use deno_core::futures::task::Poll; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::v8; +use std::cell::BorrowMutError; +use std::cell::RefCell; +use std::collections::HashMap; +use std::ffi::c_void; +use std::mem::replace; +use std::mem::take; +use std::mem::MaybeUninit; +use std::net::SocketAddr; +use std::ops::Deref; +use std::ops::DerefMut; +use std::pin::Pin; +use std::process; +use std::ptr; +use std::ptr::NonNull; +use std::sync::Arc; +use std::sync::Mutex; +use std::thread; +use uuid::Uuid; +use warp::filters::ws; +use warp::Filter; + +pub struct InspectorServer { + pub host: SocketAddr, + register_inspector_tx: UnboundedSender, + shutdown_server_tx: Option>, + thread_handle: Option>, +} + +impl InspectorServer { + pub fn new(host: SocketAddr, name: String) -> Self { + let (register_inspector_tx, register_inspector_rx) = + mpsc::unbounded::(); + + let (shutdown_server_tx, shutdown_server_rx) = oneshot::channel(); + + let thread_handle = thread::spawn(move || { + crate::tokio_util::run_basic(server( + host, + register_inspector_rx, + shutdown_server_rx, + name, + )) + }); + + Self { + host, + register_inspector_tx, + shutdown_server_tx: Some(shutdown_server_tx), + thread_handle: Some(thread_handle), + } + } + + fn register_inspector(&self, info: InspectorInfo) { + self.register_inspector_tx.unbounded_send(info).unwrap(); + } +} + +impl Drop for InspectorServer { + fn drop(&mut self) { + if let Some(shutdown_server_tx) = self.shutdown_server_tx.take() { + shutdown_server_tx + .send(()) + .expect("unable to send shutdown signal"); + } + + if let Some(thread_handle) = self.thread_handle.take() { + thread_handle.join().expect("unable to join thread"); + } + } +} + +/// Inspector information that is sent from the isolate thread to the server +/// thread when a new inspector is created. +struct InspectorInfo { + host: SocketAddr, + uuid: Uuid, + thread_name: Option, + new_websocket_tx: UnboundedSender, + canary_rx: oneshot::Receiver, +} + +impl InspectorInfo { + fn get_json_metadata(&self) -> Value { + json!({ + "description": "deno", + "devtoolsFrontendUrl": self.get_frontend_url(), + "faviconUrl": "https://deno.land/favicon.ico", + "id": self.uuid.to_string(), + "title": self.get_title(), + "type": "deno", + // TODO(ry): "url": "file://", + "webSocketDebuggerUrl": self.get_websocket_debugger_url(), + }) + } + + fn get_websocket_debugger_url(&self) -> String { + format!("ws://{}/ws/{}", &self.host, &self.uuid) + } + + fn get_frontend_url(&self) -> String { + format!( + "devtools://devtools/bundled/inspector.html?v8only=true&ws={}/ws/{}", + &self.host, &self.uuid + ) + } + + fn get_title(&self) -> String { + format!( + "[{}] deno{}", + process::id(), + self + .thread_name + .as_ref() + .map(|n| format!(" - {}", n)) + .unwrap_or_default() + ) + } +} + +async fn server( + host: SocketAddr, + register_inspector_rx: UnboundedReceiver, + shutdown_server_rx: oneshot::Receiver<()>, + name: String, +) { + // TODO: put the `inspector_map` in an `Rc>` instead. This is + // currently not possible because warp requires all filters to implement + // `Send`, which should not be necessary because we are using the + // single-threaded Tokio runtime. + let inspector_map = HashMap::::new(); + let inspector_map = Arc::new(Mutex::new(inspector_map)); + + let inspector_map_ = inspector_map.clone(); + let register_inspector_handler = register_inspector_rx + .map(|info| { + eprintln!( + "Debugger listening on {}", + info.get_websocket_debugger_url() + ); + let mut g = inspector_map_.lock().unwrap(); + if g.insert(info.uuid, info).is_some() { + panic!("Inspector UUID already in map"); + } + }) + .collect::<()>(); + + let inspector_map_ = inspector_map_.clone(); + let deregister_inspector_handler = future::poll_fn(|cx| { + let mut g = inspector_map_.lock().unwrap(); + g.retain(|_, info| info.canary_rx.poll_unpin(cx) == Poll::Pending); + Poll::::Pending + }) + .fuse(); + + let inspector_map_ = inspector_map.clone(); + let websocket_route = warp::path("ws") + .and(warp::path::param()) + .and(warp::ws()) + .and_then(move |uuid: String, ws: warp::ws::Ws| { + future::ready( + Uuid::parse_str(&uuid) + .ok() + .and_then(|uuid| { + let g = inspector_map_.lock().unwrap(); + g.get(&uuid).map(|info| info.new_websocket_tx.clone()).map( + |new_websocket_tx| { + ws.on_upgrade(move |websocket| async move { + let (proxy, pump) = create_websocket_proxy(websocket); + let _ = new_websocket_tx.unbounded_send(proxy); + pump.await; + }) + }, + ) + }) + .ok_or_else(warp::reject::not_found), + ) + }); + + let json_version_response = json!({ + "Browser": name, + "Protocol-Version": "1.3", + "V8-Version": deno_core::v8_version(), + }); + let json_version_route = warp::path!("json" / "version") + .map(move || warp::reply::json(&json_version_response)); + + let inspector_map_ = inspector_map.clone(); + let json_list_route = warp::path("json").map(move || { + let g = inspector_map_.lock().unwrap(); + let json_values = g + .values() + .map(|info| info.get_json_metadata()) + .collect::>(); + warp::reply::json(&json!(json_values)) + }); + + let server_routes = + websocket_route.or(json_version_route).or(json_list_route); + let server_handler = warp::serve(server_routes) + .try_bind_with_graceful_shutdown(host, async { + shutdown_server_rx.await.ok(); + }) + .map(|(_, fut)| fut) + .unwrap_or_else(|err| { + eprintln!("Cannot start inspector server: {}.", err); + process::exit(1); + }) + .fuse(); + + pin_mut!(register_inspector_handler); + pin_mut!(deregister_inspector_handler); + pin_mut!(server_handler); + + select! { + _ = register_inspector_handler => {}, + _ = deregister_inspector_handler => unreachable!(), + _ = server_handler => {}, + } +} + +type WebSocketProxySender = UnboundedSender; +type WebSocketProxyReceiver = + UnboundedReceiver>; + +/// Encapsulates an UnboundedSender/UnboundedReceiver pair that together form +/// a duplex channel for sending/receiving websocket messages. +struct WebSocketProxy { + tx: WebSocketProxySender, + rx: WebSocketProxyReceiver, +} + +impl WebSocketProxy { + pub fn split(self) -> (WebSocketProxySender, WebSocketProxyReceiver) { + (self.tx, self.rx) + } +} + +/// Creates a future that proxies messages sent and received on a warp WebSocket +/// to a UnboundedSender/UnboundedReceiver pair. We need this to sidestep +/// Tokio's task budget, which causes issues when DenoInspector::poll_sessions() +/// needs to block the thread because JavaScript execution is paused. +/// +/// This works because UnboundedSender/UnboundedReceiver are implemented in the +/// 'futures' crate, therefore they can't participate in Tokio's cooperative +/// task yielding. +/// +/// A tuple is returned, where the first element is a duplex channel that can +/// be used to send/receive messages on the websocket, and the second element +/// is a future that does the forwarding. +fn create_websocket_proxy( + websocket: ws::WebSocket, +) -> (WebSocketProxy, impl Future + Send) { + // The 'outbound' channel carries messages sent to the websocket. + let (outbound_tx, outbound_rx) = mpsc::unbounded(); + + // The 'inbound' channel carries messages received from the websocket. + let (inbound_tx, inbound_rx) = mpsc::unbounded(); + + let proxy = WebSocketProxy { + tx: outbound_tx, + rx: inbound_rx, + }; + + // The pump future takes care of forwarding messages between the websocket + // and channels. It resolves to () when either side disconnects, ignoring any + // errors. + let pump = async move { + let (websocket_tx, websocket_rx) = websocket.split(); + + let outbound_pump = + outbound_rx.map(Ok).forward(websocket_tx).map_err(|_| ()); + + let inbound_pump = websocket_rx + .map(|msg| inbound_tx.unbounded_send(msg)) + .map_err(|_| ()) + .try_collect::<()>(); + + let _ = future::try_join(outbound_pump, inbound_pump).await; + }; + + (proxy, pump) +} + +#[derive(Clone, Copy)] +enum PollState { + Idle, + Woken, + Polling, + Parked, + Dropped, +} + +pub struct DenoInspector { + v8_inspector_client: v8::inspector::V8InspectorClientBase, + v8_inspector: v8::UniqueRef, + sessions: RefCell, + flags: RefCell, + waker: Arc, + _canary_tx: oneshot::Sender, + pub server: Option>, + pub debugger_url: Option, +} + +impl Deref for DenoInspector { + type Target = v8::inspector::V8Inspector; + fn deref(&self) -> &Self::Target { + &self.v8_inspector + } +} + +impl DerefMut for DenoInspector { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.v8_inspector + } +} + +impl Drop for DenoInspector { + fn drop(&mut self) { + // Since the waker is cloneable, it might outlive the inspector itself. + // Set the poll state to 'dropped' so it doesn't attempt to request an + // interrupt from the isolate. + self.waker.update(|w| w.poll_state = PollState::Dropped); + // V8 automatically deletes all sessions when an Inspector instance is + // deleted, however InspectorSession also has a drop handler that cleans + // up after itself. To avoid a double free, make sure the inspector is + // dropped last. + take(&mut *self.sessions.borrow_mut()); + } +} + +impl v8::inspector::V8InspectorClientImpl for DenoInspector { + fn base(&self) -> &v8::inspector::V8InspectorClientBase { + &self.v8_inspector_client + } + + fn base_mut(&mut self) -> &mut v8::inspector::V8InspectorClientBase { + &mut self.v8_inspector_client + } + + fn run_message_loop_on_pause(&mut self, context_group_id: i32) { + assert_eq!(context_group_id, DenoInspectorSession::CONTEXT_GROUP_ID); + self.flags.borrow_mut().on_pause = true; + let _ = self.poll_sessions(None); + } + + fn quit_message_loop_on_pause(&mut self) { + self.flags.borrow_mut().on_pause = false; + } + + fn run_if_waiting_for_debugger(&mut self, context_group_id: i32) { + assert_eq!(context_group_id, DenoInspectorSession::CONTEXT_GROUP_ID); + self.flags.borrow_mut().session_handshake_done = true; + } +} + +/// DenoInspector implements a Future so that it can poll for new incoming +/// connections and messages from the WebSocket server. The Worker that owns +/// this DenoInspector will call our poll function from Worker::poll(). +impl Future for DenoInspector { + type Output = (); + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> { + self.poll_sessions(Some(cx)).unwrap() + } +} + +impl DenoInspector { + const CONTEXT_GROUP_ID: i32 = 1; + + pub fn new( + js_runtime: &mut deno_core::JsRuntime, + server: Option>, + ) -> Box { + let context = js_runtime.global_context(); + let scope = &mut v8::HandleScope::new(js_runtime.v8_isolate()); + + let (new_websocket_tx, new_websocket_rx) = + mpsc::unbounded::(); + let (canary_tx, canary_rx) = oneshot::channel::(); + + // Create DenoInspector instance. + let mut self_ = new_box_with(|self_ptr| { + let v8_inspector_client = + v8::inspector::V8InspectorClientBase::new::(); + let v8_inspector = + v8::inspector::V8Inspector::create(scope, unsafe { &mut *self_ptr }); + + let sessions = InspectorSessions::new(self_ptr, new_websocket_rx); + let flags = InspectorFlags::new(); + let waker = InspectorWaker::new(scope.thread_safe_handle()); + + let debugger_url = if let Some(server) = server.clone() { + let info = InspectorInfo { + host: server.host, + uuid: Uuid::new_v4(), + thread_name: thread::current().name().map(|n| n.to_owned()), + new_websocket_tx, + canary_rx, + }; + + let debugger_url = info.get_websocket_debugger_url(); + server.register_inspector(info); + Some(debugger_url) + } else { + None + }; + + Self { + v8_inspector_client, + v8_inspector, + sessions, + flags, + waker, + _canary_tx: canary_tx, + server, + debugger_url, + } + }); + + // Tell the inspector about the global context. + let context = v8::Local::new(scope, context); + let context_name = v8::inspector::StringView::from(&b"global context"[..]); + self_.context_created(context, Self::CONTEXT_GROUP_ID, context_name); + + // Poll the session handler so we will get notified whenever there is + // new_incoming debugger activity. + let _ = self_.poll_sessions(None).unwrap(); + + self_ + } + + fn poll_sessions( + &self, + mut invoker_cx: Option<&mut Context>, + ) -> Result, BorrowMutError> { + // Short-circuit if there is no server + if self.server.is_none() { + return Ok(Poll::Ready(())); + } + + // The futures this function uses do not have re-entrant poll() functions. + // However it is can happpen that poll_sessions() gets re-entered, e.g. + // when an interrupt request is honored while the inspector future is polled + // by the task executor. We let the caller know by returning some error. + let mut sessions = self.sessions.try_borrow_mut()?; + + self.waker.update(|w| { + match w.poll_state { + PollState::Idle | PollState::Woken => w.poll_state = PollState::Polling, + _ => unreachable!(), + }; + }); + + // Create a new Context object that will make downstream futures + // use the InspectorWaker when they are ready to be polled again. + let waker_ref = task::waker_ref(&self.waker); + let cx = &mut Context::from_waker(&waker_ref); + + loop { + loop { + // Do one "handshake" with a newly connected session at a time. + if let Some(session) = &mut sessions.handshake { + let poll_result = session.poll_unpin(cx); + let handshake_done = + replace(&mut self.flags.borrow_mut().session_handshake_done, false); + match poll_result { + Poll::Pending if handshake_done => { + let session = sessions.handshake.take().unwrap(); + sessions.established.push(session); + take(&mut self.flags.borrow_mut().waiting_for_session); + } + Poll::Ready(_) => sessions.handshake = None, + Poll::Pending => break, + }; + } + + // Accept new connections. + match sessions.new_incoming.poll_next_unpin(cx) { + Poll::Ready(Some(session)) => { + let prev = sessions.handshake.replace(session); + assert!(prev.is_none()); + continue; + } + Poll::Ready(None) => {} + Poll::Pending => {} + } + + // Poll established sessions. + match sessions.established.poll_next_unpin(cx) { + Poll::Ready(Some(_)) => continue, + Poll::Ready(None) => break, + Poll::Pending => break, + }; + } + + let should_block = sessions.handshake.is_some() + || self.flags.borrow().on_pause + || self.flags.borrow().waiting_for_session; + + let new_state = self.waker.update(|w| { + match w.poll_state { + PollState::Woken => { + // The inspector was woken while the session handler was being + // polled, so we poll it another time. + w.poll_state = PollState::Polling; + } + PollState::Polling if !should_block => { + // The session handler doesn't need to be polled any longer, and + // there's no reason to block (execution is not paused), so this + // function is about to return. + w.poll_state = PollState::Idle; + // Register the task waker that can be used to wake the parent + // task that will poll the inspector future. + if let Some(cx) = invoker_cx.take() { + w.task_waker.replace(cx.waker().clone()); + } + // Register the address of the inspector, which allows the waker + // to request an interrupt from the isolate. + w.inspector_ptr = NonNull::new(self as *const _ as *mut Self); + } + PollState::Polling if should_block => { + // Isolate execution has been paused but there are no more + // events to process, so this thread will be parked. Therefore, + // store the current thread handle in the waker so it knows + // which thread to unpark when new events arrive. + w.poll_state = PollState::Parked; + w.parked_thread.replace(thread::current()); + } + _ => unreachable!(), + }; + w.poll_state + }); + match new_state { + PollState::Idle => break Ok(Poll::Pending), // Yield to task. + PollState::Polling => {} // Poll the session handler again. + PollState::Parked => thread::park(), // Park the thread. + _ => unreachable!(), + }; + } + } + + /// This function blocks the thread until at least one inspector client has + /// established a websocket connection and successfully completed the + /// handshake. After that, it instructs V8 to pause at the next statement. + pub fn wait_for_session_and_break_on_next_statement(&mut self) { + loop { + match self.sessions.get_mut().established.iter_mut().next() { + Some(session) => break session.break_on_next_statement(), + None => { + self.flags.get_mut().waiting_for_session = true; + let _ = self.poll_sessions(None).unwrap(); + } + }; + } + } +} + +#[derive(Default)] +struct InspectorFlags { + waiting_for_session: bool, + session_handshake_done: bool, + on_pause: bool, +} + +impl InspectorFlags { + fn new() -> RefCell { + let self_ = Self::default(); + RefCell::new(self_) + } +} + +struct InspectorSessions { + new_incoming: + Pin> + 'static>>, + handshake: Option>, + established: FuturesUnordered>, +} + +impl InspectorSessions { + fn new( + inspector_ptr: *mut DenoInspector, + new_websocket_rx: UnboundedReceiver, + ) -> RefCell { + let new_incoming = new_websocket_rx + .map(move |websocket| DenoInspectorSession::new(inspector_ptr, websocket)) + .boxed_local(); + let self_ = Self { + new_incoming, + ..Default::default() + }; + RefCell::new(self_) + } +} + +impl Default for InspectorSessions { + fn default() -> Self { + Self { + new_incoming: stream::empty().boxed_local(), + handshake: None, + established: FuturesUnordered::new(), + } + } +} + +struct InspectorWakerInner { + poll_state: PollState, + task_waker: Option, + parked_thread: Option, + inspector_ptr: Option>, + isolate_handle: v8::IsolateHandle, +} + +unsafe impl Send for InspectorWakerInner {} + +struct InspectorWaker(Mutex); + +impl InspectorWaker { + fn new(isolate_handle: v8::IsolateHandle) -> Arc { + let inner = InspectorWakerInner { + poll_state: PollState::Idle, + task_waker: None, + parked_thread: None, + inspector_ptr: None, + isolate_handle, + }; + Arc::new(Self(Mutex::new(inner))) + } + + fn update(&self, update_fn: F) -> R + where + F: FnOnce(&mut InspectorWakerInner) -> R, + { + let mut g = self.0.lock().unwrap(); + update_fn(&mut g) + } +} + +impl task::ArcWake for InspectorWaker { + fn wake_by_ref(arc_self: &Arc) { + arc_self.update(|w| { + match w.poll_state { + PollState::Idle => { + // Wake the task, if any, that has polled the Inspector future last. + if let Some(waker) = w.task_waker.take() { + waker.wake() + } + // Request an interrupt from the isolate if it's running and there's + // not unhandled interrupt request in flight. + if let Some(arg) = w + .inspector_ptr + .take() + .map(|ptr| ptr.as_ptr() as *mut c_void) + { + w.isolate_handle.request_interrupt(handle_interrupt, arg); + } + extern "C" fn handle_interrupt( + _isolate: &mut v8::Isolate, + arg: *mut c_void, + ) { + let inspector = unsafe { &*(arg as *mut DenoInspector) }; + let _ = inspector.poll_sessions(None); + } + } + PollState::Parked => { + // Unpark the isolate thread. + let parked_thread = w.parked_thread.take().unwrap(); + assert_ne!(parked_thread.id(), thread::current().id()); + parked_thread.unpark(); + } + _ => {} + }; + w.poll_state = PollState::Woken; + }); + } +} + +struct DenoInspectorSession { + v8_channel: v8::inspector::ChannelBase, + v8_session: v8::UniqueRef, + websocket_tx: WebSocketProxySender, + websocket_rx_handler: Pin + 'static>>, +} + +impl Deref for DenoInspectorSession { + type Target = v8::inspector::V8InspectorSession; + fn deref(&self) -> &Self::Target { + &self.v8_session + } +} + +impl DerefMut for DenoInspectorSession { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.v8_session + } +} + +impl DenoInspectorSession { + const CONTEXT_GROUP_ID: i32 = 1; + + pub fn new( + inspector_ptr: *mut DenoInspector, + websocket: WebSocketProxy, + ) -> Box { + new_box_with(move |self_ptr| { + let v8_channel = v8::inspector::ChannelBase::new::(); + let v8_session = unsafe { &mut *inspector_ptr }.connect( + Self::CONTEXT_GROUP_ID, + // Todo(piscisaureus): V8Inspector::connect() should require that + // the 'v8_channel' argument cannot move. + unsafe { &mut *self_ptr }, + v8::inspector::StringView::empty(), + ); + + let (websocket_tx, websocket_rx) = websocket.split(); + let websocket_rx_handler = + Self::receive_from_websocket(self_ptr, websocket_rx); + + Self { + v8_channel, + v8_session, + websocket_tx, + websocket_rx_handler, + } + }) + } + + /// Returns a future that receives messages from the websocket and dispatches + /// them to the V8 session. + fn receive_from_websocket( + self_ptr: *mut Self, + websocket_rx: WebSocketProxyReceiver, + ) -> Pin + 'static>> { + async move { + eprintln!("Debugger session started."); + + let result = websocket_rx + .map_ok(move |msg| { + let msg = msg.as_bytes(); + let msg = v8::inspector::StringView::from(msg); + unsafe { &mut *self_ptr }.dispatch_protocol_message(msg); + }) + .try_collect::<()>() + .await; + + match result { + Ok(_) => eprintln!("Debugger session ended."), + Err(err) => eprintln!("Debugger session ended: {}.", err), + }; + } + .boxed_local() + } + + fn send_to_websocket(&self, msg: v8::UniquePtr) { + let msg = msg.unwrap().string().to_string(); + let msg = ws::Message::text(msg); + let _ = self.websocket_tx.unbounded_send(msg); + } + + pub fn break_on_next_statement(&mut self) { + let reason = v8::inspector::StringView::from(&b"debugCommand"[..]); + let detail = v8::inspector::StringView::empty(); + self.schedule_pause_on_next_statement(reason, detail); + } +} + +impl v8::inspector::ChannelImpl for DenoInspectorSession { + fn base(&self) -> &v8::inspector::ChannelBase { + &self.v8_channel + } + + fn base_mut(&mut self) -> &mut v8::inspector::ChannelBase { + &mut self.v8_channel + } + + fn send_response( + &mut self, + _call_id: i32, + message: v8::UniquePtr, + ) { + self.send_to_websocket(message); + } + + fn send_notification( + &mut self, + message: v8::UniquePtr, + ) { + self.send_to_websocket(message); + } + + fn flush_protocol_notifications(&mut self) {} +} + +impl Future for DenoInspectorSession { + type Output = (); + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { + self.websocket_rx_handler.poll_unpin(cx) + } +} + +/// A local inspector session that can be used to send and receive protocol messages directly on +/// the same thread as an isolate. +pub struct InspectorSession { + v8_channel: v8::inspector::ChannelBase, + v8_session: v8::UniqueRef, + response_tx_map: HashMap>, + next_message_id: i32, + notification_queue: Vec, +} + +impl Deref for InspectorSession { + type Target = v8::inspector::V8InspectorSession; + fn deref(&self) -> &Self::Target { + &self.v8_session + } +} + +impl DerefMut for InspectorSession { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.v8_session + } +} + +impl v8::inspector::ChannelImpl for InspectorSession { + fn base(&self) -> &v8::inspector::ChannelBase { + &self.v8_channel + } + + fn base_mut(&mut self) -> &mut v8::inspector::ChannelBase { + &mut self.v8_channel + } + + fn send_response( + &mut self, + call_id: i32, + message: v8::UniquePtr, + ) { + let raw_message = message.unwrap().string().to_string(); + let message = serde_json::from_str(&raw_message).unwrap(); + + self + .response_tx_map + .remove(&call_id) + .unwrap() + .send(message) + .unwrap(); + } + + fn send_notification( + &mut self, + message: v8::UniquePtr, + ) { + let raw_message = message.unwrap().string().to_string(); + let message = serde_json::from_str(&raw_message).unwrap(); + + self.notification_queue.push(message); + } + + fn flush_protocol_notifications(&mut self) {} +} + +impl InspectorSession { + const CONTEXT_GROUP_ID: i32 = 1; + + pub fn new(inspector_ptr: *mut DenoInspector) -> Box { + new_box_with(move |self_ptr| { + let v8_channel = v8::inspector::ChannelBase::new::(); + let v8_session = unsafe { &mut *inspector_ptr }.connect( + Self::CONTEXT_GROUP_ID, + unsafe { &mut *self_ptr }, + v8::inspector::StringView::empty(), + ); + + let response_tx_map = HashMap::new(); + let next_message_id = 0; + + let notification_queue = Vec::new(); + + Self { + v8_channel, + v8_session, + response_tx_map, + next_message_id, + notification_queue, + } + }) + } + + pub fn notifications(&mut self) -> Vec { + self.notification_queue.split_off(0) + } + + pub async fn post_message( + &mut self, + method: &str, + params: Option, + ) -> Result { + let id = self.next_message_id; + self.next_message_id += 1; + + let (response_tx, response_rx) = oneshot::channel::(); + self.response_tx_map.insert(id, response_tx); + + let message = json!({ + "id": id, + "method": method, + "params": params, + }); + + let raw_message = serde_json::to_string(&message).unwrap(); + let raw_message = v8::inspector::StringView::from(raw_message.as_bytes()); + self.v8_session.dispatch_protocol_message(raw_message); + + let response = response_rx.await.unwrap(); + if let Some(error) = response.get("error") { + return Err(generic_error(error.to_string())); + } + + let result = response.get("result").unwrap().clone(); + Ok(result) + } +} + +fn new_box_with(new_fn: impl FnOnce(*mut T) -> T) -> Box { + let b = Box::new(MaybeUninit::::uninit()); + let p = Box::into_raw(b) as *mut T; + unsafe { ptr::write(p, new_fn(p)) }; + unsafe { Box::from_raw(p) } +} diff --git a/runtime/js.rs b/runtime/js.rs new file mode 100644 index 000000000..efbc958c7 --- /dev/null +++ b/runtime/js.rs @@ -0,0 +1,31 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use deno_core::Snapshot; + +pub static CLI_SNAPSHOT: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/CLI_SNAPSHOT.bin")); + +pub fn deno_isolate_init() -> Snapshot { + debug!("Deno isolate init with snapshots."); + let data = CLI_SNAPSHOT; + Snapshot::Static(data) +} + +#[test] +fn cli_snapshot() { + let mut js_runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions { + startup_snapshot: Some(deno_isolate_init()), + ..Default::default() + }); + js_runtime + .execute( + "", + r#" + if (!(bootstrap.mainRuntime && bootstrap.workerRuntime)) { + throw Error("bad"); + } + console.log("we have console.log!!!"); + "#, + ) + .unwrap(); +} diff --git a/runtime/lib.rs b/runtime/lib.rs new file mode 100644 index 000000000..6745f3ec8 --- /dev/null +++ b/runtime/lib.rs @@ -0,0 +1,26 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +#![deny(warnings)] + +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate log; + +pub use deno_crypto; +pub use deno_fetch; +pub use deno_web; + +pub mod colors; +pub mod errors; +pub mod fs_util; +pub mod http_util; +pub mod inspector; +pub mod js; +pub mod metrics; +pub mod ops; +pub mod permissions; +pub mod resolve_addr; +pub mod tokio_util; +pub mod web_worker; +pub mod worker; diff --git a/runtime/metrics.rs b/runtime/metrics.rs new file mode 100644 index 000000000..c70e0dab9 --- /dev/null +++ b/runtime/metrics.rs @@ -0,0 +1,131 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +#[derive(Default, Debug)] +pub struct Metrics { + pub ops_dispatched: u64, + pub ops_dispatched_sync: u64, + pub ops_dispatched_async: u64, + pub ops_dispatched_async_unref: u64, + pub ops_completed: u64, + pub ops_completed_sync: u64, + pub ops_completed_async: u64, + pub ops_completed_async_unref: u64, + pub bytes_sent_control: u64, + pub bytes_sent_data: u64, + pub bytes_received: u64, +} + +impl Metrics { + fn op_dispatched( + &mut self, + bytes_sent_control: usize, + bytes_sent_data: usize, + ) { + self.ops_dispatched += 1; + self.bytes_sent_control += bytes_sent_control as u64; + self.bytes_sent_data += bytes_sent_data as u64; + } + + fn op_completed(&mut self, bytes_received: usize) { + self.ops_completed += 1; + self.bytes_received += bytes_received as u64; + } + + pub fn op_sync( + &mut self, + bytes_sent_control: usize, + bytes_sent_data: usize, + bytes_received: usize, + ) { + self.ops_dispatched_sync += 1; + self.op_dispatched(bytes_sent_control, bytes_sent_data); + self.ops_completed_sync += 1; + self.op_completed(bytes_received); + } + + pub fn op_dispatched_async( + &mut self, + bytes_sent_control: usize, + bytes_sent_data: usize, + ) { + self.ops_dispatched_async += 1; + self.op_dispatched(bytes_sent_control, bytes_sent_data) + } + + pub fn op_dispatched_async_unref( + &mut self, + bytes_sent_control: usize, + bytes_sent_data: usize, + ) { + self.ops_dispatched_async_unref += 1; + self.op_dispatched(bytes_sent_control, bytes_sent_data) + } + + pub fn op_completed_async(&mut self, bytes_received: usize) { + self.ops_completed_async += 1; + self.op_completed(bytes_received); + } + + pub fn op_completed_async_unref(&mut self, bytes_received: usize) { + self.ops_completed_async_unref += 1; + self.op_completed(bytes_received); + } +} + +use deno_core::BufVec; +use deno_core::Op; +use deno_core::OpFn; +use deno_core::OpState; +use std::cell::RefCell; +use std::rc::Rc; + +pub fn metrics_op(op_fn: Box) -> Box { + Box::new(move |op_state: Rc>, bufs: BufVec| -> Op { + // TODOs: + // * The 'bytes' metrics seem pretty useless, especially now that the + // distinction between 'control' and 'data' buffers has become blurry. + // * Tracking completion of async ops currently makes us put the boxed + // future into _another_ box. Keeping some counters may not be expensive + // in itself, but adding a heap allocation for every metric seems bad. + let mut buf_len_iter = bufs.iter().map(|buf| buf.len()); + let bytes_sent_control = buf_len_iter.next().unwrap_or(0); + let bytes_sent_data = buf_len_iter.sum(); + + let op = (op_fn)(op_state.clone(), bufs); + + let op_state_ = op_state.clone(); + let mut s = op_state.borrow_mut(); + let metrics = s.borrow_mut::(); + + use deno_core::futures::future::FutureExt; + + match op { + Op::Sync(buf) => { + metrics.op_sync(bytes_sent_control, bytes_sent_data, buf.len()); + Op::Sync(buf) + } + Op::Async(fut) => { + metrics.op_dispatched_async(bytes_sent_control, bytes_sent_data); + let fut = fut + .inspect(move |buf| { + let mut s = op_state_.borrow_mut(); + let metrics = s.borrow_mut::(); + metrics.op_completed_async(buf.len()); + }) + .boxed_local(); + Op::Async(fut) + } + Op::AsyncUnref(fut) => { + metrics.op_dispatched_async_unref(bytes_sent_control, bytes_sent_data); + let fut = fut + .inspect(move |buf| { + let mut s = op_state_.borrow_mut(); + let metrics = s.borrow_mut::(); + metrics.op_completed_async_unref(buf.len()); + }) + .boxed_local(); + Op::AsyncUnref(fut) + } + other => other, + } + }) +} diff --git a/runtime/ops/crypto.rs b/runtime/ops/crypto.rs new file mode 100644 index 000000000..a73843a33 --- /dev/null +++ b/runtime/ops/crypto.rs @@ -0,0 +1,14 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use deno_crypto::op_get_random_values; +use deno_crypto::rand::rngs::StdRng; +use deno_crypto::rand::SeedableRng; + +pub fn init(rt: &mut deno_core::JsRuntime, maybe_seed: Option) { + if let Some(seed) = maybe_seed { + let rng = StdRng::seed_from_u64(seed); + let op_state = rt.op_state(); + let mut state = op_state.borrow_mut(); + state.put::(rng); + } + super::reg_json_sync(rt, "op_get_random_values", op_get_random_values); +} diff --git a/runtime/ops/dispatch_minimal.rs b/runtime/ops/dispatch_minimal.rs new file mode 100644 index 000000000..ae8fa819d --- /dev/null +++ b/runtime/ops/dispatch_minimal.rs @@ -0,0 +1,205 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::AnyError; +use deno_core::futures::future::FutureExt; +use deno_core::BufVec; +use deno_core::Op; +use deno_core::OpFn; +use deno_core::OpState; +use std::cell::RefCell; +use std::future::Future; +use std::iter::repeat; +use std::mem::size_of_val; +use std::pin::Pin; +use std::rc::Rc; +use std::slice; + +pub enum MinimalOp { + Sync(Result), + Async(Pin>>>), +} + +#[derive(Copy, Clone, Debug, PartialEq)] +// This corresponds to RecordMinimal on the TS side. +pub struct Record { + pub promise_id: i32, + pub arg: i32, + pub result: i32, +} + +impl Into> for Record { + fn into(self) -> Box<[u8]> { + let vec = vec![self.promise_id, self.arg, self.result]; + let buf32 = vec.into_boxed_slice(); + let ptr = Box::into_raw(buf32) as *mut [u8; 3 * 4]; + unsafe { Box::from_raw(ptr) } + } +} + +pub struct ErrorRecord { + pub promise_id: i32, + pub arg: i32, + pub error_len: i32, + pub error_class: &'static [u8], + pub error_message: Vec, +} + +impl Into> for ErrorRecord { + fn into(self) -> Box<[u8]> { + let Self { + promise_id, + arg, + error_len, + error_class, + error_message, + .. + } = self; + let header_i32 = [promise_id, arg, error_len]; + let header_u8 = unsafe { + slice::from_raw_parts( + &header_i32 as *const _ as *const u8, + size_of_val(&header_i32), + ) + }; + let padded_len = + (header_u8.len() + error_class.len() + error_message.len() + 3usize) + & !3usize; + header_u8 + .iter() + .cloned() + .chain(error_class.iter().cloned()) + .chain(error_message.into_iter()) + .chain(repeat(b' ')) + .take(padded_len) + .collect() + } +} + +#[test] +fn test_error_record() { + let expected = vec![ + 1, 0, 0, 0, 255, 255, 255, 255, 11, 0, 0, 0, 66, 97, 100, 82, 101, 115, + 111, 117, 114, 99, 101, 69, 114, 114, 111, 114, + ]; + let err_record = ErrorRecord { + promise_id: 1, + arg: -1, + error_len: 11, + error_class: b"BadResource", + error_message: b"Error".to_vec(), + }; + let buf: Box<[u8]> = err_record.into(); + assert_eq!(buf, expected.into_boxed_slice()); +} + +pub fn parse_min_record(bytes: &[u8]) -> Option { + if bytes.len() % std::mem::size_of::() != 0 { + return None; + } + let p = bytes.as_ptr(); + #[allow(clippy::cast_ptr_alignment)] + let p32 = p as *const i32; + let s = unsafe { std::slice::from_raw_parts(p32, bytes.len() / 4) }; + + if s.len() != 3 { + return None; + } + let ptr = s.as_ptr(); + let ints = unsafe { std::slice::from_raw_parts(ptr, 3) }; + Some(Record { + promise_id: ints[0], + arg: ints[1], + result: ints[2], + }) +} + +#[test] +fn test_parse_min_record() { + let buf = vec![1, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0]; + assert_eq!( + parse_min_record(&buf), + Some(Record { + promise_id: 1, + arg: 3, + result: 4 + }) + ); + + let buf = vec![]; + assert_eq!(parse_min_record(&buf), None); + + let buf = vec![5]; + assert_eq!(parse_min_record(&buf), None); +} + +pub fn minimal_op(op_fn: F) -> Box +where + F: Fn(Rc>, bool, i32, BufVec) -> MinimalOp + 'static, +{ + Box::new(move |state: Rc>, bufs: BufVec| { + let mut bufs_iter = bufs.into_iter(); + let record_buf = bufs_iter.next().expect("Expected record at position 0"); + let zero_copy = bufs_iter.collect::(); + + let mut record = match parse_min_record(&record_buf) { + Some(r) => r, + None => { + let error_class = b"TypeError"; + let error_message = b"Unparsable control buffer"; + let error_record = ErrorRecord { + promise_id: 0, + arg: -1, + error_len: error_class.len() as i32, + error_class, + error_message: error_message[..].to_owned(), + }; + return Op::Sync(error_record.into()); + } + }; + let is_sync = record.promise_id == 0; + let rid = record.arg; + let min_op = op_fn(state.clone(), is_sync, rid, zero_copy); + + match min_op { + MinimalOp::Sync(sync_result) => Op::Sync(match sync_result { + Ok(r) => { + record.result = r; + record.into() + } + Err(err) => { + let error_class = (state.borrow().get_error_class_fn)(&err); + let error_record = ErrorRecord { + promise_id: record.promise_id, + arg: -1, + error_len: error_class.len() as i32, + error_class: error_class.as_bytes(), + error_message: err.to_string().as_bytes().to_owned(), + }; + error_record.into() + } + }), + MinimalOp::Async(min_fut) => { + let fut = async move { + match min_fut.await { + Ok(r) => { + record.result = r; + record.into() + } + Err(err) => { + let error_class = (state.borrow().get_error_class_fn)(&err); + let error_record = ErrorRecord { + promise_id: record.promise_id, + arg: -1, + error_len: error_class.len() as i32, + error_class: error_class.as_bytes(), + error_message: err.to_string().as_bytes().to_owned(), + }; + error_record.into() + } + } + }; + Op::Async(fut.boxed_local()) + } + } + }) +} diff --git a/runtime/ops/fetch.rs b/runtime/ops/fetch.rs new file mode 100644 index 000000000..0ef99f73d --- /dev/null +++ b/runtime/ops/fetch.rs @@ -0,0 +1,25 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use crate::http_util; +use crate::permissions::Permissions; +use deno_fetch::reqwest; + +pub fn init( + rt: &mut deno_core::JsRuntime, + user_agent: String, + maybe_ca_file: Option<&str>, +) { + { + let op_state = rt.op_state(); + let mut state = op_state.borrow_mut(); + state.put::({ + http_util::create_http_client(user_agent, maybe_ca_file).unwrap() + }); + } + super::reg_json_async(rt, "op_fetch", deno_fetch::op_fetch::); + super::reg_json_async(rt, "op_fetch_read", deno_fetch::op_fetch_read); + super::reg_json_sync( + rt, + "op_create_http_client", + deno_fetch::op_create_http_client::, + ); +} diff --git a/runtime/ops/fs.rs b/runtime/ops/fs.rs new file mode 100644 index 000000000..865c5bcca --- /dev/null +++ b/runtime/ops/fs.rs @@ -0,0 +1,1702 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +// Some deserializer fields are only used on Unix and Windows build fails without it +use super::io::std_file_resource; +use super::io::{FileMetadata, StreamResource, StreamResourceHolder}; +use crate::fs_util::canonicalize_path; +use crate::permissions::Permissions; +use deno_core::error::custom_error; +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::BufVec; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; +use deno_crypto::rand::thread_rng; +use deno_crypto::rand::Rng; +use serde::Deserialize; +use std::cell::RefCell; +use std::convert::From; +use std::env::{current_dir, set_current_dir, temp_dir}; +use std::io; +use std::io::{Seek, SeekFrom}; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; + +#[cfg(not(unix))] +use deno_core::error::generic_error; +#[cfg(not(unix))] +use deno_core::error::not_supported; + +pub fn init(rt: &mut deno_core::JsRuntime) { + super::reg_json_sync(rt, "op_open_sync", op_open_sync); + super::reg_json_async(rt, "op_open_async", op_open_async); + + super::reg_json_sync(rt, "op_seek_sync", op_seek_sync); + super::reg_json_async(rt, "op_seek_async", op_seek_async); + + super::reg_json_sync(rt, "op_fdatasync_sync", op_fdatasync_sync); + super::reg_json_async(rt, "op_fdatasync_async", op_fdatasync_async); + + super::reg_json_sync(rt, "op_fsync_sync", op_fsync_sync); + super::reg_json_async(rt, "op_fsync_async", op_fsync_async); + + super::reg_json_sync(rt, "op_fstat_sync", op_fstat_sync); + super::reg_json_async(rt, "op_fstat_async", op_fstat_async); + + super::reg_json_sync(rt, "op_umask", op_umask); + super::reg_json_sync(rt, "op_chdir", op_chdir); + + super::reg_json_sync(rt, "op_mkdir_sync", op_mkdir_sync); + super::reg_json_async(rt, "op_mkdir_async", op_mkdir_async); + + super::reg_json_sync(rt, "op_chmod_sync", op_chmod_sync); + super::reg_json_async(rt, "op_chmod_async", op_chmod_async); + + super::reg_json_sync(rt, "op_chown_sync", op_chown_sync); + super::reg_json_async(rt, "op_chown_async", op_chown_async); + + super::reg_json_sync(rt, "op_remove_sync", op_remove_sync); + super::reg_json_async(rt, "op_remove_async", op_remove_async); + + super::reg_json_sync(rt, "op_copy_file_sync", op_copy_file_sync); + super::reg_json_async(rt, "op_copy_file_async", op_copy_file_async); + + super::reg_json_sync(rt, "op_stat_sync", op_stat_sync); + super::reg_json_async(rt, "op_stat_async", op_stat_async); + + super::reg_json_sync(rt, "op_realpath_sync", op_realpath_sync); + super::reg_json_async(rt, "op_realpath_async", op_realpath_async); + + super::reg_json_sync(rt, "op_read_dir_sync", op_read_dir_sync); + super::reg_json_async(rt, "op_read_dir_async", op_read_dir_async); + + super::reg_json_sync(rt, "op_rename_sync", op_rename_sync); + super::reg_json_async(rt, "op_rename_async", op_rename_async); + + super::reg_json_sync(rt, "op_link_sync", op_link_sync); + super::reg_json_async(rt, "op_link_async", op_link_async); + + super::reg_json_sync(rt, "op_symlink_sync", op_symlink_sync); + super::reg_json_async(rt, "op_symlink_async", op_symlink_async); + + super::reg_json_sync(rt, "op_read_link_sync", op_read_link_sync); + super::reg_json_async(rt, "op_read_link_async", op_read_link_async); + + super::reg_json_sync(rt, "op_ftruncate_sync", op_ftruncate_sync); + super::reg_json_async(rt, "op_ftruncate_async", op_ftruncate_async); + + super::reg_json_sync(rt, "op_truncate_sync", op_truncate_sync); + super::reg_json_async(rt, "op_truncate_async", op_truncate_async); + + super::reg_json_sync(rt, "op_make_temp_dir_sync", op_make_temp_dir_sync); + super::reg_json_async(rt, "op_make_temp_dir_async", op_make_temp_dir_async); + + super::reg_json_sync(rt, "op_make_temp_file_sync", op_make_temp_file_sync); + super::reg_json_async(rt, "op_make_temp_file_async", op_make_temp_file_async); + + super::reg_json_sync(rt, "op_cwd", op_cwd); + + super::reg_json_sync(rt, "op_futime_sync", op_futime_sync); + super::reg_json_async(rt, "op_futime_async", op_futime_async); + + super::reg_json_sync(rt, "op_utime_sync", op_utime_sync); + super::reg_json_async(rt, "op_utime_async", op_utime_async); +} + +fn into_string(s: std::ffi::OsString) -> Result { + s.into_string().map_err(|s| { + let message = format!("File name or path {:?} is not valid UTF-8", s); + custom_error("InvalidData", message) + }) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct OpenArgs { + path: String, + mode: Option, + options: OpenOptions, +} + +#[derive(Deserialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(default)] +struct OpenOptions { + read: bool, + write: bool, + create: bool, + truncate: bool, + append: bool, + create_new: bool, +} + +fn open_helper( + state: &mut OpState, + args: Value, +) -> Result<(PathBuf, std::fs::OpenOptions), AnyError> { + let args: OpenArgs = serde_json::from_value(args)?; + let path = Path::new(&args.path).to_path_buf(); + + let mut open_options = std::fs::OpenOptions::new(); + + if let Some(mode) = args.mode { + // mode only used if creating the file on Unix + // if not specified, defaults to 0o666 + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + open_options.mode(mode & 0o777); + } + #[cfg(not(unix))] + let _ = mode; // avoid unused warning + } + + let permissions = state.borrow::(); + let options = args.options; + + if options.read { + permissions.check_read(&path)?; + } + + if options.write || options.append { + permissions.check_write(&path)?; + } + + open_options + .read(options.read) + .create(options.create) + .write(options.write) + .truncate(options.truncate) + .append(options.append) + .create_new(options.create_new); + + Ok((path, open_options)) +} + +fn op_open_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let (path, open_options) = open_helper(state, args)?; + let std_file = open_options.open(path)?; + let tokio_file = tokio::fs::File::from_std(std_file); + let rid = state.resource_table.add( + "fsFile", + Box::new(StreamResourceHolder::new(StreamResource::FsFile(Some(( + tokio_file, + FileMetadata::default(), + ))))), + ); + Ok(json!(rid)) +} + +async fn op_open_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let (path, open_options) = open_helper(&mut state.borrow_mut(), args)?; + let tokio_file = tokio::fs::OpenOptions::from(open_options) + .open(path) + .await?; + let rid = state.borrow_mut().resource_table.add( + "fsFile", + Box::new(StreamResourceHolder::new(StreamResource::FsFile(Some(( + tokio_file, + FileMetadata::default(), + ))))), + ); + Ok(json!(rid)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SeekArgs { + rid: i32, + offset: i64, + whence: i32, +} + +fn seek_helper(args: Value) -> Result<(u32, SeekFrom), AnyError> { + let args: SeekArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + let offset = args.offset; + let whence = args.whence as u32; + // Translate seek mode to Rust repr. + let seek_from = match whence { + 0 => SeekFrom::Start(offset as u64), + 1 => SeekFrom::Current(offset), + 2 => SeekFrom::End(offset), + _ => { + return Err(type_error(format!("Invalid seek mode: {}", whence))); + } + }; + + Ok((rid, seek_from)) +} + +fn op_seek_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let (rid, seek_from) = seek_helper(args)?; + let pos = std_file_resource(state, rid, |r| match r { + Ok(std_file) => std_file.seek(seek_from).map_err(AnyError::from), + Err(_) => Err(type_error( + "cannot seek on this type of resource".to_string(), + )), + })?; + Ok(json!(pos)) +} + +async fn op_seek_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let (rid, seek_from) = seek_helper(args)?; + // TODO(ry) This is a fake async op. We need to use poll_fn, + // tokio::fs::File::start_seek and tokio::fs::File::poll_complete + let pos = std_file_resource(&mut state.borrow_mut(), rid, |r| match r { + Ok(std_file) => std_file.seek(seek_from).map_err(AnyError::from), + Err(_) => Err(type_error( + "cannot seek on this type of resource".to_string(), + )), + })?; + Ok(json!(pos)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct FdatasyncArgs { + rid: i32, +} + +fn op_fdatasync_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: FdatasyncArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + std_file_resource(state, rid, |r| match r { + Ok(std_file) => std_file.sync_data().map_err(AnyError::from), + Err(_) => Err(type_error("cannot sync this type of resource".to_string())), + })?; + Ok(json!({})) +} + +async fn op_fdatasync_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: FdatasyncArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + std_file_resource(&mut state.borrow_mut(), rid, |r| match r { + Ok(std_file) => std_file.sync_data().map_err(AnyError::from), + Err(_) => Err(type_error("cannot sync this type of resource".to_string())), + })?; + Ok(json!({})) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct FsyncArgs { + rid: i32, +} + +fn op_fsync_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: FsyncArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + std_file_resource(state, rid, |r| match r { + Ok(std_file) => std_file.sync_all().map_err(AnyError::from), + Err(_) => Err(type_error("cannot sync this type of resource".to_string())), + })?; + Ok(json!({})) +} + +async fn op_fsync_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: FsyncArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + std_file_resource(&mut state.borrow_mut(), rid, |r| match r { + Ok(std_file) => std_file.sync_all().map_err(AnyError::from), + Err(_) => Err(type_error("cannot sync this type of resource".to_string())), + })?; + Ok(json!({})) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct FstatArgs { + rid: i32, +} + +fn op_fstat_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.fstat"); + let args: FstatArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + let metadata = std_file_resource(state, rid, |r| match r { + Ok(std_file) => std_file.metadata().map_err(AnyError::from), + Err(_) => Err(type_error("cannot stat this type of resource".to_string())), + })?; + Ok(get_stat_json(metadata)) +} + +async fn op_fstat_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + super::check_unstable2(&state, "Deno.fstat"); + + let args: FstatArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + let metadata = + std_file_resource(&mut state.borrow_mut(), rid, |r| match r { + Ok(std_file) => std_file.metadata().map_err(AnyError::from), + Err(_) => { + Err(type_error("cannot stat this type of resource".to_string())) + } + })?; + Ok(get_stat_json(metadata)) +} + +#[derive(Deserialize)] +struct UmaskArgs { + mask: Option, +} + +fn op_umask( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.umask"); + let args: UmaskArgs = serde_json::from_value(args)?; + // TODO implement umask for Windows + // see https://github.com/nodejs/node/blob/master/src/node_process_methods.cc + // and https://docs.microsoft.com/fr-fr/cpp/c-runtime-library/reference/umask?view=vs-2019 + #[cfg(not(unix))] + { + let _ = args.mask; // avoid unused warning. + Err(not_supported()) + } + #[cfg(unix)] + { + use nix::sys::stat::mode_t; + use nix::sys::stat::umask; + use nix::sys::stat::Mode; + let r = if let Some(mask) = args.mask { + // If mask provided, return previous. + umask(Mode::from_bits_truncate(mask as mode_t)) + } else { + // If no mask provided, we query the current. Requires two syscalls. + let prev = umask(Mode::from_bits_truncate(0o777)); + let _ = umask(prev); + prev + }; + Ok(json!(r.bits() as u32)) + } +} + +#[derive(Deserialize)] +struct ChdirArgs { + directory: String, +} + +fn op_chdir( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: ChdirArgs = serde_json::from_value(args)?; + let d = PathBuf::from(&args.directory); + state.borrow::().check_read(&d)?; + set_current_dir(&d)?; + Ok(json!({})) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct MkdirArgs { + path: String, + recursive: bool, + mode: Option, +} + +fn op_mkdir_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: MkdirArgs = serde_json::from_value(args)?; + let path = Path::new(&args.path).to_path_buf(); + let mode = args.mode.unwrap_or(0o777) & 0o777; + state.borrow::().check_write(&path)?; + debug!("op_mkdir {} {:o} {}", path.display(), mode, args.recursive); + let mut builder = std::fs::DirBuilder::new(); + builder.recursive(args.recursive); + #[cfg(unix)] + { + use std::os::unix::fs::DirBuilderExt; + builder.mode(mode); + } + builder.create(path)?; + Ok(json!({})) +} + +async fn op_mkdir_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: MkdirArgs = serde_json::from_value(args)?; + let path = Path::new(&args.path).to_path_buf(); + let mode = args.mode.unwrap_or(0o777) & 0o777; + + { + let state = state.borrow(); + state.borrow::().check_write(&path)?; + } + + tokio::task::spawn_blocking(move || { + debug!("op_mkdir {} {:o} {}", path.display(), mode, args.recursive); + let mut builder = std::fs::DirBuilder::new(); + builder.recursive(args.recursive); + #[cfg(unix)] + { + use std::os::unix::fs::DirBuilderExt; + builder.mode(mode); + } + builder.create(path)?; + Ok(json!({})) + }) + .await + .unwrap() +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ChmodArgs { + path: String, + mode: u32, +} + +fn op_chmod_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: ChmodArgs = serde_json::from_value(args)?; + let path = Path::new(&args.path).to_path_buf(); + let mode = args.mode & 0o777; + + state.borrow::().check_write(&path)?; + debug!("op_chmod_sync {} {:o}", path.display(), mode); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let permissions = PermissionsExt::from_mode(mode); + std::fs::set_permissions(&path, permissions)?; + Ok(json!({})) + } + // TODO Implement chmod for Windows (#4357) + #[cfg(not(unix))] + { + // Still check file/dir exists on Windows + let _metadata = std::fs::metadata(&path)?; + Err(generic_error("Not implemented")) + } +} + +async fn op_chmod_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: ChmodArgs = serde_json::from_value(args)?; + let path = Path::new(&args.path).to_path_buf(); + let mode = args.mode & 0o777; + + { + let state = state.borrow(); + state.borrow::().check_write(&path)?; + } + + tokio::task::spawn_blocking(move || { + debug!("op_chmod_async {} {:o}", path.display(), mode); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let permissions = PermissionsExt::from_mode(mode); + std::fs::set_permissions(&path, permissions)?; + Ok(json!({})) + } + // TODO Implement chmod for Windows (#4357) + #[cfg(not(unix))] + { + // Still check file/dir exists on Windows + let _metadata = std::fs::metadata(&path)?; + Err(not_supported()) + } + }) + .await + .unwrap() +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ChownArgs { + path: String, + uid: Option, + gid: Option, +} + +fn op_chown_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: ChownArgs = serde_json::from_value(args)?; + let path = Path::new(&args.path).to_path_buf(); + state.borrow::().check_write(&path)?; + debug!( + "op_chown_sync {} {:?} {:?}", + path.display(), + args.uid, + args.gid, + ); + #[cfg(unix)] + { + use nix::unistd::{chown, Gid, Uid}; + let nix_uid = args.uid.map(Uid::from_raw); + let nix_gid = args.gid.map(Gid::from_raw); + chown(&path, nix_uid, nix_gid)?; + Ok(json!({})) + } + // TODO Implement chown for Windows + #[cfg(not(unix))] + { + Err(generic_error("Not implemented")) + } +} + +async fn op_chown_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: ChownArgs = serde_json::from_value(args)?; + let path = Path::new(&args.path).to_path_buf(); + + { + let state = state.borrow(); + state.borrow::().check_write(&path)?; + } + + tokio::task::spawn_blocking(move || { + debug!( + "op_chown_async {} {:?} {:?}", + path.display(), + args.uid, + args.gid, + ); + #[cfg(unix)] + { + use nix::unistd::{chown, Gid, Uid}; + let nix_uid = args.uid.map(Uid::from_raw); + let nix_gid = args.gid.map(Gid::from_raw); + chown(&path, nix_uid, nix_gid)?; + Ok(json!({})) + } + // TODO Implement chown for Windows + #[cfg(not(unix))] + Err(not_supported()) + }) + .await + .unwrap() +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct RemoveArgs { + path: String, + recursive: bool, +} + +fn op_remove_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: RemoveArgs = serde_json::from_value(args)?; + let path = PathBuf::from(&args.path); + let recursive = args.recursive; + + state.borrow::().check_write(&path)?; + + #[cfg(not(unix))] + use std::os::windows::prelude::MetadataExt; + + let metadata = std::fs::symlink_metadata(&path)?; + + debug!("op_remove_sync {} {}", path.display(), recursive); + let file_type = metadata.file_type(); + if file_type.is_file() { + std::fs::remove_file(&path)?; + } else if recursive { + std::fs::remove_dir_all(&path)?; + } else if file_type.is_symlink() { + #[cfg(unix)] + std::fs::remove_file(&path)?; + #[cfg(not(unix))] + { + use winapi::um::winnt::FILE_ATTRIBUTE_DIRECTORY; + if metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 { + std::fs::remove_dir(&path)?; + } else { + std::fs::remove_file(&path)?; + } + } + } else if file_type.is_dir() { + std::fs::remove_dir(&path)?; + } else { + // pipes, sockets, etc... + std::fs::remove_file(&path)?; + } + Ok(json!({})) +} + +async fn op_remove_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: RemoveArgs = serde_json::from_value(args)?; + let path = PathBuf::from(&args.path); + let recursive = args.recursive; + + { + let state = state.borrow(); + state.borrow::().check_write(&path)?; + } + + tokio::task::spawn_blocking(move || { + #[cfg(not(unix))] + use std::os::windows::prelude::MetadataExt; + + let metadata = std::fs::symlink_metadata(&path)?; + + debug!("op_remove_async {} {}", path.display(), recursive); + let file_type = metadata.file_type(); + if file_type.is_file() { + std::fs::remove_file(&path)?; + } else if recursive { + std::fs::remove_dir_all(&path)?; + } else if file_type.is_symlink() { + #[cfg(unix)] + std::fs::remove_file(&path)?; + #[cfg(not(unix))] + { + use winapi::um::winnt::FILE_ATTRIBUTE_DIRECTORY; + if metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 { + std::fs::remove_dir(&path)?; + } else { + std::fs::remove_file(&path)?; + } + } + } else if file_type.is_dir() { + std::fs::remove_dir(&path)?; + } else { + // pipes, sockets, etc... + std::fs::remove_file(&path)?; + } + Ok(json!({})) + }) + .await + .unwrap() +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct CopyFileArgs { + from: String, + to: String, +} + +fn op_copy_file_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: CopyFileArgs = serde_json::from_value(args)?; + let from = PathBuf::from(&args.from); + let to = PathBuf::from(&args.to); + + let permissions = state.borrow::(); + permissions.check_read(&from)?; + permissions.check_write(&to)?; + + debug!("op_copy_file_sync {} {}", from.display(), to.display()); + // On *nix, Rust reports non-existent `from` as ErrorKind::InvalidInput + // See https://github.com/rust-lang/rust/issues/54800 + // Once the issue is resolved, we should remove this workaround. + if cfg!(unix) && !from.is_file() { + return Err(custom_error("NotFound", "File not found")); + } + + // returns size of from as u64 (we ignore) + std::fs::copy(&from, &to)?; + Ok(json!({})) +} + +async fn op_copy_file_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: CopyFileArgs = serde_json::from_value(args)?; + let from = PathBuf::from(&args.from); + let to = PathBuf::from(&args.to); + + { + let state = state.borrow(); + let permissions = state.borrow::(); + permissions.check_read(&from)?; + permissions.check_write(&to)?; + } + + debug!("op_copy_file_async {} {}", from.display(), to.display()); + tokio::task::spawn_blocking(move || { + // On *nix, Rust reports non-existent `from` as ErrorKind::InvalidInput + // See https://github.com/rust-lang/rust/issues/54800 + // Once the issue is resolved, we should remove this workaround. + if cfg!(unix) && !from.is_file() { + return Err(custom_error("NotFound", "File not found")); + } + + // returns size of from as u64 (we ignore) + std::fs::copy(&from, &to)?; + Ok(json!({})) + }) + .await + .unwrap() +} + +fn to_msec(maybe_time: Result) -> Value { + match maybe_time { + Ok(time) => { + let msec = time + .duration_since(UNIX_EPOCH) + .map(|t| t.as_secs_f64() * 1000f64) + .unwrap_or_else(|err| err.duration().as_secs_f64() * -1000f64); + serde_json::Number::from_f64(msec) + .map(Value::Number) + .unwrap_or(Value::Null) + } + Err(_) => Value::Null, + } +} + +#[inline(always)] +fn get_stat_json(metadata: std::fs::Metadata) -> Value { + // Unix stat member (number types only). 0 if not on unix. + macro_rules! usm { + ($member:ident) => {{ + #[cfg(unix)] + { + metadata.$member() + } + #[cfg(not(unix))] + { + 0 + } + }}; + } + + #[cfg(unix)] + use std::os::unix::fs::MetadataExt; + let json_val = json!({ + "isFile": metadata.is_file(), + "isDirectory": metadata.is_dir(), + "isSymlink": metadata.file_type().is_symlink(), + "size": metadata.len(), + // In milliseconds, like JavaScript. Available on both Unix or Windows. + "mtime": to_msec(metadata.modified()), + "atime": to_msec(metadata.accessed()), + "birthtime": to_msec(metadata.created()), + // Following are only valid under Unix. + "dev": usm!(dev), + "ino": usm!(ino), + "mode": usm!(mode), + "nlink": usm!(nlink), + "uid": usm!(uid), + "gid": usm!(gid), + "rdev": usm!(rdev), + // TODO(kevinkassimo): *time_nsec requires BigInt. + // Probably should be treated as String if we need to add them. + "blksize": usm!(blksize), + "blocks": usm!(blocks), + }); + json_val +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct StatArgs { + path: String, + lstat: bool, +} + +fn op_stat_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: StatArgs = serde_json::from_value(args)?; + let path = PathBuf::from(&args.path); + let lstat = args.lstat; + state.borrow::().check_read(&path)?; + debug!("op_stat_sync {} {}", path.display(), lstat); + let metadata = if lstat { + std::fs::symlink_metadata(&path)? + } else { + std::fs::metadata(&path)? + }; + Ok(get_stat_json(metadata)) +} + +async fn op_stat_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: StatArgs = serde_json::from_value(args)?; + let path = PathBuf::from(&args.path); + let lstat = args.lstat; + + { + let state = state.borrow(); + state.borrow::().check_read(&path)?; + } + + tokio::task::spawn_blocking(move || { + debug!("op_stat_async {} {}", path.display(), lstat); + let metadata = if lstat { + std::fs::symlink_metadata(&path)? + } else { + std::fs::metadata(&path)? + }; + Ok(get_stat_json(metadata)) + }) + .await + .unwrap() +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct RealpathArgs { + path: String, +} + +fn op_realpath_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: RealpathArgs = serde_json::from_value(args)?; + let path = PathBuf::from(&args.path); + + let permissions = state.borrow::(); + permissions.check_read(&path)?; + if path.is_relative() { + permissions.check_read_blind(¤t_dir()?, "CWD")?; + } + + debug!("op_realpath_sync {}", path.display()); + // corresponds to the realpath on Unix and + // CreateFile and GetFinalPathNameByHandle on Windows + let realpath = canonicalize_path(&path)?; + let realpath_str = into_string(realpath.into_os_string())?; + Ok(json!(realpath_str)) +} + +async fn op_realpath_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: RealpathArgs = serde_json::from_value(args)?; + let path = PathBuf::from(&args.path); + + { + let state = state.borrow(); + let permissions = state.borrow::(); + permissions.check_read(&path)?; + if path.is_relative() { + permissions.check_read_blind(¤t_dir()?, "CWD")?; + } + } + + tokio::task::spawn_blocking(move || { + debug!("op_realpath_async {}", path.display()); + // corresponds to the realpath on Unix and + // CreateFile and GetFinalPathNameByHandle on Windows + let realpath = canonicalize_path(&path)?; + let realpath_str = into_string(realpath.into_os_string())?; + Ok(json!(realpath_str)) + }) + .await + .unwrap() +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ReadDirArgs { + path: String, +} + +fn op_read_dir_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: ReadDirArgs = serde_json::from_value(args)?; + let path = PathBuf::from(&args.path); + + state.borrow::().check_read(&path)?; + + debug!("op_read_dir_sync {}", path.display()); + let entries: Vec<_> = std::fs::read_dir(path)? + .filter_map(|entry| { + let entry = entry.unwrap(); + let file_type = entry.file_type().unwrap(); + // Not all filenames can be encoded as UTF-8. Skip those for now. + if let Ok(name) = into_string(entry.file_name()) { + Some(json!({ + "name": name, + "isFile": file_type.is_file(), + "isDirectory": file_type.is_dir(), + "isSymlink": file_type.is_symlink() + })) + } else { + None + } + }) + .collect(); + + Ok(json!({ "entries": entries })) +} + +async fn op_read_dir_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: ReadDirArgs = serde_json::from_value(args)?; + let path = PathBuf::from(&args.path); + { + let state = state.borrow(); + state.borrow::().check_read(&path)?; + } + tokio::task::spawn_blocking(move || { + debug!("op_read_dir_async {}", path.display()); + let entries: Vec<_> = std::fs::read_dir(path)? + .filter_map(|entry| { + let entry = entry.unwrap(); + let file_type = entry.file_type().unwrap(); + // Not all filenames can be encoded as UTF-8. Skip those for now. + if let Ok(name) = into_string(entry.file_name()) { + Some(json!({ + "name": name, + "isFile": file_type.is_file(), + "isDirectory": file_type.is_dir(), + "isSymlink": file_type.is_symlink() + })) + } else { + None + } + }) + .collect(); + + Ok(json!({ "entries": entries })) + }) + .await + .unwrap() +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct RenameArgs { + oldpath: String, + newpath: String, +} + +fn op_rename_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: RenameArgs = serde_json::from_value(args)?; + let oldpath = PathBuf::from(&args.oldpath); + let newpath = PathBuf::from(&args.newpath); + + let permissions = state.borrow::(); + permissions.check_read(&oldpath)?; + permissions.check_write(&oldpath)?; + permissions.check_write(&newpath)?; + debug!("op_rename_sync {} {}", oldpath.display(), newpath.display()); + std::fs::rename(&oldpath, &newpath)?; + Ok(json!({})) +} + +async fn op_rename_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: RenameArgs = serde_json::from_value(args)?; + let oldpath = PathBuf::from(&args.oldpath); + let newpath = PathBuf::from(&args.newpath); + { + let state = state.borrow(); + let permissions = state.borrow::(); + permissions.check_read(&oldpath)?; + permissions.check_write(&oldpath)?; + permissions.check_write(&newpath)?; + } + tokio::task::spawn_blocking(move || { + debug!( + "op_rename_async {} {}", + oldpath.display(), + newpath.display() + ); + std::fs::rename(&oldpath, &newpath)?; + Ok(json!({})) + }) + .await + .unwrap() +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct LinkArgs { + oldpath: String, + newpath: String, +} + +fn op_link_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.link"); + let args: LinkArgs = serde_json::from_value(args)?; + let oldpath = PathBuf::from(&args.oldpath); + let newpath = PathBuf::from(&args.newpath); + + let permissions = state.borrow::(); + permissions.check_read(&oldpath)?; + permissions.check_write(&newpath)?; + + debug!("op_link_sync {} {}", oldpath.display(), newpath.display()); + std::fs::hard_link(&oldpath, &newpath)?; + Ok(json!({})) +} + +async fn op_link_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + super::check_unstable2(&state, "Deno.link"); + + let args: LinkArgs = serde_json::from_value(args)?; + let oldpath = PathBuf::from(&args.oldpath); + let newpath = PathBuf::from(&args.newpath); + + { + let state = state.borrow(); + let permissions = state.borrow::(); + permissions.check_read(&oldpath)?; + permissions.check_write(&newpath)?; + } + + tokio::task::spawn_blocking(move || { + debug!("op_link_async {} {}", oldpath.display(), newpath.display()); + std::fs::hard_link(&oldpath, &newpath)?; + Ok(json!({})) + }) + .await + .unwrap() +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SymlinkArgs { + oldpath: String, + newpath: String, + #[cfg(not(unix))] + options: Option, +} + +#[cfg(not(unix))] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SymlinkOptions { + _type: String, +} + +fn op_symlink_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.symlink"); + let args: SymlinkArgs = serde_json::from_value(args)?; + let oldpath = PathBuf::from(&args.oldpath); + let newpath = PathBuf::from(&args.newpath); + + state.borrow::().check_write(&newpath)?; + + debug!( + "op_symlink_sync {} {}", + oldpath.display(), + newpath.display() + ); + #[cfg(unix)] + { + use std::os::unix::fs::symlink; + symlink(&oldpath, &newpath)?; + Ok(json!({})) + } + #[cfg(not(unix))] + { + use std::os::windows::fs::{symlink_dir, symlink_file}; + + match args.options { + Some(options) => match options._type.as_ref() { + "file" => symlink_file(&oldpath, &newpath)?, + "dir" => symlink_dir(&oldpath, &newpath)?, + _ => return Err(type_error("unsupported type")), + }, + None => { + let old_meta = std::fs::metadata(&oldpath); + match old_meta { + Ok(metadata) => { + if metadata.is_file() { + symlink_file(&oldpath, &newpath)? + } else if metadata.is_dir() { + symlink_dir(&oldpath, &newpath)? + } + } + Err(_) => return Err(type_error("you must pass a `options` argument for non-existent target path in windows".to_string())), + } + } + }; + Ok(json!({})) + } +} + +async fn op_symlink_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + super::check_unstable2(&state, "Deno.symlink"); + + let args: SymlinkArgs = serde_json::from_value(args)?; + let oldpath = PathBuf::from(&args.oldpath); + let newpath = PathBuf::from(&args.newpath); + + { + let state = state.borrow(); + state.borrow::().check_write(&newpath)?; + } + + tokio::task::spawn_blocking(move || { + debug!("op_symlink_async {} {}", oldpath.display(), newpath.display()); + #[cfg(unix)] + { + use std::os::unix::fs::symlink; + symlink(&oldpath, &newpath)?; + Ok(json!({})) + } + #[cfg(not(unix))] + { + use std::os::windows::fs::{symlink_dir, symlink_file}; + + match args.options { + Some(options) => match options._type.as_ref() { + "file" => symlink_file(&oldpath, &newpath)?, + "dir" => symlink_dir(&oldpath, &newpath)?, + _ => return Err(type_error("unsupported type")), + }, + None => { + let old_meta = std::fs::metadata(&oldpath); + match old_meta { + Ok(metadata) => { + if metadata.is_file() { + symlink_file(&oldpath, &newpath)? + } else if metadata.is_dir() { + symlink_dir(&oldpath, &newpath)? + } + } + Err(_) => return Err(type_error("you must pass a `options` argument for non-existent target path in windows".to_string())), + } + } + }; + Ok(json!({})) + } + }) + .await + .unwrap() +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ReadLinkArgs { + path: String, +} + +fn op_read_link_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: ReadLinkArgs = serde_json::from_value(args)?; + let path = PathBuf::from(&args.path); + + state.borrow::().check_read(&path)?; + + debug!("op_read_link_value {}", path.display()); + let target = std::fs::read_link(&path)?.into_os_string(); + let targetstr = into_string(target)?; + Ok(json!(targetstr)) +} + +async fn op_read_link_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: ReadLinkArgs = serde_json::from_value(args)?; + let path = PathBuf::from(&args.path); + { + let state = state.borrow(); + state.borrow::().check_read(&path)?; + } + tokio::task::spawn_blocking(move || { + debug!("op_read_link_async {}", path.display()); + let target = std::fs::read_link(&path)?.into_os_string(); + let targetstr = into_string(target)?; + Ok(json!(targetstr)) + }) + .await + .unwrap() +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct FtruncateArgs { + rid: i32, + len: i32, +} + +fn op_ftruncate_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.ftruncate"); + let args: FtruncateArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + let len = args.len as u64; + std_file_resource(state, rid, |r| match r { + Ok(std_file) => std_file.set_len(len).map_err(AnyError::from), + Err(_) => Err(type_error("cannot truncate this type of resource")), + })?; + Ok(json!({})) +} + +async fn op_ftruncate_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + super::check_unstable2(&state, "Deno.ftruncate"); + let args: FtruncateArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + let len = args.len as u64; + std_file_resource(&mut state.borrow_mut(), rid, |r| match r { + Ok(std_file) => std_file.set_len(len).map_err(AnyError::from), + Err(_) => Err(type_error("cannot truncate this type of resource")), + })?; + Ok(json!({})) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TruncateArgs { + path: String, + len: u64, +} + +fn op_truncate_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: TruncateArgs = serde_json::from_value(args)?; + let path = PathBuf::from(&args.path); + let len = args.len; + + state.borrow::().check_write(&path)?; + + debug!("op_truncate_sync {} {}", path.display(), len); + let f = std::fs::OpenOptions::new().write(true).open(&path)?; + f.set_len(len)?; + Ok(json!({})) +} + +async fn op_truncate_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: TruncateArgs = serde_json::from_value(args)?; + let path = PathBuf::from(&args.path); + let len = args.len; + { + let state = state.borrow(); + state.borrow::().check_write(&path)?; + } + tokio::task::spawn_blocking(move || { + debug!("op_truncate_async {} {}", path.display(), len); + let f = std::fs::OpenOptions::new().write(true).open(&path)?; + f.set_len(len)?; + Ok(json!({})) + }) + .await + .unwrap() +} + +fn make_temp( + dir: Option<&Path>, + prefix: Option<&str>, + suffix: Option<&str>, + is_dir: bool, +) -> std::io::Result { + let prefix_ = prefix.unwrap_or(""); + let suffix_ = suffix.unwrap_or(""); + let mut buf: PathBuf = match dir { + Some(ref p) => p.to_path_buf(), + None => temp_dir(), + } + .join("_"); + let mut rng = thread_rng(); + loop { + let unique = rng.gen::(); + buf.set_file_name(format!("{}{:08x}{}", prefix_, unique, suffix_)); + let r = if is_dir { + #[allow(unused_mut)] + let mut builder = std::fs::DirBuilder::new(); + #[cfg(unix)] + { + use std::os::unix::fs::DirBuilderExt; + builder.mode(0o700); + } + builder.create(buf.as_path()) + } else { + let mut open_options = std::fs::OpenOptions::new(); + open_options.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + open_options.mode(0o600); + } + open_options.open(buf.as_path())?; + Ok(()) + }; + match r { + Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue, + Ok(_) => return Ok(buf), + Err(e) => return Err(e), + } + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct MakeTempArgs { + dir: Option, + prefix: Option, + suffix: Option, +} + +fn op_make_temp_dir_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: MakeTempArgs = serde_json::from_value(args)?; + + let dir = args.dir.map(|s| PathBuf::from(&s)); + let prefix = args.prefix.map(String::from); + let suffix = args.suffix.map(String::from); + + state + .borrow::() + .check_write(dir.clone().unwrap_or_else(temp_dir).as_path())?; + + // TODO(piscisaureus): use byte vector for paths, not a string. + // See https://github.com/denoland/deno/issues/627. + // We can't assume that paths are always valid utf8 strings. + let path = make_temp( + // Converting Option to Option<&str> + dir.as_deref(), + prefix.as_deref(), + suffix.as_deref(), + true, + )?; + let path_str = into_string(path.into_os_string())?; + + Ok(json!(path_str)) +} + +async fn op_make_temp_dir_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: MakeTempArgs = serde_json::from_value(args)?; + + let dir = args.dir.map(|s| PathBuf::from(&s)); + let prefix = args.prefix.map(String::from); + let suffix = args.suffix.map(String::from); + { + let state = state.borrow(); + state + .borrow::() + .check_write(dir.clone().unwrap_or_else(temp_dir).as_path())?; + } + tokio::task::spawn_blocking(move || { + // TODO(piscisaureus): use byte vector for paths, not a string. + // See https://github.com/denoland/deno/issues/627. + // We can't assume that paths are always valid utf8 strings. + let path = make_temp( + // Converting Option to Option<&str> + dir.as_deref(), + prefix.as_deref(), + suffix.as_deref(), + true, + )?; + let path_str = into_string(path.into_os_string())?; + + Ok(json!(path_str)) + }) + .await + .unwrap() +} + +fn op_make_temp_file_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: MakeTempArgs = serde_json::from_value(args)?; + + let dir = args.dir.map(|s| PathBuf::from(&s)); + let prefix = args.prefix.map(String::from); + let suffix = args.suffix.map(String::from); + + state + .borrow::() + .check_write(dir.clone().unwrap_or_else(temp_dir).as_path())?; + + // TODO(piscisaureus): use byte vector for paths, not a string. + // See https://github.com/denoland/deno/issues/627. + // We can't assume that paths are always valid utf8 strings. + let path = make_temp( + // Converting Option to Option<&str> + dir.as_deref(), + prefix.as_deref(), + suffix.as_deref(), + false, + )?; + let path_str = into_string(path.into_os_string())?; + + Ok(json!(path_str)) +} + +async fn op_make_temp_file_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: MakeTempArgs = serde_json::from_value(args)?; + + let dir = args.dir.map(|s| PathBuf::from(&s)); + let prefix = args.prefix.map(String::from); + let suffix = args.suffix.map(String::from); + { + let state = state.borrow(); + state + .borrow::() + .check_write(dir.clone().unwrap_or_else(temp_dir).as_path())?; + } + tokio::task::spawn_blocking(move || { + // TODO(piscisaureus): use byte vector for paths, not a string. + // See https://github.com/denoland/deno/issues/627. + // We can't assume that paths are always valid utf8 strings. + let path = make_temp( + // Converting Option to Option<&str> + dir.as_deref(), + prefix.as_deref(), + suffix.as_deref(), + false, + )?; + let path_str = into_string(path.into_os_string())?; + + Ok(json!(path_str)) + }) + .await + .unwrap() +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct FutimeArgs { + rid: i32, + atime: (i64, u32), + mtime: (i64, u32), +} + +fn op_futime_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.futimeSync"); + let args: FutimeArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + let atime = filetime::FileTime::from_unix_time(args.atime.0, args.atime.1); + let mtime = filetime::FileTime::from_unix_time(args.mtime.0, args.mtime.1); + + std_file_resource(state, rid, |r| match r { + Ok(std_file) => { + filetime::set_file_handle_times(std_file, Some(atime), Some(mtime)) + .map_err(AnyError::from) + } + Err(_) => Err(type_error( + "cannot futime on this type of resource".to_string(), + )), + })?; + + Ok(json!({})) +} + +async fn op_futime_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let mut state = state.borrow_mut(); + super::check_unstable(&state, "Deno.futime"); + let args: FutimeArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + let atime = filetime::FileTime::from_unix_time(args.atime.0, args.atime.1); + let mtime = filetime::FileTime::from_unix_time(args.mtime.0, args.mtime.1); + // TODO Not actually async! https://github.com/denoland/deno/issues/7400 + std_file_resource(&mut state, rid, |r| match r { + Ok(std_file) => { + filetime::set_file_handle_times(std_file, Some(atime), Some(mtime)) + .map_err(AnyError::from) + } + Err(_) => Err(type_error( + "cannot futime on this type of resource".to_string(), + )), + })?; + + Ok(json!({})) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct UtimeArgs { + path: String, + atime: (i64, u32), + mtime: (i64, u32), +} + +fn op_utime_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.utime"); + + let args: UtimeArgs = serde_json::from_value(args)?; + let path = PathBuf::from(&args.path); + let atime = filetime::FileTime::from_unix_time(args.atime.0, args.atime.1); + let mtime = filetime::FileTime::from_unix_time(args.mtime.0, args.mtime.1); + + state.borrow::().check_write(&path)?; + filetime::set_file_times(path, atime, mtime)?; + Ok(json!({})) +} + +async fn op_utime_async( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let state = state.borrow(); + super::check_unstable(&state, "Deno.utime"); + + let args: UtimeArgs = serde_json::from_value(args)?; + let path = PathBuf::from(&args.path); + let atime = filetime::FileTime::from_unix_time(args.atime.0, args.atime.1); + let mtime = filetime::FileTime::from_unix_time(args.mtime.0, args.mtime.1); + + state.borrow::().check_write(&path)?; + + tokio::task::spawn_blocking(move || { + filetime::set_file_times(path, atime, mtime)?; + Ok(json!({})) + }) + .await + .unwrap() +} + +fn op_cwd( + state: &mut OpState, + _args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let path = current_dir()?; + state + .borrow::() + .check_read_blind(&path, "CWD")?; + let path_str = into_string(path.into_os_string())?; + Ok(json!(path_str)) +} diff --git a/runtime/ops/fs_events.rs b/runtime/ops/fs_events.rs new file mode 100644 index 000000000..4832c915c --- /dev/null +++ b/runtime/ops/fs_events.rs @@ -0,0 +1,133 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::permissions::Permissions; +use deno_core::error::bad_resource_id; +use deno_core::error::AnyError; +use deno_core::futures::future::poll_fn; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::BufVec; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; +use notify::event::Event as NotifyEvent; +use notify::Error as NotifyError; +use notify::EventKind; +use notify::RecommendedWatcher; +use notify::RecursiveMode; +use notify::Watcher; +use serde::Deserialize; +use serde::Serialize; +use std::cell::RefCell; +use std::convert::From; +use std::path::PathBuf; +use std::rc::Rc; +use tokio::sync::mpsc; + +pub fn init(rt: &mut deno_core::JsRuntime) { + super::reg_json_sync(rt, "op_fs_events_open", op_fs_events_open); + super::reg_json_async(rt, "op_fs_events_poll", op_fs_events_poll); +} + +struct FsEventsResource { + #[allow(unused)] + watcher: RecommendedWatcher, + receiver: mpsc::Receiver>, +} + +/// Represents a file system event. +/// +/// We do not use the event directly from the notify crate. We flatten +/// the structure into this simpler structure. We want to only make it more +/// complex as needed. +/// +/// Feel free to expand this struct as long as you can add tests to demonstrate +/// the complexity. +#[derive(Serialize, Debug)] +struct FsEvent { + kind: String, + paths: Vec, +} + +impl From for FsEvent { + fn from(e: NotifyEvent) -> Self { + let kind = match e.kind { + EventKind::Any => "any", + EventKind::Access(_) => "access", + EventKind::Create(_) => "create", + EventKind::Modify(_) => "modify", + EventKind::Remove(_) => "remove", + EventKind::Other => todo!(), // What's this for? Leaving it out for now. + } + .to_string(); + FsEvent { + kind, + paths: e.paths, + } + } +} + +fn op_fs_events_open( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + #[derive(Deserialize)] + struct OpenArgs { + recursive: bool, + paths: Vec, + } + let args: OpenArgs = serde_json::from_value(args)?; + let (sender, receiver) = mpsc::channel::>(16); + let sender = std::sync::Mutex::new(sender); + let mut watcher: RecommendedWatcher = + Watcher::new_immediate(move |res: Result| { + let res2 = res.map(FsEvent::from).map_err(AnyError::from); + let mut sender = sender.lock().unwrap(); + // Ignore result, if send failed it means that watcher was already closed, + // but not all messages have been flushed. + let _ = sender.try_send(res2); + })?; + let recursive_mode = if args.recursive { + RecursiveMode::Recursive + } else { + RecursiveMode::NonRecursive + }; + for path in &args.paths { + state + .borrow::() + .check_read(&PathBuf::from(path))?; + watcher.watch(path, recursive_mode)?; + } + let resource = FsEventsResource { watcher, receiver }; + let rid = state.resource_table.add("fsEvents", Box::new(resource)); + Ok(json!(rid)) +} + +async fn op_fs_events_poll( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + #[derive(Deserialize)] + struct PollArgs { + rid: u32, + } + let PollArgs { rid } = serde_json::from_value(args)?; + poll_fn(move |cx| { + let mut state = state.borrow_mut(); + let watcher = state + .resource_table + .get_mut::(rid) + .ok_or_else(bad_resource_id)?; + watcher + .receiver + .poll_recv(cx) + .map(|maybe_result| match maybe_result { + Some(Ok(value)) => Ok(json!({ "value": value, "done": false })), + Some(Err(err)) => Err(err), + None => Ok(json!({ "done": true })), + }) + }) + .await +} diff --git a/runtime/ops/io.rs b/runtime/ops/io.rs new file mode 100644 index 000000000..0f8af905a --- /dev/null +++ b/runtime/ops/io.rs @@ -0,0 +1,473 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use super::dispatch_minimal::minimal_op; +use super::dispatch_minimal::MinimalOp; +use crate::metrics::metrics_op; +use deno_core::error::bad_resource_id; +use deno_core::error::resource_unavailable; +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::futures; +use deno_core::futures::future::poll_fn; +use deno_core::futures::future::FutureExt; +use deno_core::futures::ready; +use deno_core::BufVec; +use deno_core::JsRuntime; +use deno_core::OpState; +use std::cell::RefCell; +use std::collections::HashMap; +use std::pin::Pin; +use std::rc::Rc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::task::Context; +use std::task::Poll; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::net::TcpStream; +use tokio_rustls::client::TlsStream as ClientTlsStream; +use tokio_rustls::server::TlsStream as ServerTlsStream; + +#[cfg(not(windows))] +use std::os::unix::io::FromRawFd; + +#[cfg(windows)] +use std::os::windows::io::FromRawHandle; + +lazy_static! { + /// Due to portability issues on Windows handle to stdout is created from raw + /// file descriptor. The caveat of that approach is fact that when this + /// handle is dropped underlying file descriptor is closed - that is highly + /// not desirable in case of stdout. That's why we store this global handle + /// that is then cloned when obtaining stdio for process. In turn when + /// resource table is dropped storing reference to that handle, the handle + /// itself won't be closed (so Deno.core.print) will still work. + // TODO(ry) It should be possible to close stdout. + static ref STDIN_HANDLE: Option = { + #[cfg(not(windows))] + let stdin = unsafe { Some(std::fs::File::from_raw_fd(0)) }; + #[cfg(windows)] + let stdin = unsafe { + let handle = winapi::um::processenv::GetStdHandle( + winapi::um::winbase::STD_INPUT_HANDLE, + ); + if handle.is_null() { + return None; + } + Some(std::fs::File::from_raw_handle(handle)) + }; + stdin + }; + static ref STDOUT_HANDLE: Option = { + #[cfg(not(windows))] + let stdout = unsafe { Some(std::fs::File::from_raw_fd(1)) }; + #[cfg(windows)] + let stdout = unsafe { + let handle = winapi::um::processenv::GetStdHandle( + winapi::um::winbase::STD_OUTPUT_HANDLE, + ); + if handle.is_null() { + return None; + } + Some(std::fs::File::from_raw_handle(handle)) + }; + stdout + }; + static ref STDERR_HANDLE: Option = { + #[cfg(not(windows))] + let stderr = unsafe { Some(std::fs::File::from_raw_fd(2)) }; + #[cfg(windows)] + let stderr = unsafe { + let handle = winapi::um::processenv::GetStdHandle( + winapi::um::winbase::STD_ERROR_HANDLE, + ); + if handle.is_null() { + return None; + } + Some(std::fs::File::from_raw_handle(handle)) + }; + stderr + }; +} + +pub fn init(rt: &mut JsRuntime) { + rt.register_op("op_read", metrics_op(minimal_op(op_read))); + rt.register_op("op_write", metrics_op(minimal_op(op_write))); +} + +pub fn get_stdio() -> ( + Option, + Option, + Option, +) { + let stdin = get_stdio_stream(&STDIN_HANDLE); + let stdout = get_stdio_stream(&STDOUT_HANDLE); + let stderr = get_stdio_stream(&STDERR_HANDLE); + + (stdin, stdout, stderr) +} + +fn get_stdio_stream( + handle: &Option, +) -> Option { + match handle { + None => None, + Some(file_handle) => match file_handle.try_clone() { + Ok(clone) => Some(StreamResourceHolder::new(StreamResource::FsFile( + Some((tokio::fs::File::from_std(clone), FileMetadata::default())), + ))), + Err(_e) => None, + }, + } +} + +fn no_buffer_specified() -> AnyError { + type_error("no buffer specified") +} + +#[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 struct StreamResourceHolder { + pub resource: StreamResource, + waker: HashMap, + waker_counter: AtomicUsize, +} + +impl StreamResourceHolder { + pub fn new(resource: StreamResource) -> StreamResourceHolder { + StreamResourceHolder { + resource, + // Atleast one task is expecter for the resource + waker: HashMap::with_capacity(1), + // Tracks wakers Ids + waker_counter: AtomicUsize::new(0), + } + } +} + +impl Drop for StreamResourceHolder { + fn drop(&mut self) { + self.wake_tasks(); + } +} + +impl StreamResourceHolder { + pub fn track_task(&mut self, cx: &Context) -> Result { + let waker = futures::task::AtomicWaker::new(); + waker.register(cx.waker()); + // Its OK if it overflows + let task_waker_id = self.waker_counter.fetch_add(1, Ordering::Relaxed); + self.waker.insert(task_waker_id, waker); + Ok(task_waker_id) + } + + pub fn wake_tasks(&mut self) { + for waker in self.waker.values() { + waker.wake(); + } + } + + pub fn untrack_task(&mut self, task_waker_id: usize) { + self.waker.remove(&task_waker_id); + } +} + +pub enum StreamResource { + FsFile(Option<(tokio::fs::File, FileMetadata)>), + TcpStream(Option), + #[cfg(not(windows))] + UnixStream(tokio::net::UnixStream), + ServerTlsStream(Box>), + ClientTlsStream(Box>), + ChildStdin(tokio::process::ChildStdin), + ChildStdout(tokio::process::ChildStdout), + ChildStderr(tokio::process::ChildStderr), +} + +trait UnpinAsyncRead: AsyncRead + Unpin {} +trait UnpinAsyncWrite: AsyncWrite + Unpin {} + +impl UnpinAsyncRead for T {} +impl UnpinAsyncWrite for T {} + +/// `DenoAsyncRead` is the same as the `tokio_io::AsyncRead` trait +/// but uses an `AnyError` error instead of `std::io:Error` +pub trait DenoAsyncRead { + fn poll_read( + &mut self, + cx: &mut Context, + buf: &mut [u8], + ) -> Poll>; +} + +impl DenoAsyncRead for StreamResource { + fn poll_read( + &mut self, + cx: &mut Context, + buf: &mut [u8], + ) -> Poll> { + use StreamResource::*; + let f: &mut dyn UnpinAsyncRead = match self { + FsFile(Some((f, _))) => f, + FsFile(None) => return Poll::Ready(Err(resource_unavailable())), + TcpStream(Some(f)) => f, + #[cfg(not(windows))] + UnixStream(f) => f, + ClientTlsStream(f) => f, + ServerTlsStream(f) => f, + ChildStdout(f) => f, + ChildStderr(f) => f, + _ => return Err(bad_resource_id()).into(), + }; + let v = ready!(Pin::new(f).poll_read(cx, buf))?; + Ok(v).into() + } +} + +pub fn op_read( + state: Rc>, + is_sync: bool, + rid: i32, + mut zero_copy: BufVec, +) -> MinimalOp { + debug!("read rid={}", rid); + match zero_copy.len() { + 0 => return MinimalOp::Sync(Err(no_buffer_specified())), + 1 => {} + _ => panic!("Invalid number of arguments"), + } + + if is_sync { + MinimalOp::Sync({ + // First we look up the rid in the resource table. + std_file_resource(&mut state.borrow_mut(), rid as u32, move |r| match r { + Ok(std_file) => { + use std::io::Read; + std_file + .read(&mut zero_copy[0]) + .map(|n: usize| n as i32) + .map_err(AnyError::from) + } + Err(_) => Err(type_error("sync read not allowed on this resource")), + }) + }) + } else { + let mut zero_copy = zero_copy[0].clone(); + MinimalOp::Async( + poll_fn(move |cx| { + let mut state = state.borrow_mut(); + let resource_holder = state + .resource_table + .get_mut::(rid as u32) + .ok_or_else(bad_resource_id)?; + + let mut task_tracker_id: Option = None; + let nread = match resource_holder.resource.poll_read(cx, &mut zero_copy) + { + Poll::Ready(t) => { + if let Some(id) = task_tracker_id { + resource_holder.untrack_task(id); + } + t + } + Poll::Pending => { + task_tracker_id.replace(resource_holder.track_task(cx)?); + return Poll::Pending; + } + }?; + Poll::Ready(Ok(nread as i32)) + }) + .boxed_local(), + ) + } +} + +/// `DenoAsyncWrite` is the same as the `tokio_io::AsyncWrite` trait +/// but uses an `AnyError` error instead of `std::io:Error` +pub trait DenoAsyncWrite { + fn poll_write( + &mut self, + cx: &mut Context, + buf: &[u8], + ) -> Poll>; + + fn poll_close(&mut self, cx: &mut Context) -> Poll>; + + fn poll_flush(&mut self, cx: &mut Context) -> Poll>; +} + +impl DenoAsyncWrite for StreamResource { + fn poll_write( + &mut self, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + use StreamResource::*; + let f: &mut dyn UnpinAsyncWrite = match self { + FsFile(Some((f, _))) => f, + FsFile(None) => return Poll::Pending, + TcpStream(Some(f)) => f, + #[cfg(not(windows))] + UnixStream(f) => f, + ClientTlsStream(f) => f, + ServerTlsStream(f) => f, + ChildStdin(f) => f, + _ => return Err(bad_resource_id()).into(), + }; + + let v = ready!(Pin::new(f).poll_write(cx, buf))?; + Ok(v).into() + } + + fn poll_flush(&mut self, cx: &mut Context) -> Poll> { + use StreamResource::*; + let f: &mut dyn UnpinAsyncWrite = match self { + FsFile(Some((f, _))) => f, + FsFile(None) => return Poll::Pending, + TcpStream(Some(f)) => f, + #[cfg(not(windows))] + UnixStream(f) => f, + ClientTlsStream(f) => f, + ServerTlsStream(f) => f, + ChildStdin(f) => f, + _ => return Err(bad_resource_id()).into(), + }; + + ready!(Pin::new(f).poll_flush(cx))?; + Ok(()).into() + } + + fn poll_close(&mut self, _cx: &mut Context) -> Poll> { + unimplemented!() + } +} + +pub fn op_write( + state: Rc>, + is_sync: bool, + rid: i32, + zero_copy: BufVec, +) -> MinimalOp { + debug!("write rid={}", rid); + match zero_copy.len() { + 0 => return MinimalOp::Sync(Err(no_buffer_specified())), + 1 => {} + _ => panic!("Invalid number of arguments"), + } + + if is_sync { + MinimalOp::Sync({ + // First we look up the rid in the resource table. + std_file_resource(&mut state.borrow_mut(), rid as u32, move |r| match r { + Ok(std_file) => { + use std::io::Write; + std_file + .write(&zero_copy[0]) + .map(|nwritten: usize| nwritten as i32) + .map_err(AnyError::from) + } + Err(_) => Err(type_error("sync read not allowed on this resource")), + }) + }) + } else { + let zero_copy = zero_copy[0].clone(); + MinimalOp::Async( + async move { + let nwritten = poll_fn(|cx| { + let mut state = state.borrow_mut(); + let resource_holder = state + .resource_table + .get_mut::(rid as u32) + .ok_or_else(bad_resource_id)?; + resource_holder.resource.poll_write(cx, &zero_copy) + }) + .await?; + + // TODO(bartlomieju): this step was added during upgrade to Tokio 0.2 + // and the reasons for the need to explicitly flush are not fully known. + // Figure out why it's needed and preferably remove it. + // https://github.com/denoland/deno/issues/3565 + poll_fn(|cx| { + let mut state = state.borrow_mut(); + let resource_holder = state + .resource_table + .get_mut::(rid as u32) + .ok_or_else(bad_resource_id)?; + resource_holder.resource.poll_flush(cx) + }) + .await?; + + Ok(nwritten as i32) + } + .boxed_local(), + ) + } +} + +/// Helper function for operating on a std::fs::File stored in the resource table. +/// +/// We store file system file resources as tokio::fs::File, so this is a little +/// utility function that gets a std::fs:File when you need to do blocking +/// operations. +/// +/// Returns ErrorKind::Busy if the resource is being used by another op. +pub fn std_file_resource( + state: &mut OpState, + rid: u32, + mut f: F, +) -> Result +where + F: FnMut( + Result<&mut std::fs::File, &mut StreamResource>, + ) -> Result, +{ + // First we look up the rid in the resource table. + let mut r = state.resource_table.get_mut::(rid); + if let Some(ref mut resource_holder) = r { + // Sync write only works for FsFile. It doesn't make sense to do this + // for non-blocking sockets. So we error out if not FsFile. + match &mut resource_holder.resource { + StreamResource::FsFile(option_file_metadata) => { + // The object in the resource table is a tokio::fs::File - but in + // order to do a blocking write on it, we must turn it into a + // std::fs::File. Hopefully this code compiles down to nothing. + if let Some((tokio_file, metadata)) = option_file_metadata.take() { + match tokio_file.try_into_std() { + Ok(mut std_file) => { + let result = f(Ok(&mut std_file)); + // Turn the std_file handle back into a tokio file, put it back + // in the resource table. + let tokio_file = tokio::fs::File::from_std(std_file); + resource_holder.resource = + StreamResource::FsFile(Some((tokio_file, metadata))); + // return the result. + result + } + Err(tokio_file) => { + // This function will return an error containing the file if + // some operation is in-flight. + resource_holder.resource = + StreamResource::FsFile(Some((tokio_file, metadata))); + Err(resource_unavailable()) + } + } + } else { + Err(resource_unavailable()) + } + } + _ => f(Err(&mut resource_holder.resource)), + } + } else { + Err(bad_resource_id()) + } +} diff --git a/runtime/ops/mod.rs b/runtime/ops/mod.rs new file mode 100644 index 000000000..a27122657 --- /dev/null +++ b/runtime/ops/mod.rs @@ -0,0 +1,89 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +mod dispatch_minimal; +pub use dispatch_minimal::MinimalOp; + +pub mod crypto; +pub mod fetch; +pub mod fs; +pub mod fs_events; +pub mod io; +pub mod net; +#[cfg(unix)] +mod net_unix; +pub mod os; +pub mod permissions; +pub mod plugin; +pub mod process; +pub mod runtime; +pub mod signal; +pub mod timers; +pub mod tls; +pub mod tty; +pub mod web_worker; +pub mod websocket; +pub mod worker_host; + +use crate::metrics::metrics_op; +use deno_core::error::AnyError; +use deno_core::json_op_async; +use deno_core::json_op_sync; +use deno_core::serde_json::Value; +use deno_core::BufVec; +use deno_core::JsRuntime; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; +use std::cell::RefCell; +use std::future::Future; +use std::rc::Rc; + +pub fn reg_json_async(rt: &mut JsRuntime, name: &'static str, op_fn: F) +where + F: Fn(Rc>, Value, BufVec) -> R + 'static, + R: Future> + 'static, +{ + rt.register_op(name, metrics_op(json_op_async(op_fn))); +} + +pub fn reg_json_sync(rt: &mut JsRuntime, name: &'static str, op_fn: F) +where + F: Fn(&mut OpState, Value, &mut [ZeroCopyBuf]) -> Result + + 'static, +{ + rt.register_op(name, metrics_op(json_op_sync(op_fn))); +} + +/// `UnstableChecker` is a struct so it can be placed inside `GothamState`; +/// using type alias for a bool could work, but there's a high chance +/// that there might be another type alias pointing to a bool, which +/// would override previously used alias. +pub struct UnstableChecker { + pub unstable: bool, +} + +impl UnstableChecker { + /// Quits the process if the --unstable flag was not provided. + /// + /// This is intentionally a non-recoverable check so that people cannot probe + /// for unstable APIs from stable programs. + // NOTE(bartlomieju): keep in sync with `cli/program_state.rs` + pub fn check_unstable(&self, api_name: &str) { + if !self.unstable { + eprintln!( + "Unstable API '{}'. The --unstable flag must be provided.", + api_name + ); + std::process::exit(70); + } + } +} +/// Helper for checking unstable features. Used for sync ops. +pub fn check_unstable(state: &OpState, api_name: &str) { + state.borrow::().check_unstable(api_name) +} + +/// Helper for checking unstable features. Used for async ops. +pub fn check_unstable2(state: &Rc>, api_name: &str) { + let state = state.borrow(); + state.borrow::().check_unstable(api_name) +} diff --git a/runtime/ops/net.rs b/runtime/ops/net.rs new file mode 100644 index 000000000..98ff83fc0 --- /dev/null +++ b/runtime/ops/net.rs @@ -0,0 +1,566 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::ops::io::StreamResource; +use crate::ops::io::StreamResourceHolder; +use crate::permissions::Permissions; +use crate::resolve_addr::resolve_addr; +use deno_core::error::bad_resource; +use deno_core::error::bad_resource_id; +use deno_core::error::custom_error; +use deno_core::error::generic_error; +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::futures; +use deno_core::futures::future::poll_fn; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::BufVec; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; +use serde::Deserialize; +use std::cell::RefCell; +use std::net::Shutdown; +use std::net::SocketAddr; +use std::rc::Rc; +use std::task::Context; +use std::task::Poll; +use tokio::net::TcpListener; +use tokio::net::TcpStream; +use tokio::net::UdpSocket; + +#[cfg(unix)] +use super::net_unix; +#[cfg(unix)] +use std::path::Path; + +pub fn init(rt: &mut deno_core::JsRuntime) { + super::reg_json_async(rt, "op_accept", op_accept); + super::reg_json_async(rt, "op_connect", op_connect); + super::reg_json_sync(rt, "op_shutdown", op_shutdown); + super::reg_json_sync(rt, "op_listen", op_listen); + super::reg_json_async(rt, "op_datagram_receive", op_datagram_receive); + super::reg_json_async(rt, "op_datagram_send", op_datagram_send); +} + +#[derive(Deserialize)] +pub(crate) struct AcceptArgs { + pub rid: i32, + pub transport: String, +} + +async fn accept_tcp( + state: Rc>, + args: AcceptArgs, + _zero_copy: BufVec, +) -> Result { + let rid = args.rid as u32; + + let accept_fut = poll_fn(|cx| { + let mut state = state.borrow_mut(); + let listener_resource = state + .resource_table + .get_mut::(rid) + .ok_or_else(|| bad_resource("Listener has been closed"))?; + let listener = &mut listener_resource.listener; + match listener.poll_accept(cx).map_err(AnyError::from) { + Poll::Ready(Ok((stream, addr))) => { + listener_resource.untrack_task(); + Poll::Ready(Ok((stream, addr))) + } + Poll::Pending => { + listener_resource.track_task(cx)?; + Poll::Pending + } + Poll::Ready(Err(e)) => { + listener_resource.untrack_task(); + Poll::Ready(Err(e)) + } + } + }); + let (tcp_stream, _socket_addr) = accept_fut.await?; + let local_addr = tcp_stream.local_addr()?; + let remote_addr = tcp_stream.peer_addr()?; + + let mut state = state.borrow_mut(); + let rid = state.resource_table.add( + "tcpStream", + Box::new(StreamResourceHolder::new(StreamResource::TcpStream(Some( + tcp_stream, + )))), + ); + Ok(json!({ + "rid": rid, + "localAddr": { + "hostname": local_addr.ip().to_string(), + "port": local_addr.port(), + "transport": "tcp", + }, + "remoteAddr": { + "hostname": remote_addr.ip().to_string(), + "port": remote_addr.port(), + "transport": "tcp", + } + })) +} + +async fn op_accept( + state: Rc>, + args: Value, + bufs: BufVec, +) -> Result { + let args: AcceptArgs = serde_json::from_value(args)?; + match args.transport.as_str() { + "tcp" => accept_tcp(state, args, bufs).await, + #[cfg(unix)] + "unix" => net_unix::accept_unix(state, args, bufs).await, + _ => Err(generic_error(format!( + "Unsupported transport protocol {}", + args.transport + ))), + } +} + +#[derive(Deserialize)] +pub(crate) struct ReceiveArgs { + pub rid: i32, + pub transport: String, +} + +async fn receive_udp( + state: Rc>, + args: ReceiveArgs, + zero_copy: BufVec, +) -> Result { + assert_eq!(zero_copy.len(), 1, "Invalid number of arguments"); + let mut zero_copy = zero_copy[0].clone(); + + let rid = args.rid as u32; + + let receive_fut = poll_fn(|cx| { + let mut state = state.borrow_mut(); + let resource = state + .resource_table + .get_mut::(rid) + .ok_or_else(|| bad_resource("Socket has been closed"))?; + let socket = &mut resource.socket; + socket + .poll_recv_from(cx, &mut zero_copy) + .map_err(AnyError::from) + }); + let (size, remote_addr) = receive_fut.await?; + Ok(json!({ + "size": size, + "remoteAddr": { + "hostname": remote_addr.ip().to_string(), + "port": remote_addr.port(), + "transport": "udp", + } + })) +} + +async fn op_datagram_receive( + state: Rc>, + args: Value, + zero_copy: BufVec, +) -> Result { + assert_eq!(zero_copy.len(), 1, "Invalid number of arguments"); + + let args: ReceiveArgs = serde_json::from_value(args)?; + match args.transport.as_str() { + "udp" => receive_udp(state, args, zero_copy).await, + #[cfg(unix)] + "unixpacket" => net_unix::receive_unix_packet(state, args, zero_copy).await, + _ => Err(generic_error(format!( + "Unsupported transport protocol {}", + args.transport + ))), + } +} + +#[derive(Deserialize)] +struct SendArgs { + rid: i32, + transport: String, + #[serde(flatten)] + transport_args: ArgsEnum, +} + +async fn op_datagram_send( + state: Rc>, + args: Value, + zero_copy: BufVec, +) -> Result { + assert_eq!(zero_copy.len(), 1, "Invalid number of arguments"); + let zero_copy = zero_copy[0].clone(); + + match serde_json::from_value(args)? { + SendArgs { + rid, + transport, + transport_args: ArgsEnum::Ip(args), + } if transport == "udp" => { + { + let s = state.borrow(); + s.borrow::() + .check_net(&args.hostname, args.port)?; + } + let addr = resolve_addr(&args.hostname, args.port)?; + poll_fn(move |cx| { + let mut state = state.borrow_mut(); + let resource = state + .resource_table + .get_mut::(rid as u32) + .ok_or_else(|| bad_resource("Socket has been closed"))?; + resource + .socket + .poll_send_to(cx, &zero_copy, &addr) + .map_ok(|byte_length| json!(byte_length)) + .map_err(AnyError::from) + }) + .await + } + #[cfg(unix)] + SendArgs { + rid, + transport, + transport_args: ArgsEnum::Unix(args), + } if transport == "unixpacket" => { + let address_path = Path::new(&args.path); + { + let s = state.borrow(); + s.borrow::().check_write(&address_path)?; + } + let mut state = state.borrow_mut(); + let resource = state + .resource_table + .get_mut::(rid as u32) + .ok_or_else(|| { + custom_error("NotConnected", "Socket has been closed") + })?; + let socket = &mut resource.socket; + let byte_length = socket + .send_to(&zero_copy, &resource.local_addr.as_pathname().unwrap()) + .await?; + + Ok(json!(byte_length)) + } + _ => Err(type_error("Wrong argument format!")), + } +} + +#[derive(Deserialize)] +struct ConnectArgs { + transport: String, + #[serde(flatten)] + transport_args: ArgsEnum, +} + +async fn op_connect( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + match serde_json::from_value(args)? { + ConnectArgs { + transport, + transport_args: ArgsEnum::Ip(args), + } if transport == "tcp" => { + { + let state_ = state.borrow(); + state_ + .borrow::() + .check_net(&args.hostname, args.port)?; + } + let addr = resolve_addr(&args.hostname, args.port)?; + let tcp_stream = TcpStream::connect(&addr).await?; + let local_addr = tcp_stream.local_addr()?; + let remote_addr = tcp_stream.peer_addr()?; + + let mut state_ = state.borrow_mut(); + let rid = state_.resource_table.add( + "tcpStream", + Box::new(StreamResourceHolder::new(StreamResource::TcpStream(Some( + tcp_stream, + )))), + ); + Ok(json!({ + "rid": rid, + "localAddr": { + "hostname": local_addr.ip().to_string(), + "port": local_addr.port(), + "transport": transport, + }, + "remoteAddr": { + "hostname": remote_addr.ip().to_string(), + "port": remote_addr.port(), + "transport": transport, + } + })) + } + #[cfg(unix)] + ConnectArgs { + transport, + transport_args: ArgsEnum::Unix(args), + } if transport == "unix" => { + let address_path = Path::new(&args.path); + super::check_unstable2(&state, "Deno.connect"); + { + let state_ = state.borrow(); + state_.borrow::().check_read(&address_path)?; + state_.borrow::().check_write(&address_path)?; + } + let path = args.path; + let unix_stream = net_unix::UnixStream::connect(Path::new(&path)).await?; + let local_addr = unix_stream.local_addr()?; + let remote_addr = unix_stream.peer_addr()?; + + let mut state_ = state.borrow_mut(); + let rid = state_.resource_table.add( + "unixStream", + Box::new(StreamResourceHolder::new(StreamResource::UnixStream( + unix_stream, + ))), + ); + Ok(json!({ + "rid": rid, + "localAddr": { + "path": local_addr.as_pathname(), + "transport": transport, + }, + "remoteAddr": { + "path": remote_addr.as_pathname(), + "transport": transport, + } + })) + } + _ => Err(type_error("Wrong argument format!")), + } +} + +#[derive(Deserialize)] +struct ShutdownArgs { + rid: i32, + how: i32, +} + +fn op_shutdown( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.shutdown"); + + let args: ShutdownArgs = serde_json::from_value(args)?; + + let rid = args.rid as u32; + let how = args.how; + + let shutdown_mode = match how { + 0 => Shutdown::Read, + 1 => Shutdown::Write, + _ => unimplemented!(), + }; + + let resource_holder = state + .resource_table + .get_mut::(rid) + .ok_or_else(bad_resource_id)?; + match resource_holder.resource { + StreamResource::TcpStream(Some(ref mut stream)) => { + TcpStream::shutdown(stream, shutdown_mode)?; + } + #[cfg(unix)] + StreamResource::UnixStream(ref mut stream) => { + net_unix::UnixStream::shutdown(stream, shutdown_mode)?; + } + _ => return Err(bad_resource_id()), + } + + Ok(json!({})) +} + +#[allow(dead_code)] +struct TcpListenerResource { + listener: TcpListener, + waker: Option, + local_addr: SocketAddr, +} + +impl Drop for TcpListenerResource { + fn drop(&mut self) { + self.wake_task(); + } +} + +impl TcpListenerResource { + /// Track the current task so future awaiting for connection + /// can be notified when listener is closed. + /// + /// Throws an error if another task is already tracked. + pub fn track_task(&mut self, cx: &Context) -> Result<(), AnyError> { + // Currently, we only allow tracking a single accept task for a listener. + // This might be changed in the future with multiple workers. + // Caveat: TcpListener by itself also only tracks an accept task at a time. + // See https://github.com/tokio-rs/tokio/issues/846#issuecomment-454208883 + if self.waker.is_some() { + return Err(custom_error("Busy", "Another accept task is ongoing")); + } + + let waker = futures::task::AtomicWaker::new(); + waker.register(cx.waker()); + self.waker.replace(waker); + Ok(()) + } + + /// Notifies a task when listener is closed so accept future can resolve. + pub fn wake_task(&mut self) { + if let Some(waker) = self.waker.as_ref() { + waker.wake(); + } + } + + /// Stop tracking a task. + /// Happens when the task is done and thus no further tracking is needed. + pub fn untrack_task(&mut self) { + if self.waker.is_some() { + self.waker.take(); + } + } +} + +struct UdpSocketResource { + socket: UdpSocket, +} + +#[derive(Deserialize)] +struct IpListenArgs { + hostname: String, + port: u16, +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum ArgsEnum { + Ip(IpListenArgs), + #[cfg(unix)] + Unix(net_unix::UnixListenArgs), +} + +#[derive(Deserialize)] +struct ListenArgs { + transport: String, + #[serde(flatten)] + transport_args: ArgsEnum, +} + +fn listen_tcp( + state: &mut OpState, + addr: SocketAddr, +) -> Result<(u32, SocketAddr), AnyError> { + let std_listener = std::net::TcpListener::bind(&addr)?; + let listener = TcpListener::from_std(std_listener)?; + let local_addr = listener.local_addr()?; + let listener_resource = TcpListenerResource { + listener, + waker: None, + local_addr, + }; + let rid = state + .resource_table + .add("tcpListener", Box::new(listener_resource)); + + Ok((rid, local_addr)) +} + +fn listen_udp( + state: &mut OpState, + addr: SocketAddr, +) -> Result<(u32, SocketAddr), AnyError> { + let std_socket = std::net::UdpSocket::bind(&addr)?; + let socket = UdpSocket::from_std(std_socket)?; + let local_addr = socket.local_addr()?; + let socket_resource = UdpSocketResource { socket }; + let rid = state + .resource_table + .add("udpSocket", Box::new(socket_resource)); + + Ok((rid, local_addr)) +} + +fn op_listen( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let permissions = state.borrow::(); + match serde_json::from_value(args)? { + ListenArgs { + transport, + transport_args: ArgsEnum::Ip(args), + } => { + { + if transport == "udp" { + super::check_unstable(state, "Deno.listenDatagram"); + } + permissions.check_net(&args.hostname, args.port)?; + } + let addr = resolve_addr(&args.hostname, args.port)?; + let (rid, local_addr) = if transport == "tcp" { + listen_tcp(state, addr)? + } else { + listen_udp(state, addr)? + }; + debug!( + "New listener {} {}:{}", + rid, + local_addr.ip().to_string(), + local_addr.port() + ); + Ok(json!({ + "rid": rid, + "localAddr": { + "hostname": local_addr.ip().to_string(), + "port": local_addr.port(), + "transport": transport, + }, + })) + } + #[cfg(unix)] + ListenArgs { + transport, + transport_args: ArgsEnum::Unix(args), + } if transport == "unix" || transport == "unixpacket" => { + let address_path = Path::new(&args.path); + { + if transport == "unix" { + super::check_unstable(state, "Deno.listen"); + } + if transport == "unixpacket" { + super::check_unstable(state, "Deno.listenDatagram"); + } + permissions.check_read(&address_path)?; + permissions.check_write(&address_path)?; + } + let (rid, local_addr) = if transport == "unix" { + net_unix::listen_unix(state, &address_path)? + } else { + net_unix::listen_unix_packet(state, &address_path)? + }; + debug!( + "New listener {} {}", + rid, + local_addr.as_pathname().unwrap().display(), + ); + Ok(json!({ + "rid": rid, + "localAddr": { + "path": local_addr.as_pathname(), + "transport": transport, + }, + })) + } + #[cfg(unix)] + _ => Err(type_error("Wrong argument format!")), + } +} diff --git a/runtime/ops/net_unix.rs b/runtime/ops/net_unix.rs new file mode 100644 index 000000000..4c416a5a4 --- /dev/null +++ b/runtime/ops/net_unix.rs @@ -0,0 +1,151 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::ops::io::StreamResource; +use crate::ops::io::StreamResourceHolder; +use crate::ops::net::AcceptArgs; +use crate::ops::net::ReceiveArgs; +use deno_core::error::bad_resource; +use deno_core::error::AnyError; +use deno_core::futures::future::poll_fn; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::BufVec; +use deno_core::OpState; +use serde::Deserialize; +use std::cell::RefCell; +use std::fs::remove_file; +use std::os::unix; +use std::path::Path; +use std::rc::Rc; +use std::task::Poll; +use tokio::net::UnixDatagram; +use tokio::net::UnixListener; +pub use tokio::net::UnixStream; + +struct UnixListenerResource { + listener: UnixListener, +} + +pub struct UnixDatagramResource { + pub socket: UnixDatagram, + pub local_addr: unix::net::SocketAddr, +} + +#[derive(Deserialize)] +pub struct UnixListenArgs { + pub path: String, +} + +pub(crate) async fn accept_unix( + state: Rc>, + args: AcceptArgs, + _bufs: BufVec, +) -> Result { + let rid = args.rid as u32; + + let accept_fut = poll_fn(|cx| { + let mut state = state.borrow_mut(); + let listener_resource = state + .resource_table + .get_mut::(rid) + .ok_or_else(|| bad_resource("Listener has been closed"))?; + let listener = &mut listener_resource.listener; + use deno_core::futures::StreamExt; + match listener.poll_next_unpin(cx) { + Poll::Ready(Some(stream)) => { + //listener_resource.untrack_task(); + Poll::Ready(stream) + } + Poll::Ready(None) => todo!(), + Poll::Pending => { + //listener_resource.track_task(cx)?; + Poll::Pending + } + } + .map_err(AnyError::from) + }); + let unix_stream = accept_fut.await?; + + let local_addr = unix_stream.local_addr()?; + let remote_addr = unix_stream.peer_addr()?; + let mut state = state.borrow_mut(); + let rid = state.resource_table.add( + "unixStream", + Box::new(StreamResourceHolder::new(StreamResource::UnixStream( + unix_stream, + ))), + ); + Ok(json!({ + "rid": rid, + "localAddr": { + "path": local_addr.as_pathname(), + "transport": "unix", + }, + "remoteAddr": { + "path": remote_addr.as_pathname(), + "transport": "unix", + } + })) +} + +pub(crate) async fn receive_unix_packet( + state: Rc>, + args: ReceiveArgs, + bufs: BufVec, +) -> Result { + assert_eq!(bufs.len(), 1, "Invalid number of arguments"); + + let rid = args.rid as u32; + let mut buf = bufs.into_iter().next().unwrap(); + + let mut state = state.borrow_mut(); + let resource = state + .resource_table + .get_mut::(rid) + .ok_or_else(|| bad_resource("Socket has been closed"))?; + let (size, remote_addr) = resource.socket.recv_from(&mut buf).await?; + Ok(json!({ + "size": size, + "remoteAddr": { + "path": remote_addr.as_pathname(), + "transport": "unixpacket", + } + })) +} + +pub fn listen_unix( + state: &mut OpState, + addr: &Path, +) -> Result<(u32, unix::net::SocketAddr), AnyError> { + if addr.exists() { + remove_file(&addr).unwrap(); + } + let listener = UnixListener::bind(&addr)?; + let local_addr = listener.local_addr()?; + let listener_resource = UnixListenerResource { listener }; + let rid = state + .resource_table + .add("unixListener", Box::new(listener_resource)); + + Ok((rid, local_addr)) +} + +pub fn listen_unix_packet( + state: &mut OpState, + addr: &Path, +) -> Result<(u32, unix::net::SocketAddr), AnyError> { + if addr.exists() { + remove_file(&addr).unwrap(); + } + let socket = UnixDatagram::bind(&addr)?; + let local_addr = socket.local_addr()?; + let datagram_resource = UnixDatagramResource { + socket, + local_addr: local_addr.clone(), + }; + let rid = state + .resource_table + .add("unixDatagram", Box::new(datagram_resource)); + + Ok((rid, local_addr)) +} diff --git a/runtime/ops/os.rs b/runtime/ops/os.rs new file mode 100644 index 000000000..6fd404a23 --- /dev/null +++ b/runtime/ops/os.rs @@ -0,0 +1,192 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::permissions::Permissions; +use deno_core::error::AnyError; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::url::Url; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; +use serde::Deserialize; +use std::collections::HashMap; +use std::env; + +pub fn init(rt: &mut deno_core::JsRuntime) { + super::reg_json_sync(rt, "op_exit", op_exit); + super::reg_json_sync(rt, "op_env", op_env); + super::reg_json_sync(rt, "op_exec_path", op_exec_path); + super::reg_json_sync(rt, "op_set_env", op_set_env); + super::reg_json_sync(rt, "op_get_env", op_get_env); + super::reg_json_sync(rt, "op_delete_env", op_delete_env); + super::reg_json_sync(rt, "op_hostname", op_hostname); + super::reg_json_sync(rt, "op_loadavg", op_loadavg); + super::reg_json_sync(rt, "op_os_release", op_os_release); + super::reg_json_sync(rt, "op_system_memory_info", op_system_memory_info); + super::reg_json_sync(rt, "op_system_cpu_info", op_system_cpu_info); +} + +fn op_exec_path( + state: &mut OpState, + _args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let current_exe = env::current_exe().unwrap(); + state + .borrow::() + .check_read_blind(¤t_exe, "exec_path")?; + // Now apply URL parser to current exe to get fully resolved path, otherwise + // we might get `./` and `../` bits in `exec_path` + let exe_url = Url::from_file_path(current_exe).unwrap(); + let path = exe_url.to_file_path().unwrap(); + Ok(json!(path)) +} + +#[derive(Deserialize)] +struct SetEnv { + key: String, + value: String, +} + +fn op_set_env( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: SetEnv = serde_json::from_value(args)?; + state.borrow::().check_env()?; + env::set_var(args.key, args.value); + Ok(json!({})) +} + +fn op_env( + state: &mut OpState, + _args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + state.borrow::().check_env()?; + let v = env::vars().collect::>(); + Ok(json!(v)) +} + +#[derive(Deserialize)] +struct GetEnv { + key: String, +} + +fn op_get_env( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: GetEnv = serde_json::from_value(args)?; + state.borrow::().check_env()?; + let r = match env::var(args.key) { + Err(env::VarError::NotPresent) => json!([]), + v => json!([v?]), + }; + Ok(r) +} + +#[derive(Deserialize)] +struct DeleteEnv { + key: String, +} + +fn op_delete_env( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: DeleteEnv = serde_json::from_value(args)?; + state.borrow::().check_env()?; + env::remove_var(args.key); + Ok(json!({})) +} + +#[derive(Deserialize)] +struct Exit { + code: i32, +} + +fn op_exit( + _state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: Exit = serde_json::from_value(args)?; + std::process::exit(args.code) +} + +fn op_loadavg( + state: &mut OpState, + _args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.loadavg"); + state.borrow::().check_env()?; + match sys_info::loadavg() { + Ok(loadavg) => Ok(json!([loadavg.one, loadavg.five, loadavg.fifteen])), + Err(_) => Ok(json!([0f64, 0f64, 0f64])), + } +} + +fn op_hostname( + state: &mut OpState, + _args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.hostname"); + state.borrow::().check_env()?; + let hostname = sys_info::hostname().unwrap_or_else(|_| "".to_string()); + Ok(json!(hostname)) +} + +fn op_os_release( + state: &mut OpState, + _args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.osRelease"); + state.borrow::().check_env()?; + let release = sys_info::os_release().unwrap_or_else(|_| "".to_string()); + Ok(json!(release)) +} + +fn op_system_memory_info( + state: &mut OpState, + _args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.systemMemoryInfo"); + state.borrow::().check_env()?; + match sys_info::mem_info() { + Ok(info) => Ok(json!({ + "total": info.total, + "free": info.free, + "available": info.avail, + "buffers": info.buffers, + "cached": info.cached, + "swapTotal": info.swap_total, + "swapFree": info.swap_free + })), + Err(_) => Ok(json!({})), + } +} + +fn op_system_cpu_info( + state: &mut OpState, + _args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.systemCpuInfo"); + state.borrow::().check_env()?; + + let cores = sys_info::cpu_num().ok(); + let speed = sys_info::cpu_speed().ok(); + + Ok(json!({ + "cores": cores, + "speed": speed + })) +} diff --git a/runtime/ops/permissions.rs b/runtime/ops/permissions.rs new file mode 100644 index 000000000..7474c0e37 --- /dev/null +++ b/runtime/ops/permissions.rs @@ -0,0 +1,103 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::permissions::Permissions; +use deno_core::error::custom_error; +use deno_core::error::AnyError; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; +use serde::Deserialize; +use std::path::Path; + +pub fn init(rt: &mut deno_core::JsRuntime) { + super::reg_json_sync(rt, "op_query_permission", op_query_permission); + super::reg_json_sync(rt, "op_revoke_permission", op_revoke_permission); + super::reg_json_sync(rt, "op_request_permission", op_request_permission); +} + +#[derive(Deserialize)] +struct PermissionArgs { + name: String, + url: Option, + path: Option, +} + +pub fn op_query_permission( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: PermissionArgs = serde_json::from_value(args)?; + let permissions = state.borrow::(); + let path = args.path.as_deref(); + let perm = match args.name.as_ref() { + "read" => permissions.query_read(&path.as_deref().map(Path::new)), + "write" => permissions.query_write(&path.as_deref().map(Path::new)), + "net" => permissions.query_net_url(&args.url.as_deref())?, + "env" => permissions.query_env(), + "run" => permissions.query_run(), + "plugin" => permissions.query_plugin(), + "hrtime" => permissions.query_hrtime(), + n => { + return Err(custom_error( + "ReferenceError", + format!("No such permission name: {}", n), + )) + } + }; + Ok(json!({ "state": perm.to_string() })) +} + +pub fn op_revoke_permission( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: PermissionArgs = serde_json::from_value(args)?; + let permissions = state.borrow_mut::(); + let path = args.path.as_deref(); + let perm = match args.name.as_ref() { + "read" => permissions.revoke_read(&path.as_deref().map(Path::new)), + "write" => permissions.revoke_write(&path.as_deref().map(Path::new)), + "net" => permissions.revoke_net(&args.url.as_deref())?, + "env" => permissions.revoke_env(), + "run" => permissions.revoke_run(), + "plugin" => permissions.revoke_plugin(), + "hrtime" => permissions.revoke_hrtime(), + n => { + return Err(custom_error( + "ReferenceError", + format!("No such permission name: {}", n), + )) + } + }; + Ok(json!({ "state": perm.to_string() })) +} + +pub fn op_request_permission( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: PermissionArgs = serde_json::from_value(args)?; + let permissions = state.borrow_mut::(); + let path = args.path.as_deref(); + let perm = match args.name.as_ref() { + "read" => permissions.request_read(&path.as_deref().map(Path::new)), + "write" => permissions.request_write(&path.as_deref().map(Path::new)), + "net" => permissions.request_net(&args.url.as_deref())?, + "env" => permissions.request_env(), + "run" => permissions.request_run(), + "plugin" => permissions.request_plugin(), + "hrtime" => permissions.request_hrtime(), + n => { + return Err(custom_error( + "ReferenceError", + format!("No such permission name: {}", n), + )) + } + }; + Ok(json!({ "state": perm.to_string() })) +} diff --git a/runtime/ops/plugin.rs b/runtime/ops/plugin.rs new file mode 100644 index 000000000..1f3669b6f --- /dev/null +++ b/runtime/ops/plugin.rs @@ -0,0 +1,156 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::metrics::metrics_op; +use crate::permissions::Permissions; +use deno_core::error::AnyError; +use deno_core::futures::prelude::*; +use deno_core::plugin_api; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::BufVec; +use deno_core::JsRuntime; +use deno_core::Op; +use deno_core::OpAsyncFuture; +use deno_core::OpId; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; +use dlopen::symbor::Library; +use serde::Deserialize; +use std::cell::RefCell; +use std::path::PathBuf; +use std::pin::Pin; +use std::rc::Rc; +use std::task::Context; +use std::task::Poll; + +pub fn init(rt: &mut JsRuntime) { + super::reg_json_sync(rt, "op_open_plugin", op_open_plugin); +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct OpenPluginArgs { + filename: String, +} + +pub fn op_open_plugin( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: OpenPluginArgs = serde_json::from_value(args)?; + let filename = PathBuf::from(&args.filename); + + super::check_unstable(state, "Deno.openPlugin"); + let permissions = state.borrow::(); + permissions.check_plugin(&filename)?; + + debug!("Loading Plugin: {:#?}", filename); + let plugin_lib = Library::open(filename).map(Rc::new)?; + let plugin_resource = PluginResource::new(&plugin_lib); + + let rid; + let deno_plugin_init; + { + rid = state + .resource_table + .add("plugin", Box::new(plugin_resource)); + deno_plugin_init = *unsafe { + state + .resource_table + .get::(rid) + .unwrap() + .lib + .symbol::("deno_plugin_init") + .unwrap() + }; + } + + let mut interface = PluginInterface::new(state, &plugin_lib); + deno_plugin_init(&mut interface); + + Ok(json!(rid)) +} + +struct PluginResource { + lib: Rc, +} + +impl PluginResource { + fn new(lib: &Rc) -> Self { + Self { lib: lib.clone() } + } +} + +struct PluginInterface<'a> { + state: &'a mut OpState, + plugin_lib: &'a Rc, +} + +impl<'a> PluginInterface<'a> { + fn new(state: &'a mut OpState, plugin_lib: &'a Rc) -> Self { + Self { state, plugin_lib } + } +} + +impl<'a> plugin_api::Interface for PluginInterface<'a> { + /// Does the same as `core::Isolate::register_op()`, but additionally makes + /// the registered op dispatcher, as well as the op futures created by it, + /// keep reference to the plugin `Library` object, so that the plugin doesn't + /// get unloaded before all its op registrations and the futures created by + /// them are dropped. + fn register_op( + &mut self, + name: &str, + dispatch_op_fn: plugin_api::DispatchOpFn, + ) -> OpId { + let plugin_lib = self.plugin_lib.clone(); + let plugin_op_fn = move |state_rc: Rc>, + mut zero_copy: BufVec| { + let mut state = state_rc.borrow_mut(); + let mut interface = PluginInterface::new(&mut state, &plugin_lib); + let op = dispatch_op_fn(&mut interface, &mut zero_copy); + match op { + sync_op @ Op::Sync(..) => sync_op, + Op::Async(fut) => Op::Async(PluginOpAsyncFuture::new(&plugin_lib, fut)), + Op::AsyncUnref(fut) => { + Op::AsyncUnref(PluginOpAsyncFuture::new(&plugin_lib, fut)) + } + _ => unreachable!(), + } + }; + self + .state + .op_table + .register_op(name, metrics_op(Box::new(plugin_op_fn))) + } +} + +struct PluginOpAsyncFuture { + fut: Option, + _plugin_lib: Rc, +} + +impl PluginOpAsyncFuture { + fn new(plugin_lib: &Rc, fut: OpAsyncFuture) -> Pin> { + let wrapped_fut = Self { + fut: Some(fut), + _plugin_lib: plugin_lib.clone(), + }; + Box::pin(wrapped_fut) + } +} + +impl Future for PluginOpAsyncFuture { + type Output = ::Output; + fn poll(mut self: Pin<&mut Self>, ctx: &mut Context) -> Poll { + self.fut.as_mut().unwrap().poll_unpin(ctx) + } +} + +impl Drop for PluginOpAsyncFuture { + fn drop(&mut self) { + self.fut.take(); + } +} diff --git a/runtime/ops/process.rs b/runtime/ops/process.rs new file mode 100644 index 000000000..67b3d0761 --- /dev/null +++ b/runtime/ops/process.rs @@ -0,0 +1,290 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use super::io::{std_file_resource, StreamResource, StreamResourceHolder}; +use crate::permissions::Permissions; +use deno_core::error::bad_resource_id; +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::futures::future::poll_fn; +use deno_core::futures::future::FutureExt; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::BufVec; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; +use serde::Deserialize; +use std::cell::RefCell; +use std::rc::Rc; +use tokio::process::Command; + +#[cfg(unix)] +use std::os::unix::process::ExitStatusExt; + +pub fn init(rt: &mut deno_core::JsRuntime) { + super::reg_json_sync(rt, "op_run", op_run); + super::reg_json_async(rt, "op_run_status", op_run_status); + super::reg_json_sync(rt, "op_kill", op_kill); +} + +fn clone_file( + state: &mut OpState, + rid: u32, +) -> Result { + std_file_resource(state, rid, move |r| match r { + Ok(std_file) => std_file.try_clone().map_err(AnyError::from), + Err(_) => Err(bad_resource_id()), + }) +} + +fn subprocess_stdio_map(s: &str) -> Result { + match s { + "inherit" => Ok(std::process::Stdio::inherit()), + "piped" => Ok(std::process::Stdio::piped()), + "null" => Ok(std::process::Stdio::null()), + _ => Err(type_error("Invalid resource for stdio")), + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct RunArgs { + cmd: Vec, + cwd: Option, + env: Vec<(String, String)>, + stdin: String, + stdout: String, + stderr: String, + stdin_rid: u32, + stdout_rid: u32, + stderr_rid: u32, +} + +struct ChildResource { + child: tokio::process::Child, +} + +fn op_run( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let run_args: RunArgs = serde_json::from_value(args)?; + state.borrow::().check_run()?; + + let args = run_args.cmd; + let env = run_args.env; + let cwd = run_args.cwd; + + let mut c = Command::new(args.get(0).unwrap()); + (1..args.len()).for_each(|i| { + let arg = args.get(i).unwrap(); + c.arg(arg); + }); + cwd.map(|d| c.current_dir(d)); + for (key, value) in &env { + c.env(key, value); + } + + // TODO: make this work with other resources, eg. sockets + if !run_args.stdin.is_empty() { + c.stdin(subprocess_stdio_map(run_args.stdin.as_ref())?); + } else { + let file = clone_file(state, run_args.stdin_rid)?; + c.stdin(file); + } + + if !run_args.stdout.is_empty() { + c.stdout(subprocess_stdio_map(run_args.stdout.as_ref())?); + } else { + let file = clone_file(state, run_args.stdout_rid)?; + c.stdout(file); + } + + if !run_args.stderr.is_empty() { + c.stderr(subprocess_stdio_map(run_args.stderr.as_ref())?); + } else { + let file = clone_file(state, run_args.stderr_rid)?; + c.stderr(file); + } + + // We want to kill child when it's closed + c.kill_on_drop(true); + + // Spawn the command. + let mut child = c.spawn()?; + let pid = child.id(); + + let stdin_rid = match child.stdin.take() { + Some(child_stdin) => { + let rid = state.resource_table.add( + "childStdin", + Box::new(StreamResourceHolder::new(StreamResource::ChildStdin( + child_stdin, + ))), + ); + Some(rid) + } + None => None, + }; + + let stdout_rid = match child.stdout.take() { + Some(child_stdout) => { + let rid = state.resource_table.add( + "childStdout", + Box::new(StreamResourceHolder::new(StreamResource::ChildStdout( + child_stdout, + ))), + ); + Some(rid) + } + None => None, + }; + + let stderr_rid = match child.stderr.take() { + Some(child_stderr) => { + let rid = state.resource_table.add( + "childStderr", + Box::new(StreamResourceHolder::new(StreamResource::ChildStderr( + child_stderr, + ))), + ); + Some(rid) + } + None => None, + }; + + let child_resource = ChildResource { child }; + let child_rid = state.resource_table.add("child", Box::new(child_resource)); + + Ok(json!({ + "rid": child_rid, + "pid": pid, + "stdinRid": stdin_rid, + "stdoutRid": stdout_rid, + "stderrRid": stderr_rid, + })) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct RunStatusArgs { + rid: i32, +} + +async fn op_run_status( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: RunStatusArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + + { + let s = state.borrow(); + s.borrow::().check_run()?; + } + + let run_status = poll_fn(|cx| { + let mut state = state.borrow_mut(); + let child_resource = state + .resource_table + .get_mut::(rid) + .ok_or_else(bad_resource_id)?; + let child = &mut child_resource.child; + child.poll_unpin(cx).map_err(AnyError::from) + }) + .await?; + + let code = run_status.code(); + + #[cfg(unix)] + let signal = run_status.signal(); + #[cfg(not(unix))] + let signal = None; + + code + .or(signal) + .expect("Should have either an exit code or a signal."); + let got_signal = signal.is_some(); + + Ok(json!({ + "gotSignal": got_signal, + "exitCode": code.unwrap_or(-1), + "exitSignal": signal.unwrap_or(-1), + })) +} + +#[cfg(not(unix))] +const SIGINT: i32 = 2; +#[cfg(not(unix))] +const SIGKILL: i32 = 9; +#[cfg(not(unix))] +const SIGTERM: i32 = 15; + +#[cfg(not(unix))] +use winapi::{ + shared::minwindef::DWORD, + um::{ + handleapi::CloseHandle, + processthreadsapi::{OpenProcess, TerminateProcess}, + winnt::PROCESS_TERMINATE, + }, +}; + +#[cfg(unix)] +pub fn kill(pid: i32, signo: i32) -> Result<(), AnyError> { + use nix::sys::signal::{kill as unix_kill, Signal}; + use nix::unistd::Pid; + use std::convert::TryFrom; + let sig = Signal::try_from(signo)?; + unix_kill(Pid::from_raw(pid), Option::Some(sig)).map_err(AnyError::from) +} + +#[cfg(not(unix))] +pub fn kill(pid: i32, signal: i32) -> Result<(), AnyError> { + use std::io::Error; + match signal { + SIGINT | SIGKILL | SIGTERM => { + if pid <= 0 { + return Err(type_error("unsupported pid")); + } + unsafe { + let handle = OpenProcess(PROCESS_TERMINATE, 0, pid as DWORD); + if handle.is_null() { + return Err(Error::last_os_error().into()); + } + if TerminateProcess(handle, 1) == 0 { + CloseHandle(handle); + return Err(Error::last_os_error().into()); + } + if CloseHandle(handle) == 0 { + return Err(Error::last_os_error().into()); + } + } + } + _ => { + return Err(type_error("unsupported signal")); + } + } + Ok(()) +} + +#[derive(Deserialize)] +struct KillArgs { + pid: i32, + signo: i32, +} + +fn op_kill( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.kill"); + state.borrow::().check_run()?; + + let args: KillArgs = serde_json::from_value(args)?; + kill(args.pid, args.signo)?; + Ok(json!({})) +} diff --git a/runtime/ops/runtime.rs b/runtime/ops/runtime.rs new file mode 100644 index 000000000..cb3b53d53 --- /dev/null +++ b/runtime/ops/runtime.rs @@ -0,0 +1,118 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::metrics::Metrics; +use crate::permissions::Permissions; +use deno_core::error::AnyError; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::ModuleSpecifier; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; + +pub fn init(rt: &mut deno_core::JsRuntime, main_module: ModuleSpecifier) { + { + let op_state = rt.op_state(); + let mut state = op_state.borrow_mut(); + state.put::(main_module); + } + super::reg_json_sync(rt, "op_main_module", op_main_module); + super::reg_json_sync(rt, "op_metrics", op_metrics); +} + +fn op_main_module( + state: &mut OpState, + _args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let main = state.borrow::().to_string(); + let main_url = ModuleSpecifier::resolve_url_or_path(&main)?; + if main_url.as_url().scheme() == "file" { + let main_path = std::env::current_dir().unwrap().join(main_url.to_string()); + state + .borrow::() + .check_read_blind(&main_path, "main_module")?; + } + Ok(json!(&main)) +} + +fn op_metrics( + state: &mut OpState, + _args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let m = state.borrow::(); + + Ok(json!({ + "opsDispatched": m.ops_dispatched, + "opsDispatchedSync": m.ops_dispatched_sync, + "opsDispatchedAsync": m.ops_dispatched_async, + "opsDispatchedAsyncUnref": m.ops_dispatched_async_unref, + "opsCompleted": m.ops_completed, + "opsCompletedSync": m.ops_completed_sync, + "opsCompletedAsync": m.ops_completed_async, + "opsCompletedAsyncUnref": m.ops_completed_async_unref, + "bytesSentControl": m.bytes_sent_control, + "bytesSentData": m.bytes_sent_data, + "bytesReceived": m.bytes_received + })) +} + +pub fn ppid() -> Value { + #[cfg(windows)] + { + // Adopted from rustup: + // https://github.com/rust-lang/rustup/blob/1.21.1/src/cli/self_update.rs#L1036 + // Copyright Diggory Blake, the Mozilla Corporation, and rustup contributors. + // Licensed under either of + // - Apache License, Version 2.0 + // - MIT license + use std::mem; + use winapi::shared::minwindef::DWORD; + use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE}; + use winapi::um::processthreadsapi::GetCurrentProcessId; + use winapi::um::tlhelp32::{ + CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32, + TH32CS_SNAPPROCESS, + }; + unsafe { + // Take a snapshot of system processes, one of which is ours + // and contains our parent's pid + let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if snapshot == INVALID_HANDLE_VALUE { + return serde_json::to_value(-1).unwrap(); + } + + let mut entry: PROCESSENTRY32 = mem::zeroed(); + entry.dwSize = mem::size_of::() as DWORD; + + // Iterate over system processes looking for ours + let success = Process32First(snapshot, &mut entry); + if success == 0 { + CloseHandle(snapshot); + return serde_json::to_value(-1).unwrap(); + } + + let this_pid = GetCurrentProcessId(); + while entry.th32ProcessID != this_pid { + let success = Process32Next(snapshot, &mut entry); + if success == 0 { + CloseHandle(snapshot); + return serde_json::to_value(-1).unwrap(); + } + } + CloseHandle(snapshot); + + // FIXME: Using the process ID exposes a race condition + // wherein the parent process already exited and the OS + // reassigned its ID. + let parent_id = entry.th32ParentProcessID; + serde_json::to_value(parent_id).unwrap() + } + } + #[cfg(not(windows))] + { + use std::os::unix::process::parent_id; + serde_json::to_value(parent_id()).unwrap() + } +} diff --git a/runtime/ops/signal.rs b/runtime/ops/signal.rs new file mode 100644 index 000000000..be6bc0a35 --- /dev/null +++ b/runtime/ops/signal.rs @@ -0,0 +1,142 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::AnyError; +use deno_core::serde_json::Value; +use deno_core::BufVec; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; +use std::cell::RefCell; +use std::rc::Rc; + +#[cfg(unix)] +use deno_core::error::bad_resource_id; +#[cfg(unix)] +use deno_core::futures::future::poll_fn; +#[cfg(unix)] +use deno_core::serde_json; +#[cfg(unix)] +use deno_core::serde_json::json; +#[cfg(unix)] +use serde::Deserialize; +#[cfg(unix)] +use std::task::Waker; +#[cfg(unix)] +use tokio::signal::unix::{signal, Signal, SignalKind}; + +pub fn init(rt: &mut deno_core::JsRuntime) { + super::reg_json_sync(rt, "op_signal_bind", op_signal_bind); + super::reg_json_sync(rt, "op_signal_unbind", op_signal_unbind); + super::reg_json_async(rt, "op_signal_poll", op_signal_poll); +} + +#[cfg(unix)] +/// The resource for signal stream. +/// The second element is the waker of polling future. +pub struct SignalStreamResource(pub Signal, pub Option); + +#[cfg(unix)] +#[derive(Deserialize)] +struct BindSignalArgs { + signo: i32, +} + +#[cfg(unix)] +#[derive(Deserialize)] +struct SignalArgs { + rid: i32, +} + +#[cfg(unix)] +fn op_signal_bind( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.signal"); + let args: BindSignalArgs = serde_json::from_value(args)?; + let rid = state.resource_table.add( + "signal", + Box::new(SignalStreamResource( + signal(SignalKind::from_raw(args.signo)).expect(""), + None, + )), + ); + Ok(json!({ + "rid": rid, + })) +} + +#[cfg(unix)] +async fn op_signal_poll( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + super::check_unstable2(&state, "Deno.signal"); + let args: SignalArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + + let future = poll_fn(move |cx| { + let mut state = state.borrow_mut(); + if let Some(mut signal) = + state.resource_table.get_mut::(rid) + { + signal.1 = Some(cx.waker().clone()); + return signal.0.poll_recv(cx); + } + std::task::Poll::Ready(None) + }); + let result = future.await; + Ok(json!({ "done": result.is_none() })) +} + +#[cfg(unix)] +pub fn op_signal_unbind( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.signal"); + let args: SignalArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + let resource = state.resource_table.get_mut::(rid); + if let Some(signal) = resource { + if let Some(waker) = &signal.1 { + // Wakes up the pending poll if exists. + // This prevents the poll future from getting stuck forever. + waker.clone().wake(); + } + } + state + .resource_table + .close(rid) + .ok_or_else(bad_resource_id)?; + Ok(json!({})) +} + +#[cfg(not(unix))] +pub fn op_signal_bind( + _state: &mut OpState, + _args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + unimplemented!(); +} + +#[cfg(not(unix))] +fn op_signal_unbind( + _state: &mut OpState, + _args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + unimplemented!(); +} + +#[cfg(not(unix))] +async fn op_signal_poll( + _state: Rc>, + _args: Value, + _zero_copy: BufVec, +) -> Result { + unimplemented!(); +} diff --git a/runtime/ops/timers.rs b/runtime/ops/timers.rs new file mode 100644 index 000000000..8037fd698 --- /dev/null +++ b/runtime/ops/timers.rs @@ -0,0 +1,193 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +//! This module helps deno implement timers. +//! +//! As an optimization, we want to avoid an expensive calls into rust for every +//! setTimeout in JavaScript. Thus in //js/timers.ts a data structure is +//! implemented that calls into Rust for only the smallest timeout. Thus we +//! only need to be able to start, cancel and await a single timer (or Delay, as Tokio +//! calls it) for an entire Isolate. This is what is implemented here. + +use super::dispatch_minimal::minimal_op; +use super::dispatch_minimal::MinimalOp; +use crate::metrics::metrics_op; +use crate::permissions::Permissions; +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::futures; +use deno_core::futures::channel::oneshot; +use deno_core::futures::FutureExt; +use deno_core::futures::TryFutureExt; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::BufVec; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; +use serde::Deserialize; +use std::cell::RefCell; +use std::future::Future; +use std::pin::Pin; +use std::rc::Rc; +use std::thread::sleep; +use std::time::Duration; +use std::time::Instant; + +pub type StartTime = Instant; + +type TimerFuture = Pin>>>; + +#[derive(Default)] +pub struct GlobalTimer { + tx: Option>, + pub future: Option, +} + +impl GlobalTimer { + pub fn cancel(&mut self) { + if let Some(tx) = self.tx.take() { + tx.send(()).ok(); + } + } + + pub fn new_timeout(&mut self, deadline: Instant) { + if self.tx.is_some() { + self.cancel(); + } + assert!(self.tx.is_none()); + self.future.take(); + + let (tx, rx) = oneshot::channel(); + self.tx = Some(tx); + + let delay = tokio::time::delay_until(deadline.into()); + let rx = rx + .map_err(|err| panic!("Unexpected error in receiving channel {:?}", err)); + + let fut = futures::future::select(delay, rx) + .then(|_| futures::future::ok(())) + .boxed_local(); + self.future = Some(fut); + } +} + +pub fn init(rt: &mut deno_core::JsRuntime) { + { + let op_state = rt.op_state(); + let mut state = op_state.borrow_mut(); + state.put::(GlobalTimer::default()); + state.put::(StartTime::now()); + } + super::reg_json_sync(rt, "op_global_timer_stop", op_global_timer_stop); + super::reg_json_sync(rt, "op_global_timer_start", op_global_timer_start); + super::reg_json_async(rt, "op_global_timer", op_global_timer); + rt.register_op("op_now", metrics_op(minimal_op(op_now))); + super::reg_json_sync(rt, "op_sleep_sync", op_sleep_sync); +} + +fn op_global_timer_stop( + state: &mut OpState, + _args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let global_timer = state.borrow_mut::(); + global_timer.cancel(); + Ok(json!({})) +} + +#[derive(Deserialize)] +struct GlobalTimerArgs { + timeout: u64, +} + +// Set up a timer that will be later awaited by JS promise. +// It's a separate op, because canceling a timeout immediately +// after setting it caused a race condition (because Tokio timeout) +// might have been registered after next event loop tick. +// +// See https://github.com/denoland/deno/issues/7599 for more +// details. +fn op_global_timer_start( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: GlobalTimerArgs = serde_json::from_value(args)?; + let val = args.timeout; + + let deadline = Instant::now() + Duration::from_millis(val); + let global_timer = state.borrow_mut::(); + global_timer.new_timeout(deadline); + Ok(json!({})) +} + +async fn op_global_timer( + state: Rc>, + _args: Value, + _zero_copy: BufVec, +) -> Result { + let maybe_timer_fut = { + let mut s = state.borrow_mut(); + let global_timer = s.borrow_mut::(); + global_timer.future.take() + }; + if let Some(timer_fut) = maybe_timer_fut { + let _ = timer_fut.await; + } + Ok(json!({})) +} + +// Returns a milliseconds and nanoseconds subsec +// since the start time of the deno runtime. +// If the High precision flag is not set, the +// nanoseconds are rounded on 2ms. +fn op_now( + state: Rc>, + // Arguments are discarded + _sync: bool, + _x: i32, + mut zero_copy: BufVec, +) -> MinimalOp { + match zero_copy.len() { + 0 => return MinimalOp::Sync(Err(type_error("no buffer specified"))), + 1 => {} + _ => { + return MinimalOp::Sync(Err(type_error("Invalid number of arguments"))) + } + } + + let op_state = state.borrow(); + let start_time = op_state.borrow::(); + let seconds = start_time.elapsed().as_secs(); + let mut subsec_nanos = start_time.elapsed().subsec_nanos() as f64; + let reduced_time_precision = 2_000_000.0; // 2ms in nanoseconds + + // If the permission is not enabled + // Round the nano result on 2 milliseconds + // see: https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#Reduced_time_precision + if op_state.borrow::().check_hrtime().is_err() { + subsec_nanos -= subsec_nanos % reduced_time_precision; + } + + let result = (seconds * 1_000) as f64 + (subsec_nanos / 1_000_000.0); + + (&mut zero_copy[0]).copy_from_slice(&result.to_be_bytes()); + + MinimalOp::Sync(Ok(0)) +} + +#[derive(Deserialize)] +struct SleepArgs { + millis: u64, +} + +fn op_sleep_sync( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.sleepSync"); + let args: SleepArgs = serde_json::from_value(args)?; + sleep(Duration::from_millis(args.millis)); + Ok(json!({})) +} diff --git a/runtime/ops/tls.rs b/runtime/ops/tls.rs new file mode 100644 index 000000000..37fd8f206 --- /dev/null +++ b/runtime/ops/tls.rs @@ -0,0 +1,431 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use super::io::{StreamResource, StreamResourceHolder}; +use crate::permissions::Permissions; +use crate::resolve_addr::resolve_addr; +use deno_core::error::bad_resource; +use deno_core::error::bad_resource_id; +use deno_core::error::custom_error; +use deno_core::error::AnyError; +use deno_core::futures; +use deno_core::futures::future::poll_fn; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::BufVec; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; +use serde::Deserialize; +use std::cell::RefCell; +use std::convert::From; +use std::fs::File; +use std::io::BufReader; +use std::net::SocketAddr; +use std::path::Path; +use std::rc::Rc; +use std::sync::Arc; +use std::task::Context; +use std::task::Poll; +use tokio::net::TcpListener; +use tokio::net::TcpStream; +use tokio_rustls::{rustls::ClientConfig, TlsConnector}; +use tokio_rustls::{ + rustls::{ + internal::pemfile::{certs, pkcs8_private_keys, rsa_private_keys}, + Certificate, NoClientAuth, PrivateKey, ServerConfig, + }, + TlsAcceptor, +}; +use webpki::DNSNameRef; + +pub fn init(rt: &mut deno_core::JsRuntime) { + super::reg_json_async(rt, "op_start_tls", op_start_tls); + super::reg_json_async(rt, "op_connect_tls", op_connect_tls); + super::reg_json_sync(rt, "op_listen_tls", op_listen_tls); + super::reg_json_async(rt, "op_accept_tls", op_accept_tls); +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ConnectTLSArgs { + transport: String, + hostname: String, + port: u16, + cert_file: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct StartTLSArgs { + rid: u32, + cert_file: Option, + hostname: String, +} + +async fn op_start_tls( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: StartTLSArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + let cert_file = args.cert_file.clone(); + + let mut domain = args.hostname; + if domain.is_empty() { + domain.push_str("localhost"); + } + { + super::check_unstable2(&state, "Deno.startTls"); + let s = state.borrow(); + let permissions = s.borrow::(); + permissions.check_net(&domain, 0)?; + if let Some(path) = cert_file.clone() { + permissions.check_read(Path::new(&path))?; + } + } + let mut resource_holder = { + let mut state_ = state.borrow_mut(); + match state_.resource_table.remove::(rid) { + Some(resource) => *resource, + None => return Err(bad_resource_id()), + } + }; + + if let StreamResource::TcpStream(ref mut tcp_stream) = + resource_holder.resource + { + let tcp_stream = tcp_stream.take().unwrap(); + let local_addr = tcp_stream.local_addr()?; + let remote_addr = tcp_stream.peer_addr()?; + let mut config = ClientConfig::new(); + config + .root_store + .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); + if let Some(path) = cert_file { + let key_file = File::open(path)?; + let reader = &mut BufReader::new(key_file); + config.root_store.add_pem_file(reader).unwrap(); + } + + let tls_connector = TlsConnector::from(Arc::new(config)); + let dnsname = + DNSNameRef::try_from_ascii_str(&domain).expect("Invalid DNS lookup"); + let tls_stream = tls_connector.connect(dnsname, tcp_stream).await?; + + let rid = { + let mut state_ = state.borrow_mut(); + state_.resource_table.add( + "clientTlsStream", + Box::new(StreamResourceHolder::new(StreamResource::ClientTlsStream( + Box::new(tls_stream), + ))), + ) + }; + Ok(json!({ + "rid": rid, + "localAddr": { + "hostname": local_addr.ip().to_string(), + "port": local_addr.port(), + "transport": "tcp", + }, + "remoteAddr": { + "hostname": remote_addr.ip().to_string(), + "port": remote_addr.port(), + "transport": "tcp", + } + })) + } else { + Err(bad_resource_id()) + } +} + +async fn op_connect_tls( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: ConnectTLSArgs = serde_json::from_value(args)?; + let cert_file = args.cert_file.clone(); + { + let s = state.borrow(); + let permissions = s.borrow::(); + permissions.check_net(&args.hostname, args.port)?; + if let Some(path) = cert_file.clone() { + permissions.check_read(Path::new(&path))?; + } + } + let mut domain = args.hostname.clone(); + if domain.is_empty() { + domain.push_str("localhost"); + } + + let addr = resolve_addr(&args.hostname, args.port)?; + let tcp_stream = TcpStream::connect(&addr).await?; + let local_addr = tcp_stream.local_addr()?; + let remote_addr = tcp_stream.peer_addr()?; + let mut config = ClientConfig::new(); + config + .root_store + .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); + if let Some(path) = cert_file { + let key_file = File::open(path)?; + let reader = &mut BufReader::new(key_file); + config.root_store.add_pem_file(reader).unwrap(); + } + let tls_connector = TlsConnector::from(Arc::new(config)); + let dnsname = + DNSNameRef::try_from_ascii_str(&domain).expect("Invalid DNS lookup"); + let tls_stream = tls_connector.connect(dnsname, tcp_stream).await?; + let rid = { + let mut state_ = state.borrow_mut(); + state_.resource_table.add( + "clientTlsStream", + Box::new(StreamResourceHolder::new(StreamResource::ClientTlsStream( + Box::new(tls_stream), + ))), + ) + }; + Ok(json!({ + "rid": rid, + "localAddr": { + "hostname": local_addr.ip().to_string(), + "port": local_addr.port(), + "transport": args.transport, + }, + "remoteAddr": { + "hostname": remote_addr.ip().to_string(), + "port": remote_addr.port(), + "transport": args.transport, + } + })) +} + +fn load_certs(path: &str) -> Result, AnyError> { + let cert_file = File::open(path)?; + let reader = &mut BufReader::new(cert_file); + + let certs = certs(reader) + .map_err(|_| custom_error("InvalidData", "Unable to decode certificate"))?; + + if certs.is_empty() { + let e = custom_error("InvalidData", "No certificates found in cert file"); + return Err(e); + } + + Ok(certs) +} + +fn key_decode_err() -> AnyError { + custom_error("InvalidData", "Unable to decode key") +} + +fn key_not_found_err() -> AnyError { + custom_error("InvalidData", "No keys found in key file") +} + +/// Starts with -----BEGIN RSA PRIVATE KEY----- +fn load_rsa_keys(path: &str) -> Result, AnyError> { + let key_file = File::open(path)?; + let reader = &mut BufReader::new(key_file); + let keys = rsa_private_keys(reader).map_err(|_| key_decode_err())?; + Ok(keys) +} + +/// Starts with -----BEGIN PRIVATE KEY----- +fn load_pkcs8_keys(path: &str) -> Result, AnyError> { + let key_file = File::open(path)?; + let reader = &mut BufReader::new(key_file); + let keys = pkcs8_private_keys(reader).map_err(|_| key_decode_err())?; + Ok(keys) +} + +fn load_keys(path: &str) -> Result, AnyError> { + let path = path.to_string(); + let mut keys = load_rsa_keys(&path)?; + + if keys.is_empty() { + keys = load_pkcs8_keys(&path)?; + } + + if keys.is_empty() { + return Err(key_not_found_err()); + } + + Ok(keys) +} + +#[allow(dead_code)] +pub struct TlsListenerResource { + listener: TcpListener, + tls_acceptor: TlsAcceptor, + waker: Option, + local_addr: SocketAddr, +} + +impl Drop for TlsListenerResource { + fn drop(&mut self) { + self.wake_task(); + } +} + +impl TlsListenerResource { + /// Track the current task so future awaiting for connection + /// can be notified when listener is closed. + /// + /// Throws an error if another task is already tracked. + pub fn track_task(&mut self, cx: &Context) -> Result<(), AnyError> { + // Currently, we only allow tracking a single accept task for a listener. + // This might be changed in the future with multiple workers. + // Caveat: TcpListener by itself also only tracks an accept task at a time. + // See https://github.com/tokio-rs/tokio/issues/846#issuecomment-454208883 + if self.waker.is_some() { + return Err(custom_error("Busy", "Another accept task is ongoing")); + } + + let waker = futures::task::AtomicWaker::new(); + waker.register(cx.waker()); + self.waker.replace(waker); + Ok(()) + } + + /// Notifies a task when listener is closed so accept future can resolve. + pub fn wake_task(&mut self) { + if let Some(waker) = self.waker.as_ref() { + waker.wake(); + } + } + + /// Stop tracking a task. + /// Happens when the task is done and thus no further tracking is needed. + pub fn untrack_task(&mut self) { + self.waker.take(); + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ListenTlsArgs { + transport: String, + hostname: String, + port: u16, + cert_file: String, + key_file: String, +} + +fn op_listen_tls( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: ListenTlsArgs = serde_json::from_value(args)?; + assert_eq!(args.transport, "tcp"); + + let cert_file = args.cert_file; + let key_file = args.key_file; + { + let permissions = state.borrow::(); + permissions.check_net(&args.hostname, args.port)?; + permissions.check_read(Path::new(&cert_file))?; + permissions.check_read(Path::new(&key_file))?; + } + let mut config = ServerConfig::new(NoClientAuth::new()); + config + .set_single_cert(load_certs(&cert_file)?, load_keys(&key_file)?.remove(0)) + .expect("invalid key or certificate"); + let tls_acceptor = TlsAcceptor::from(Arc::new(config)); + let addr = resolve_addr(&args.hostname, args.port)?; + let std_listener = std::net::TcpListener::bind(&addr)?; + let listener = TcpListener::from_std(std_listener)?; + let local_addr = listener.local_addr()?; + let tls_listener_resource = TlsListenerResource { + listener, + tls_acceptor, + waker: None, + local_addr, + }; + + let rid = state + .resource_table + .add("tlsListener", Box::new(tls_listener_resource)); + + Ok(json!({ + "rid": rid, + "localAddr": { + "hostname": local_addr.ip().to_string(), + "port": local_addr.port(), + "transport": args.transport, + }, + })) +} + +#[derive(Deserialize)] +struct AcceptTlsArgs { + rid: i32, +} + +async fn op_accept_tls( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: AcceptTlsArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + let accept_fut = poll_fn(|cx| { + let mut state = state.borrow_mut(); + let listener_resource = state + .resource_table + .get_mut::(rid) + .ok_or_else(|| bad_resource("Listener has been closed"))?; + let listener = &mut listener_resource.listener; + match listener.poll_accept(cx).map_err(AnyError::from) { + Poll::Ready(Ok((stream, addr))) => { + listener_resource.untrack_task(); + Poll::Ready(Ok((stream, addr))) + } + Poll::Pending => { + listener_resource.track_task(cx)?; + Poll::Pending + } + Poll::Ready(Err(e)) => { + listener_resource.untrack_task(); + Poll::Ready(Err(e)) + } + } + }); + let (tcp_stream, _socket_addr) = accept_fut.await?; + let local_addr = tcp_stream.local_addr()?; + let remote_addr = tcp_stream.peer_addr()?; + let tls_acceptor = { + let state_ = state.borrow(); + let resource = state_ + .resource_table + .get::(rid) + .ok_or_else(bad_resource_id) + .expect("Can't find tls listener"); + resource.tls_acceptor.clone() + }; + let tls_stream = tls_acceptor.accept(tcp_stream).await?; + let rid = { + let mut state_ = state.borrow_mut(); + state_.resource_table.add( + "serverTlsStream", + Box::new(StreamResourceHolder::new(StreamResource::ServerTlsStream( + Box::new(tls_stream), + ))), + ) + }; + Ok(json!({ + "rid": rid, + "localAddr": { + "transport": "tcp", + "hostname": local_addr.ip().to_string(), + "port": local_addr.port() + }, + "remoteAddr": { + "transport": "tcp", + "hostname": remote_addr.ip().to_string(), + "port": remote_addr.port() + } + })) +} diff --git a/runtime/ops/tty.rs b/runtime/ops/tty.rs new file mode 100644 index 000000000..ad66bcf1a --- /dev/null +++ b/runtime/ops/tty.rs @@ -0,0 +1,334 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use super::io::std_file_resource; +use super::io::StreamResource; +use super::io::StreamResourceHolder; +use deno_core::error::bad_resource_id; +use deno_core::error::not_supported; +use deno_core::error::resource_unavailable; +use deno_core::error::AnyError; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; +use serde::Deserialize; +use serde::Serialize; +use std::io::Error; + +#[cfg(unix)] +use nix::sys::termios; + +#[cfg(windows)] +use deno_core::error::custom_error; +#[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(Error::last_os_error().into()); + } else if handle.is_null() { + return Err(custom_error("ReferenceError", "null handle")); + } + Ok(handle) +} + +pub fn init(rt: &mut deno_core::JsRuntime) { + super::reg_json_sync(rt, "op_set_raw", op_set_raw); + super::reg_json_sync(rt, "op_isatty", op_isatty); + super::reg_json_sync(rt, "op_console_size", op_console_size); +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SetRawOptions { + cbreak: bool, +} + +#[derive(Deserialize)] +struct SetRawArgs { + rid: u32, + mode: bool, + options: SetRawOptions, +} + +fn op_set_raw( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.setRaw"); + + let args: SetRawArgs = serde_json::from_value(args)?; + let rid = args.rid; + let is_raw = args.mode; + let cbreak = args.options.cbreak; + + // 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::shared::minwindef::FALSE; + use winapi::um::{consoleapi, handleapi}; + + let resource_holder = + state.resource_table.get_mut::(rid); + if resource_holder.is_none() { + return Err(bad_resource_id()); + } + if cbreak { + return Err(not_supported()); + } + let resource_holder = resource_holder.unwrap(); + + // For now, only stdin. + let handle = match &mut resource_holder.resource { + StreamResource::FsFile(ref mut option_file_metadata) => { + if let Some((tokio_file, metadata)) = option_file_metadata.take() { + match tokio_file.try_into_std() { + Ok(std_file) => { + let raw_handle = std_file.as_raw_handle(); + // Turn the std_file handle back into a tokio file, put it back + // in the resource table. + let tokio_file = tokio::fs::File::from_std(std_file); + resource_holder.resource = + StreamResource::FsFile(Some((tokio_file, metadata))); + // return the result. + raw_handle + } + Err(tokio_file) => { + // This function will return an error containing the file if + // some operation is in-flight. + resource_holder.resource = + StreamResource::FsFile(Some((tokio_file, metadata))); + return Err(resource_unavailable()); + } + } + } else { + return Err(resource_unavailable()); + } + } + _ => { + return Err(bad_resource_id()); + } + }; + + if handle == handleapi::INVALID_HANDLE_VALUE { + return Err(Error::last_os_error().into()); + } else if handle.is_null() { + return Err(custom_error("ReferenceError", "null handle")); + } + let mut original_mode: DWORD = 0; + if unsafe { consoleapi::GetConsoleMode(handle, &mut original_mode) } + == FALSE + { + return Err(Error::last_os_error().into()); + } + let new_mode = if is_raw { + original_mode & !RAW_MODE_MASK + } else { + original_mode | RAW_MODE_MASK + }; + if unsafe { consoleapi::SetConsoleMode(handle, new_mode) } == FALSE { + return Err(Error::last_os_error().into()); + } + + Ok(json!({})) + } + #[cfg(unix)] + { + use std::os::unix::io::AsRawFd; + + let resource_holder = + state.resource_table.get_mut::(rid); + if resource_holder.is_none() { + return Err(bad_resource_id()); + } + + if is_raw { + let (raw_fd, maybe_tty_mode) = + match &mut resource_holder.unwrap().resource { + StreamResource::FsFile(Some((f, ref mut metadata))) => { + (f.as_raw_fd(), &mut metadata.tty.mode) + } + StreamResource::FsFile(None) => return Err(resource_unavailable()), + _ => { + return Err(not_supported()); + } + }; + + if maybe_tty_mode.is_none() { + // Save original mode. + let original_mode = termios::tcgetattr(raw_fd)?; + maybe_tty_mode.replace(original_mode); + } + + let mut raw = maybe_tty_mode.clone().unwrap(); + + 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); + if !cbreak { + raw.local_flags &= !(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(json!({})) + } else { + // Try restore saved mode. + let (raw_fd, maybe_tty_mode) = + match &mut resource_holder.unwrap().resource { + StreamResource::FsFile(Some((f, ref mut metadata))) => { + (f.as_raw_fd(), &mut metadata.tty.mode) + } + StreamResource::FsFile(None) => { + return Err(resource_unavailable()); + } + _ => { + return Err(bad_resource_id()); + } + }; + + if let Some(mode) = maybe_tty_mode.take() { + termios::tcsetattr(raw_fd, termios::SetArg::TCSADRAIN, &mode)?; + } + + Ok(json!({})) + } + } +} + +#[derive(Deserialize)] +struct IsattyArgs { + rid: u32, +} + +fn op_isatty( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: IsattyArgs = serde_json::from_value(args)?; + let rid = args.rid; + + let isatty: bool = std_file_resource(state, rid as u32, move |r| match r { + Ok(std_file) => { + #[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. + Ok(unsafe { consoleapi::GetConsoleMode(handle, &mut test_mode) != 0 }) + } + #[cfg(unix)] + { + use std::os::unix::io::AsRawFd; + let raw_fd = std_file.as_raw_fd(); + Ok(unsafe { libc::isatty(raw_fd as libc::c_int) == 1 }) + } + } + Err(StreamResource::FsFile(_)) => unreachable!(), + _ => Ok(false), + })?; + Ok(json!(isatty)) +} + +#[derive(Deserialize)] +struct ConsoleSizeArgs { + rid: u32, +} + +#[derive(Serialize)] +struct ConsoleSize { + columns: u32, + rows: u32, +} + +fn op_console_size( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + super::check_unstable(state, "Deno.consoleSize"); + + let args: ConsoleSizeArgs = serde_json::from_value(args)?; + let rid = args.rid; + + let size = std_file_resource(state, rid as u32, move |r| match r { + Ok(std_file) => { + #[cfg(windows)] + { + use std::os::windows::io::AsRawHandle; + let handle = std_file.as_raw_handle(); + + unsafe { + let mut bufinfo: winapi::um::wincon::CONSOLE_SCREEN_BUFFER_INFO = + std::mem::zeroed(); + + if winapi::um::wincon::GetConsoleScreenBufferInfo( + handle, + &mut bufinfo, + ) == 0 + { + return Err(Error::last_os_error().into()); + } + + Ok(ConsoleSize { + columns: bufinfo.dwSize.X as u32, + rows: bufinfo.dwSize.Y as u32, + }) + } + } + + #[cfg(unix)] + { + use std::os::unix::io::AsRawFd; + + let fd = std_file.as_raw_fd(); + unsafe { + let mut size: libc::winsize = std::mem::zeroed(); + if libc::ioctl(fd, libc::TIOCGWINSZ, &mut size as *mut _) != 0 { + return Err(Error::last_os_error().into()); + } + + // TODO (caspervonb) return a tuple instead + Ok(ConsoleSize { + columns: size.ws_col as u32, + rows: size.ws_row as u32, + }) + } + } + } + Err(_) => Err(bad_resource_id()), + })?; + + Ok(json!(size)) +} diff --git a/runtime/ops/web_worker.rs b/runtime/ops/web_worker.rs new file mode 100644 index 000000000..d88330a04 --- /dev/null +++ b/runtime/ops/web_worker.rs @@ -0,0 +1,37 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::web_worker::WebWorkerHandle; +use crate::web_worker::WorkerEvent; +use deno_core::futures::channel::mpsc; +use deno_core::serde_json::json; + +pub fn init( + rt: &mut deno_core::JsRuntime, + sender: mpsc::Sender, + handle: WebWorkerHandle, +) { + // Post message to host as guest worker. + let sender_ = sender.clone(); + super::reg_json_sync( + rt, + "op_worker_post_message", + move |_state, _args, bufs| { + assert_eq!(bufs.len(), 1, "Invalid number of arguments"); + let msg_buf: Box<[u8]> = (*bufs[0]).into(); + sender_ + .clone() + .try_send(WorkerEvent::Message(msg_buf)) + .expect("Failed to post message to host"); + Ok(json!({})) + }, + ); + + // Notify host that guest worker closes. + super::reg_json_sync(rt, "op_worker_close", move |_state, _args, _bufs| { + // Notify parent that we're finished + sender.clone().close_channel(); + // Terminate execution of current worker + handle.terminate(); + Ok(json!({})) + }); +} diff --git a/runtime/ops/websocket.rs b/runtime/ops/websocket.rs new file mode 100644 index 000000000..a8c591a33 --- /dev/null +++ b/runtime/ops/websocket.rs @@ -0,0 +1,326 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::permissions::Permissions; +use core::task::Poll; +use deno_core::error::bad_resource_id; +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::futures::future::poll_fn; +use deno_core::futures::StreamExt; +use deno_core::futures::{ready, SinkExt}; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::url; +use deno_core::BufVec; +use deno_core::OpState; +use deno_core::{serde_json, ZeroCopyBuf}; +use http::{Method, Request, Uri}; +use serde::Deserialize; +use std::borrow::Cow; +use std::cell::RefCell; +use std::fs::File; +use std::io::BufReader; +use std::rc::Rc; +use std::sync::Arc; +use tokio::net::TcpStream; +use tokio_rustls::{rustls::ClientConfig, TlsConnector}; +use tokio_tungstenite::stream::Stream as StreamSwitcher; +use tokio_tungstenite::tungstenite::Error as TungsteniteError; +use tokio_tungstenite::tungstenite::{ + handshake::client::Response, protocol::frame::coding::CloseCode, + protocol::CloseFrame, Message, +}; +use tokio_tungstenite::{client_async, WebSocketStream}; +use webpki::DNSNameRef; + +#[derive(Clone)] +struct WsCaFile(String); +#[derive(Clone)] +struct WsUserAgent(String); + +pub fn init( + rt: &mut deno_core::JsRuntime, + maybe_ca_file: Option<&str>, + user_agent: String, +) { + { + let op_state = rt.op_state(); + let mut state = op_state.borrow_mut(); + if let Some(ca_file) = maybe_ca_file { + state.put::(WsCaFile(ca_file.to_string())); + } + state.put::(WsUserAgent(user_agent)); + } + super::reg_json_sync(rt, "op_ws_check_permission", op_ws_check_permission); + super::reg_json_async(rt, "op_ws_create", op_ws_create); + super::reg_json_async(rt, "op_ws_send", op_ws_send); + super::reg_json_async(rt, "op_ws_close", op_ws_close); + super::reg_json_async(rt, "op_ws_next_event", op_ws_next_event); +} + +type MaybeTlsStream = + StreamSwitcher>; + +type WsStream = WebSocketStream; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct CheckPermissionArgs { + url: String, +} + +// This op is needed because creating a WS instance in JavaScript is a sync +// operation and should throw error when permissions are not fullfiled, +// but actual op that connects WS is async. +pub fn op_ws_check_permission( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: CheckPermissionArgs = serde_json::from_value(args)?; + + state + .borrow::() + .check_net_url(&url::Url::parse(&args.url)?)?; + + Ok(json!({})) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct CreateArgs { + url: String, + protocols: String, +} + +pub async fn op_ws_create( + state: Rc>, + args: Value, + _bufs: BufVec, +) -> Result { + let args: CreateArgs = serde_json::from_value(args)?; + + { + let s = state.borrow(); + s.borrow::() + .check_net_url(&url::Url::parse(&args.url)?) + .expect( + "Permission check should have been done in op_ws_check_permission", + ); + } + + let maybe_ca_file = state.borrow().try_borrow::().cloned(); + let user_agent = state.borrow().borrow::().0.clone(); + let uri: Uri = args.url.parse()?; + let mut request = Request::builder().method(Method::GET).uri(&uri); + + request = request.header("User-Agent", user_agent); + + if !args.protocols.is_empty() { + request = request.header("Sec-WebSocket-Protocol", args.protocols); + } + + let request = request.body(())?; + let domain = &uri.host().unwrap().to_string(); + let port = &uri.port_u16().unwrap_or(match uri.scheme_str() { + Some("wss") => 443, + Some("ws") => 80, + _ => unreachable!(), + }); + let addr = format!("{}:{}", domain, port); + let try_socket = TcpStream::connect(addr).await; + let tcp_socket = match try_socket.map_err(TungsteniteError::Io) { + Ok(socket) => socket, + Err(_) => return Ok(json!({"success": false})), + }; + + let socket: MaybeTlsStream = match uri.scheme_str() { + Some("ws") => StreamSwitcher::Plain(tcp_socket), + Some("wss") => { + let mut config = ClientConfig::new(); + config + .root_store + .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); + + if let Some(ws_ca_file) = maybe_ca_file { + let key_file = File::open(ws_ca_file.0)?; + let reader = &mut BufReader::new(key_file); + config.root_store.add_pem_file(reader).unwrap(); + } + + let tls_connector = TlsConnector::from(Arc::new(config)); + let dnsname = + DNSNameRef::try_from_ascii_str(&domain).expect("Invalid DNS lookup"); + let tls_socket = tls_connector.connect(dnsname, tcp_socket).await?; + StreamSwitcher::Tls(tls_socket) + } + _ => unreachable!(), + }; + + let (stream, response): (WsStream, Response) = + client_async(request, socket).await.map_err(|err| { + type_error(format!( + "failed to connect to WebSocket: {}", + err.to_string() + )) + })?; + + let mut state = state.borrow_mut(); + let rid = state + .resource_table + .add("webSocketStream", Box::new(stream)); + + let protocol = match response.headers().get("Sec-WebSocket-Protocol") { + Some(header) => header.to_str().unwrap(), + None => "", + }; + let extensions = response + .headers() + .get_all("Sec-WebSocket-Extensions") + .iter() + .map(|header| header.to_str().unwrap()) + .collect::(); + Ok(json!({ + "success": true, + "rid": rid, + "protocol": protocol, + "extensions": extensions + })) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SendArgs { + rid: u32, + text: Option, +} + +pub async fn op_ws_send( + state: Rc>, + args: Value, + bufs: BufVec, +) -> Result { + let args: SendArgs = serde_json::from_value(args)?; + + let mut maybe_msg = Some(match args.text { + Some(text) => Message::Text(text), + None => Message::Binary(bufs[0].to_vec()), + }); + let rid = args.rid; + + poll_fn(move |cx| { + let mut state = state.borrow_mut(); + let stream = state + .resource_table + .get_mut::(rid) + .ok_or_else(bad_resource_id)?; + + // TODO(ry) Handle errors below instead of unwrap. + // Need to map `TungsteniteError` to `AnyError`. + ready!(stream.poll_ready_unpin(cx)).unwrap(); + if let Some(msg) = maybe_msg.take() { + stream.start_send_unpin(msg).unwrap(); + } + ready!(stream.poll_flush_unpin(cx)).unwrap(); + + Poll::Ready(Ok(json!({}))) + }) + .await +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct CloseArgs { + rid: u32, + code: Option, + reason: Option, +} + +pub async fn op_ws_close( + state: Rc>, + args: Value, + _bufs: BufVec, +) -> Result { + let args: CloseArgs = serde_json::from_value(args)?; + let rid = args.rid; + let mut maybe_msg = Some(Message::Close(args.code.map(|c| CloseFrame { + code: CloseCode::from(c), + reason: match args.reason { + Some(reason) => Cow::from(reason), + None => Default::default(), + }, + }))); + + poll_fn(move |cx| { + let mut state = state.borrow_mut(); + let stream = state + .resource_table + .get_mut::(rid) + .ok_or_else(bad_resource_id)?; + + // TODO(ry) Handle errors below instead of unwrap. + // Need to map `TungsteniteError` to `AnyError`. + ready!(stream.poll_ready_unpin(cx)).unwrap(); + if let Some(msg) = maybe_msg.take() { + stream.start_send_unpin(msg).unwrap(); + } + ready!(stream.poll_flush_unpin(cx)).unwrap(); + ready!(stream.poll_close_unpin(cx)).unwrap(); + + Poll::Ready(Ok(json!({}))) + }) + .await +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct NextEventArgs { + rid: u32, +} + +pub async fn op_ws_next_event( + state: Rc>, + args: Value, + _bufs: BufVec, +) -> Result { + let args: NextEventArgs = serde_json::from_value(args)?; + poll_fn(move |cx| { + let mut state = state.borrow_mut(); + let stream = state + .resource_table + .get_mut::(args.rid) + .ok_or_else(bad_resource_id)?; + stream + .poll_next_unpin(cx) + .map(|val| { + match val { + Some(Ok(Message::Text(text))) => json!({ + "type": "string", + "data": text + }), + Some(Ok(Message::Binary(data))) => { + // TODO(ry): don't use json to send binary data. + json!({ + "type": "binary", + "data": data + }) + } + Some(Ok(Message::Close(Some(frame)))) => json!({ + "type": "close", + "code": u16::from(frame.code), + "reason": frame.reason.as_ref() + }), + Some(Ok(Message::Close(None))) => json!({ "type": "close" }), + Some(Ok(Message::Ping(_))) => json!({"type": "ping"}), + Some(Ok(Message::Pong(_))) => json!({"type": "pong"}), + Some(Err(_)) => json!({"type": "error"}), + None => { + state.resource_table.close(args.rid).unwrap(); + json!({"type": "closed"}) + } + } + }) + .map(Ok) + }) + .await +} diff --git a/runtime/ops/worker_host.rs b/runtime/ops/worker_host.rs new file mode 100644 index 000000000..871e4b9fe --- /dev/null +++ b/runtime/ops/worker_host.rs @@ -0,0 +1,318 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::permissions::Permissions; +use crate::web_worker::run_web_worker; +use crate::web_worker::WebWorker; +use crate::web_worker::WebWorkerHandle; +use crate::web_worker::WorkerEvent; +use deno_core::error::generic_error; +use deno_core::error::AnyError; +use deno_core::error::JsError; +use deno_core::futures::channel::mpsc; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::BufVec; +use deno_core::ModuleSpecifier; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; +use serde::Deserialize; +use std::cell::RefCell; +use std::collections::HashMap; +use std::convert::From; +use std::rc::Rc; +use std::sync::Arc; +use std::thread::JoinHandle; + +pub struct CreateWebWorkerArgs { + pub name: String, + pub worker_id: u32, + pub permissions: Permissions, + pub main_module: ModuleSpecifier, + pub use_deno_namespace: bool, +} + +pub type CreateWebWorkerCb = + dyn Fn(CreateWebWorkerArgs) -> WebWorker + Sync + Send; + +/// A holder for callback that is used to create a new +/// WebWorker. It's a struct instead of a type alias +/// because `GothamState` used in `OpState` overrides +/// value if type alises have the same underlying type +#[derive(Clone)] +pub struct CreateWebWorkerCbHolder(Arc); + +#[derive(Deserialize)] +struct HostUnhandledErrorArgs { + message: String, +} + +pub fn init( + rt: &mut deno_core::JsRuntime, + sender: Option>, + create_web_worker_cb: Arc, +) { + { + let op_state = rt.op_state(); + let mut state = op_state.borrow_mut(); + state.put::(WorkersTable::default()); + state.put::(WorkerId::default()); + + let create_module_loader = CreateWebWorkerCbHolder(create_web_worker_cb); + state.put::(create_module_loader); + } + super::reg_json_sync(rt, "op_create_worker", op_create_worker); + super::reg_json_sync( + rt, + "op_host_terminate_worker", + op_host_terminate_worker, + ); + super::reg_json_sync(rt, "op_host_post_message", op_host_post_message); + super::reg_json_async(rt, "op_host_get_message", op_host_get_message); + super::reg_json_sync( + rt, + "op_host_unhandled_error", + move |_state, args, _zero_copy| { + if let Some(mut sender) = sender.clone() { + let args: HostUnhandledErrorArgs = serde_json::from_value(args)?; + sender + .try_send(WorkerEvent::Error(generic_error(args.message))) + .expect("Failed to propagate error event to parent worker"); + Ok(json!(true)) + } else { + Err(generic_error("Cannot be called from main worker.")) + } + }, + ); +} + +pub struct WorkerThread { + join_handle: JoinHandle>, + worker_handle: WebWorkerHandle, +} + +pub type WorkersTable = HashMap; +pub type WorkerId = u32; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct CreateWorkerArgs { + name: Option, + specifier: String, + has_source_code: bool, + source_code: String, + use_deno_namespace: bool, +} + +/// Create worker as the host +fn op_create_worker( + state: &mut OpState, + args: Value, + _data: &mut [ZeroCopyBuf], +) -> Result { + let args: CreateWorkerArgs = serde_json::from_value(args)?; + + let specifier = args.specifier.clone(); + let maybe_source_code = if args.has_source_code { + Some(args.source_code.clone()) + } else { + None + }; + let args_name = args.name; + let use_deno_namespace = args.use_deno_namespace; + if use_deno_namespace { + super::check_unstable(state, "Worker.deno"); + } + let permissions = state.borrow::().clone(); + let worker_id = state.take::(); + let create_module_loader = state.take::(); + state.put::(create_module_loader.clone()); + state.put::(worker_id + 1); + + let module_specifier = ModuleSpecifier::resolve_url(&specifier)?; + let worker_name = args_name.unwrap_or_else(|| "".to_string()); + + let (handle_sender, handle_receiver) = + std::sync::mpsc::sync_channel::>(1); + + // Setup new thread + let thread_builder = + std::thread::Builder::new().name(format!("deno-worker-{}", worker_id)); + + // Spawn it + let join_handle = thread_builder.spawn(move || { + // Any error inside this block is terminal: + // - JS worker is useless - meaning it throws an exception and can't do anything else, + // all action done upon it should be noops + // - newly spawned thread exits + + let worker = (create_module_loader.0)(CreateWebWorkerArgs { + name: worker_name, + worker_id, + permissions, + main_module: module_specifier.clone(), + use_deno_namespace, + }); + + // Send thread safe handle to newly created worker to host thread + handle_sender.send(Ok(worker.thread_safe_handle())).unwrap(); + drop(handle_sender); + + // At this point the only method of communication with host + // is using `worker.internal_channels`. + // + // Host can already push messages and interact with worker. + run_web_worker(worker, module_specifier, maybe_source_code) + })?; + + let worker_handle = handle_receiver.recv().unwrap()?; + + let worker_thread = WorkerThread { + join_handle, + worker_handle, + }; + + // At this point all interactions with worker happen using thread + // safe handler returned from previous function calls + state + .borrow_mut::() + .insert(worker_id, worker_thread); + + Ok(json!({ "id": worker_id })) +} + +#[derive(Deserialize)] +struct WorkerArgs { + id: i32, +} + +fn op_host_terminate_worker( + state: &mut OpState, + args: Value, + _data: &mut [ZeroCopyBuf], +) -> Result { + let args: WorkerArgs = serde_json::from_value(args)?; + let id = args.id as u32; + let worker_thread = state + .borrow_mut::() + .remove(&id) + .expect("No worker handle found"); + worker_thread.worker_handle.terminate(); + worker_thread + .join_handle + .join() + .expect("Panic in worker thread") + .expect("Panic in worker event loop"); + Ok(json!({})) +} + +fn serialize_worker_event(event: WorkerEvent) -> Value { + match event { + WorkerEvent::Message(buf) => json!({ "type": "msg", "data": buf }), + WorkerEvent::TerminalError(error) => match error.downcast::() { + Ok(js_error) => json!({ + "type": "terminalError", + "error": { + "message": js_error.message, + "fileName": js_error.script_resource_name, + "lineNumber": js_error.line_number, + "columnNumber": js_error.start_column, + } + }), + Err(error) => json!({ + "type": "terminalError", + "error": { + "message": error.to_string(), + } + }), + }, + WorkerEvent::Error(error) => match error.downcast::() { + Ok(js_error) => json!({ + "type": "error", + "error": { + "message": js_error.message, + "fileName": js_error.script_resource_name, + "lineNumber": js_error.line_number, + "columnNumber": js_error.start_column, + } + }), + Err(error) => json!({ + "type": "error", + "error": { + "message": error.to_string(), + } + }), + }, + } +} + +/// Try to remove worker from workers table - NOTE: `Worker.terminate()` +/// might have been called already meaning that we won't find worker in +/// table - in that case ignore. +fn try_remove_and_close(state: Rc>, id: u32) { + let mut s = state.borrow_mut(); + let workers = s.borrow_mut::(); + if let Some(mut worker_thread) = workers.remove(&id) { + worker_thread.worker_handle.sender.close_channel(); + worker_thread + .join_handle + .join() + .expect("Worker thread panicked") + .expect("Panic in worker event loop"); + } +} + +/// Get message from guest worker as host +async fn op_host_get_message( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: WorkerArgs = serde_json::from_value(args)?; + let id = args.id as u32; + + let worker_handle = { + let s = state.borrow(); + let workers_table = s.borrow::(); + let maybe_handle = workers_table.get(&id); + if let Some(handle) = maybe_handle { + handle.worker_handle.clone() + } else { + // If handle was not found it means worker has already shutdown + return Ok(json!({ "type": "close" })); + } + }; + + let maybe_event = worker_handle.get_event().await?; + if let Some(event) = maybe_event { + // Terminal error means that worker should be removed from worker table. + if let WorkerEvent::TerminalError(_) = &event { + try_remove_and_close(state, id); + } + return Ok(serialize_worker_event(event)); + } + + // If there was no event from worker it means it has already been closed. + try_remove_and_close(state, id); + Ok(json!({ "type": "close" })) +} + +/// Post message to guest worker as host +fn op_host_post_message( + state: &mut OpState, + args: Value, + data: &mut [ZeroCopyBuf], +) -> Result { + assert_eq!(data.len(), 1, "Invalid number of arguments"); + let args: WorkerArgs = serde_json::from_value(args)?; + let id = args.id as u32; + let msg = Vec::from(&*data[0]).into_boxed_slice(); + + debug!("post message to worker {}", id); + let worker_thread = state + .borrow::() + .get(&id) + .expect("No worker handle found"); + worker_thread.worker_handle.post_message(msg)?; + Ok(json!({})) +} diff --git a/runtime/permissions.rs b/runtime/permissions.rs new file mode 100644 index 000000000..88f9c7179 --- /dev/null +++ b/runtime/permissions.rs @@ -0,0 +1,1108 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::colors; +use crate::fs_util::resolve_from_cwd; +use deno_core::error::custom_error; +use deno_core::error::uri_error; +use deno_core::error::AnyError; +use deno_core::url; +use deno_core::ModuleSpecifier; +use serde::Deserialize; +use std::collections::HashSet; +use std::env::current_dir; +use std::fmt; +use std::hash::Hash; +#[cfg(not(test))] +use std::io; +use std::path::{Path, PathBuf}; +#[cfg(test)] +use std::sync::atomic::AtomicBool; +#[cfg(test)] +use std::sync::atomic::Ordering; +#[cfg(test)] +use std::sync::Mutex; + +const PERMISSION_EMOJI: &str = "⚠️"; + +/// Tri-state value for storing permission state +#[derive(PartialEq, Debug, Clone, Copy, Deserialize)] +pub enum PermissionState { + Granted = 0, + Prompt = 1, + Denied = 2, +} + +impl PermissionState { + /// Check the permission state. + fn check(self, msg: &str, flag_name: &str) -> Result<(), AnyError> { + if self == PermissionState::Granted { + log_perm_access(msg); + return Ok(()); + } + let message = format!("{}, run again with the {} flag", msg, flag_name); + Err(custom_error("PermissionDenied", message)) + } +} + +impl fmt::Display for PermissionState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PermissionState::Granted => f.pad("granted"), + PermissionState::Prompt => f.pad("prompt"), + PermissionState::Denied => f.pad("denied"), + } + } +} + +impl Default for PermissionState { + fn default() -> Self { + PermissionState::Prompt + } +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub struct UnaryPermission { + pub global_state: PermissionState, + pub granted_list: HashSet, + pub denied_list: HashSet, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub struct Permissions { + pub read: UnaryPermission, + pub write: UnaryPermission, + pub net: UnaryPermission, + pub env: PermissionState, + pub run: PermissionState, + pub plugin: PermissionState, + pub hrtime: PermissionState, +} + +fn resolve_fs_allowlist(allowlist: &[PathBuf]) -> HashSet { + allowlist + .iter() + .map(|raw_path| resolve_from_cwd(Path::new(&raw_path)).unwrap()) + .collect() +} + +#[derive(Clone, Debug, PartialEq, Default)] +pub struct PermissionsOptions { + pub allow_env: bool, + pub allow_hrtime: bool, + pub allow_net: bool, + pub allow_plugin: bool, + pub allow_read: bool, + pub allow_run: bool, + pub allow_write: bool, + pub net_allowlist: Vec, + pub read_allowlist: Vec, + pub write_allowlist: Vec, +} + +impl Permissions { + pub fn from_options(opts: &PermissionsOptions) -> Self { + fn state_from_flag_bool(flag: bool) -> PermissionState { + if flag { + PermissionState::Granted + } else { + PermissionState::Prompt + } + } + Self { + read: UnaryPermission:: { + global_state: state_from_flag_bool(opts.allow_read), + granted_list: resolve_fs_allowlist(&opts.read_allowlist), + ..Default::default() + }, + write: UnaryPermission:: { + global_state: state_from_flag_bool(opts.allow_write), + granted_list: resolve_fs_allowlist(&opts.write_allowlist), + ..Default::default() + }, + net: UnaryPermission:: { + global_state: state_from_flag_bool(opts.allow_net), + granted_list: opts.net_allowlist.iter().cloned().collect(), + ..Default::default() + }, + env: state_from_flag_bool(opts.allow_env), + run: state_from_flag_bool(opts.allow_run), + plugin: state_from_flag_bool(opts.allow_plugin), + hrtime: state_from_flag_bool(opts.allow_hrtime), + } + } + + /// Arbitrary helper. Resolves the path from CWD, and also gets a path that + /// can be displayed without leaking the CWD when not allowed. + fn resolved_and_display_path(&self, path: &Path) -> (PathBuf, PathBuf) { + let resolved_path = resolve_from_cwd(path).unwrap(); + let display_path = if path.is_absolute() { + path.to_path_buf() + } else { + match self + .query_read(&Some(¤t_dir().unwrap())) + .check("", "") + { + Ok(_) => resolved_path.clone(), + Err(_) => path.to_path_buf(), + } + }; + (resolved_path, display_path) + } + + pub fn allow_all() -> Self { + Self { + read: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + write: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + net: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + env: PermissionState::Granted, + run: PermissionState::Granted, + plugin: PermissionState::Granted, + hrtime: PermissionState::Granted, + } + } + + pub fn query_read(&self, path: &Option<&Path>) -> PermissionState { + let path = path.map(|p| resolve_from_cwd(p).unwrap()); + if self.read.global_state == PermissionState::Denied + && match path.as_ref() { + None => true, + Some(path) => check_path_blocklist(path, &self.read.denied_list), + } + { + return PermissionState::Denied; + } + if self.read.global_state == PermissionState::Granted + || match path.as_ref() { + None => false, + Some(path) => check_path_allowlist(path, &self.read.granted_list), + } + { + return PermissionState::Granted; + } + PermissionState::Prompt + } + + pub fn query_write(&self, path: &Option<&Path>) -> PermissionState { + let path = path.map(|p| resolve_from_cwd(p).unwrap()); + if self.write.global_state == PermissionState::Denied + && match path.as_ref() { + None => true, + Some(path) => check_path_blocklist(path, &self.write.denied_list), + } + { + return PermissionState::Denied; + } + if self.write.global_state == PermissionState::Granted + || match path.as_ref() { + None => false, + Some(path) => check_path_allowlist(path, &self.write.granted_list), + } + { + return PermissionState::Granted; + } + PermissionState::Prompt + } + + pub fn query_net(&self, host: &str, port: Option) -> PermissionState { + if self.net.global_state == PermissionState::Denied + || check_host_and_port_list(host, port, &self.net.denied_list) + { + return PermissionState::Denied; + } + if self.net.global_state == PermissionState::Granted + || check_host_and_port_list(host, port, &self.net.granted_list) + { + return PermissionState::Granted; + } + PermissionState::Prompt + } + + pub fn query_net_url( + &self, + url: &Option<&str>, + ) -> Result { + if url.is_none() { + return Ok(self.net.global_state); + } + let url: &str = url.unwrap(); + // If url is invalid, then throw a TypeError. + let parsed = url::Url::parse(url)?; + // The url may be parsed correctly but still lack a host, i.e. "localhost:235" or "mailto:someone@somewhere.com" or "file:/1.txt" + // Note that host:port combos are parsed as scheme:path + if parsed.host().is_none() { + return Err(custom_error( + "URIError", + "invalid urlormat: ://[:port][/subpath]", + )); + } + Ok(self.query_net( + &parsed.host().unwrap().to_string(), + parsed.port_or_known_default(), + )) + } + + pub fn query_env(&self) -> PermissionState { + self.env + } + + pub fn query_run(&self) -> PermissionState { + self.run + } + + pub fn query_plugin(&self) -> PermissionState { + self.plugin + } + + pub fn query_hrtime(&self) -> PermissionState { + self.hrtime + } + + pub fn request_read(&mut self, path: &Option<&Path>) -> PermissionState { + if let Some(path) = path { + let (resolved_path, display_path) = self.resolved_and_display_path(path); + let state = self.query_read(&Some(&resolved_path)); + if state == PermissionState::Prompt { + if permission_prompt(&format!( + "Deno requests read access to \"{}\"", + display_path.display() + )) { + self + .read + .granted_list + .retain(|path| !path.starts_with(&resolved_path)); + self.read.granted_list.insert(resolved_path); + return PermissionState::Granted; + } else { + self + .read + .denied_list + .retain(|path| !resolved_path.starts_with(path)); + self.read.denied_list.insert(resolved_path); + self.read.global_state = PermissionState::Denied; + return PermissionState::Denied; + } + } + state + } else { + let state = self.query_read(&None); + if state == PermissionState::Prompt { + if permission_prompt("Deno requests read access") { + self.read.granted_list.clear(); + self.read.global_state = PermissionState::Granted; + return PermissionState::Granted; + } else { + self.read.global_state = PermissionState::Denied; + return PermissionState::Denied; + } + } + state + } + } + + pub fn request_write(&mut self, path: &Option<&Path>) -> PermissionState { + if let Some(path) = path { + let (resolved_path, display_path) = self.resolved_and_display_path(path); + let state = self.query_write(&Some(&resolved_path)); + if state == PermissionState::Prompt { + if permission_prompt(&format!( + "Deno requests write access to \"{}\"", + display_path.display() + )) { + self + .write + .granted_list + .retain(|path| !path.starts_with(&resolved_path)); + self.write.granted_list.insert(resolved_path); + return PermissionState::Granted; + } else { + self + .write + .denied_list + .retain(|path| !resolved_path.starts_with(path)); + self.write.denied_list.insert(resolved_path); + self.write.global_state = PermissionState::Denied; + return PermissionState::Denied; + } + } + state + } else { + let state = self.query_write(&None); + if state == PermissionState::Prompt { + if permission_prompt("Deno requests write access") { + self.write.granted_list.clear(); + self.write.global_state = PermissionState::Granted; + return PermissionState::Granted; + } else { + self.write.global_state = PermissionState::Denied; + return PermissionState::Denied; + } + } + state + } + } + + pub fn request_net( + &mut self, + url: &Option<&str>, + ) -> Result { + if let Some(url) = url { + let state = self.query_net_url(&Some(url))?; + if state == PermissionState::Prompt { + if permission_prompt(&format!( + "Deno requests network access to \"{}\"", + url + )) { + self.net.granted_list.insert(url.to_string()); + return Ok(PermissionState::Granted); + } else { + self.net.denied_list.insert(url.to_string()); + self.net.global_state = PermissionState::Denied; + return Ok(PermissionState::Denied); + } + } + Ok(state) + } else { + let state = self.query_net_url(&None)?; + if state == PermissionState::Prompt { + if permission_prompt("Deno requests network access") { + self.net.granted_list.clear(); + self.net.global_state = PermissionState::Granted; + return Ok(PermissionState::Granted); + } else { + self.net.global_state = PermissionState::Denied; + return Ok(PermissionState::Denied); + } + } + Ok(state) + } + } + + pub fn request_env(&mut self) -> PermissionState { + if self.env == PermissionState::Prompt { + if permission_prompt("Deno requests access to environment variables") { + self.env = PermissionState::Granted; + } else { + self.env = PermissionState::Denied; + } + } + self.env + } + + pub fn request_run(&mut self) -> PermissionState { + if self.run == PermissionState::Prompt { + if permission_prompt("Deno requests to access to run a subprocess") { + self.run = PermissionState::Granted; + } else { + self.run = PermissionState::Denied; + } + } + self.run + } + + pub fn request_plugin(&mut self) -> PermissionState { + if self.plugin == PermissionState::Prompt { + if permission_prompt("Deno requests to open plugins") { + self.plugin = PermissionState::Granted; + } else { + self.plugin = PermissionState::Denied; + } + } + self.plugin + } + + pub fn request_hrtime(&mut self) -> PermissionState { + if self.hrtime == PermissionState::Prompt { + if permission_prompt("Deno requests access to high precision time") { + self.hrtime = PermissionState::Granted; + } else { + self.hrtime = PermissionState::Denied; + } + } + self.hrtime + } + + pub fn revoke_read(&mut self, path: &Option<&Path>) -> PermissionState { + if let Some(path) = path { + let path = resolve_from_cwd(path).unwrap(); + self + .read + .granted_list + .retain(|path_| !path_.starts_with(&path)); + } else { + self.read.granted_list.clear(); + if self.read.global_state == PermissionState::Granted { + self.read.global_state = PermissionState::Prompt; + } + } + self.query_read(path) + } + + pub fn revoke_write(&mut self, path: &Option<&Path>) -> PermissionState { + if let Some(path) = path { + let path = resolve_from_cwd(path).unwrap(); + self + .write + .granted_list + .retain(|path_| !path_.starts_with(&path)); + } else { + self.write.granted_list.clear(); + if self.write.global_state == PermissionState::Granted { + self.write.global_state = PermissionState::Prompt; + } + } + self.query_write(path) + } + + pub fn revoke_net( + &mut self, + url: &Option<&str>, + ) -> Result { + if let Some(url) = url { + self.net.granted_list.remove(*url); + } else { + self.net.granted_list.clear(); + if self.net.global_state == PermissionState::Granted { + self.net.global_state = PermissionState::Prompt; + } + } + self.query_net_url(url) + } + + pub fn revoke_env(&mut self) -> PermissionState { + if self.env == PermissionState::Granted { + self.env = PermissionState::Prompt; + } + self.env + } + + pub fn revoke_run(&mut self) -> PermissionState { + if self.run == PermissionState::Granted { + self.run = PermissionState::Prompt; + } + self.run + } + + pub fn revoke_plugin(&mut self) -> PermissionState { + if self.plugin == PermissionState::Granted { + self.plugin = PermissionState::Prompt; + } + self.plugin + } + + pub fn revoke_hrtime(&mut self) -> PermissionState { + if self.hrtime == PermissionState::Granted { + self.hrtime = PermissionState::Prompt; + } + self.hrtime + } + + pub fn check_read(&self, path: &Path) -> Result<(), AnyError> { + let (resolved_path, display_path) = self.resolved_and_display_path(path); + self.query_read(&Some(&resolved_path)).check( + &format!("read access to \"{}\"", display_path.display()), + "--allow-read", + ) + } + + /// As `check_read()`, but permission error messages will anonymize the path + /// by replacing it with the given `display`. + pub fn check_read_blind( + &self, + path: &Path, + display: &str, + ) -> Result<(), AnyError> { + let resolved_path = resolve_from_cwd(path).unwrap(); + self + .query_read(&Some(&resolved_path)) + .check(&format!("read access to <{}>", display), "--allow-read") + } + + pub fn check_write(&self, path: &Path) -> Result<(), AnyError> { + let (resolved_path, display_path) = self.resolved_and_display_path(path); + self.query_write(&Some(&resolved_path)).check( + &format!("write access to \"{}\"", display_path.display()), + "--allow-write", + ) + } + + pub fn check_net(&self, hostname: &str, port: u16) -> Result<(), AnyError> { + self.query_net(hostname, Some(port)).check( + &format!("network access to \"{}:{}\"", hostname, port), + "--allow-net", + ) + } + + pub fn check_net_url(&self, url: &url::Url) -> Result<(), AnyError> { + let host = url.host_str().ok_or_else(|| uri_error("missing host"))?; + self + .query_net(host, url.port_or_known_default()) + .check(&format!("network access to \"{}\"", url), "--allow-net") + } + + /// A helper function that determines if the module specifier is a local or + /// remote, and performs a read or net check for the specifier. + pub fn check_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> Result<(), AnyError> { + let url = specifier.as_url(); + if url.scheme() == "file" { + let path = url.to_file_path().unwrap(); + self.check_read(&path) + } else { + self.check_net_url(url) + } + } + + pub fn check_env(&self) -> Result<(), AnyError> { + self + .env + .check("access to environment variables", "--allow-env") + } + + pub fn check_run(&self) -> Result<(), AnyError> { + self.run.check("access to run a subprocess", "--allow-run") + } + + pub fn check_plugin(&self, path: &Path) -> Result<(), AnyError> { + let (_, display_path) = self.resolved_and_display_path(path); + self.plugin.check( + &format!("access to open a plugin: {}", display_path.display()), + "--allow-plugin", + ) + } + + pub fn check_hrtime(&self) -> Result<(), AnyError> { + self + .hrtime + .check("access to high precision time", "--allow-hrtime") + } +} + +impl deno_fetch::FetchPermissions for Permissions { + fn check_net_url(&self, url: &url::Url) -> Result<(), AnyError> { + Permissions::check_net_url(self, url) + } + + fn check_read(&self, p: &PathBuf) -> Result<(), AnyError> { + Permissions::check_read(self, p) + } +} + +/// Shows the permission prompt and returns the answer according to the user input. +/// This loops until the user gives the proper input. +#[cfg(not(test))] +fn permission_prompt(message: &str) -> bool { + if !atty::is(atty::Stream::Stdin) || !atty::is(atty::Stream::Stderr) { + return false; + }; + let msg = format!( + "️{} {}. Grant? [g/d (g = grant, d = deny)] ", + PERMISSION_EMOJI, message + ); + // print to stderr so that if deno is > to a file this is still displayed. + eprint!("{}", colors::bold(&msg)); + loop { + let mut input = String::new(); + let stdin = io::stdin(); + let result = stdin.read_line(&mut input); + if result.is_err() { + return false; + }; + let ch = input.chars().next().unwrap(); + match ch.to_ascii_lowercase() { + 'g' => return true, + 'd' => return false, + _ => { + // If we don't get a recognized option try again. + let msg_again = + format!("Unrecognized option '{}' [g/d (g = grant, d = deny)] ", ch); + eprint!("{}", colors::bold(&msg_again)); + } + }; + } +} + +#[cfg(test)] +lazy_static! { + /// Lock this when you use `set_prompt_result` in a test case. + static ref PERMISSION_PROMPT_GUARD: Mutex<()> = Mutex::new(()); +} + +#[cfg(test)] +static STUB_PROMPT_VALUE: AtomicBool = AtomicBool::new(true); + +#[cfg(test)] +fn set_prompt_result(value: bool) { + STUB_PROMPT_VALUE.store(value, Ordering::SeqCst); +} + +// When testing, permission prompt returns the value of STUB_PROMPT_VALUE +// which we set from the test functions. +#[cfg(test)] +fn permission_prompt(_message: &str) -> bool { + STUB_PROMPT_VALUE.load(Ordering::SeqCst) +} + +fn log_perm_access(message: &str) { + debug!( + "{}", + colors::bold(&format!("{}️ Granted {}", PERMISSION_EMOJI, message)) + ); +} + +fn check_path_allowlist(path: &Path, allowlist: &HashSet) -> bool { + for path_ in allowlist { + if path.starts_with(path_) { + return true; + } + } + false +} + +fn check_path_blocklist(path: &Path, blocklist: &HashSet) -> bool { + for path_ in blocklist { + if path_.starts_with(path) { + return true; + } + } + false +} + +fn check_host_and_port_list( + host: &str, + port: Option, + allowlist: &HashSet, +) -> bool { + allowlist.contains(host) + || (port.is_some() + && allowlist.contains(&format!("{}:{}", host, port.unwrap()))) +} + +#[cfg(test)] +mod tests { + use super::*; + use deno_core::serde_json; + + // Creates vector of strings, Vec + macro_rules! svec { + ($($x:expr),*) => (vec![$($x.to_string()),*]); + } + + #[test] + fn check_paths() { + let allowlist = vec![ + PathBuf::from("/a/specific/dir/name"), + PathBuf::from("/a/specific"), + PathBuf::from("/b/c"), + ]; + + let perms = Permissions::from_options(&PermissionsOptions { + read_allowlist: allowlist.clone(), + write_allowlist: allowlist, + ..Default::default() + }); + + // Inside of /a/specific and /a/specific/dir/name + assert!(perms.check_read(Path::new("/a/specific/dir/name")).is_ok()); + assert!(perms.check_write(Path::new("/a/specific/dir/name")).is_ok()); + + // Inside of /a/specific but outside of /a/specific/dir/name + assert!(perms.check_read(Path::new("/a/specific/dir")).is_ok()); + assert!(perms.check_write(Path::new("/a/specific/dir")).is_ok()); + + // Inside of /a/specific and /a/specific/dir/name + assert!(perms + .check_read(Path::new("/a/specific/dir/name/inner")) + .is_ok()); + assert!(perms + .check_write(Path::new("/a/specific/dir/name/inner")) + .is_ok()); + + // Inside of /a/specific but outside of /a/specific/dir/name + assert!(perms.check_read(Path::new("/a/specific/other/dir")).is_ok()); + assert!(perms + .check_write(Path::new("/a/specific/other/dir")) + .is_ok()); + + // Exact match with /b/c + assert!(perms.check_read(Path::new("/b/c")).is_ok()); + assert!(perms.check_write(Path::new("/b/c")).is_ok()); + + // Sub path within /b/c + assert!(perms.check_read(Path::new("/b/c/sub/path")).is_ok()); + assert!(perms.check_write(Path::new("/b/c/sub/path")).is_ok()); + + // Sub path within /b/c, needs normalizing + assert!(perms + .check_read(Path::new("/b/c/sub/path/../path/.")) + .is_ok()); + assert!(perms + .check_write(Path::new("/b/c/sub/path/../path/.")) + .is_ok()); + + // Inside of /b but outside of /b/c + assert!(perms.check_read(Path::new("/b/e")).is_err()); + assert!(perms.check_write(Path::new("/b/e")).is_err()); + + // Inside of /a but outside of /a/specific + assert!(perms.check_read(Path::new("/a/b")).is_err()); + assert!(perms.check_write(Path::new("/a/b")).is_err()); + } + + #[test] + fn test_check_net() { + let perms = Permissions::from_options(&PermissionsOptions { + net_allowlist: svec![ + "localhost", + "deno.land", + "github.com:3000", + "127.0.0.1", + "172.16.0.2:8000", + "www.github.com:443" + ], + ..Default::default() + }); + + let domain_tests = vec![ + ("localhost", 1234, true), + ("deno.land", 0, true), + ("deno.land", 3000, true), + ("deno.lands", 0, false), + ("deno.lands", 3000, false), + ("github.com", 3000, true), + ("github.com", 0, false), + ("github.com", 2000, false), + ("github.net", 3000, false), + ("127.0.0.1", 0, true), + ("127.0.0.1", 3000, true), + ("127.0.0.2", 0, false), + ("127.0.0.2", 3000, false), + ("172.16.0.2", 8000, true), + ("172.16.0.2", 0, false), + ("172.16.0.2", 6000, false), + ("172.16.0.1", 8000, false), + // Just some random hosts that should err + ("somedomain", 0, false), + ("192.168.0.1", 0, false), + ]; + + let url_tests = vec![ + // Any protocol + port for localhost should be ok, since we don't specify + ("http://localhost", true), + ("https://localhost", true), + ("https://localhost:4443", true), + ("tcp://localhost:5000", true), + ("udp://localhost:6000", true), + // Correct domain + any port and protocol should be ok incorrect shouldn't + ("https://deno.land/std/example/welcome.ts", true), + ("https://deno.land:3000/std/example/welcome.ts", true), + ("https://deno.lands/std/example/welcome.ts", false), + ("https://deno.lands:3000/std/example/welcome.ts", false), + // Correct domain + port should be ok all other combinations should err + ("https://github.com:3000/denoland/deno", true), + ("https://github.com/denoland/deno", false), + ("https://github.com:2000/denoland/deno", false), + ("https://github.net:3000/denoland/deno", false), + // Correct ipv4 address + any port should be ok others should err + ("tcp://127.0.0.1", true), + ("https://127.0.0.1", true), + ("tcp://127.0.0.1:3000", true), + ("https://127.0.0.1:3000", true), + ("tcp://127.0.0.2", false), + ("https://127.0.0.2", false), + ("tcp://127.0.0.2:3000", false), + ("https://127.0.0.2:3000", false), + // Correct address + port should be ok all other combinations should err + ("tcp://172.16.0.2:8000", true), + ("https://172.16.0.2:8000", true), + ("tcp://172.16.0.2", false), + ("https://172.16.0.2", false), + ("tcp://172.16.0.2:6000", false), + ("https://172.16.0.2:6000", false), + ("tcp://172.16.0.1:8000", false), + ("https://172.16.0.1:8000", false), + // Testing issue #6531 (Network permissions check doesn't account for well-known default ports) so we dont regress + ("https://www.github.com:443/robots.txt", true), + ]; + + for (url_str, is_ok) in url_tests.iter() { + let u = url::Url::parse(url_str).unwrap(); + assert_eq!(*is_ok, perms.check_net_url(&u).is_ok()); + } + + for (host, port, is_ok) in domain_tests.iter() { + assert_eq!(*is_ok, perms.check_net(host, *port).is_ok()); + } + } + + #[test] + fn check_specifiers() { + let read_allowlist = if cfg!(target_os = "windows") { + vec![PathBuf::from("C:\\a")] + } else { + vec![PathBuf::from("/a")] + }; + let perms = Permissions::from_options(&PermissionsOptions { + read_allowlist, + net_allowlist: svec!["localhost"], + ..Default::default() + }); + + let mut fixtures = vec![ + ( + ModuleSpecifier::resolve_url_or_path("http://localhost:4545/mod.ts") + .unwrap(), + true, + ), + ( + ModuleSpecifier::resolve_url_or_path("http://deno.land/x/mod.ts") + .unwrap(), + false, + ), + ]; + + if cfg!(target_os = "windows") { + fixtures.push(( + ModuleSpecifier::resolve_url_or_path("file:///C:/a/mod.ts").unwrap(), + true, + )); + fixtures.push(( + ModuleSpecifier::resolve_url_or_path("file:///C:/b/mod.ts").unwrap(), + false, + )); + } else { + fixtures.push(( + ModuleSpecifier::resolve_url_or_path("file:///a/mod.ts").unwrap(), + true, + )); + fixtures.push(( + ModuleSpecifier::resolve_url_or_path("file:///b/mod.ts").unwrap(), + false, + )); + } + + for (specifier, expected) in fixtures { + assert_eq!(perms.check_specifier(&specifier).is_ok(), expected); + } + } + + #[test] + fn test_deserialize_perms() { + let json_perms = r#" + { + "read": { + "global_state": "Granted", + "granted_list": [], + "denied_list": [] + }, + "write": { + "global_state": "Granted", + "granted_list": [], + "denied_list": [] + }, + "net": { + "global_state": "Granted", + "granted_list": [], + "denied_list": [] + }, + "env": "Granted", + "run": "Granted", + "plugin": "Granted", + "hrtime": "Granted" + } + "#; + let perms0 = Permissions { + read: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + write: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + net: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + env: PermissionState::Granted, + run: PermissionState::Granted, + hrtime: PermissionState::Granted, + plugin: PermissionState::Granted, + }; + let deserialized_perms: Permissions = + serde_json::from_str(json_perms).unwrap(); + assert_eq!(perms0, deserialized_perms); + } + + #[test] + fn test_query() { + let perms1 = Permissions { + read: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + write: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + net: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + env: PermissionState::Granted, + run: PermissionState::Granted, + plugin: PermissionState::Granted, + hrtime: PermissionState::Granted, + }; + let perms2 = Permissions { + read: UnaryPermission { + global_state: PermissionState::Prompt, + granted_list: resolve_fs_allowlist(&[PathBuf::from("/foo")]), + ..Default::default() + }, + write: UnaryPermission { + global_state: PermissionState::Prompt, + granted_list: resolve_fs_allowlist(&[PathBuf::from("/foo")]), + ..Default::default() + }, + net: UnaryPermission { + global_state: PermissionState::Prompt, + granted_list: ["127.0.0.1:8000".to_string()].iter().cloned().collect(), + ..Default::default() + }, + env: PermissionState::Prompt, + run: PermissionState::Prompt, + plugin: PermissionState::Prompt, + hrtime: PermissionState::Prompt, + }; + #[rustfmt::skip] + { + assert_eq!(perms1.query_read(&None), PermissionState::Granted); + assert_eq!(perms1.query_read(&Some(&Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms2.query_read(&None), PermissionState::Prompt); + assert_eq!(perms2.query_read(&Some(&Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms2.query_read(&Some(&Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms1.query_write(&None), PermissionState::Granted); + assert_eq!(perms1.query_write(&Some(&Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms2.query_write(&None), PermissionState::Prompt); + assert_eq!(perms2.query_write(&Some(&Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms2.query_write(&Some(&Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms1.query_net_url(&None).unwrap(), PermissionState::Granted); + assert_eq!(perms1.query_net_url(&Some("http://127.0.0.1:8000")).unwrap(), PermissionState::Granted); + assert_eq!(perms2.query_net_url(&None).unwrap(), PermissionState::Prompt); + assert_eq!(perms2.query_net_url(&Some("http://127.0.0.1:8000")).unwrap(), PermissionState::Granted); + assert_eq!(perms1.query_env(), PermissionState::Granted); + assert_eq!(perms2.query_env(), PermissionState::Prompt); + assert_eq!(perms1.query_run(), PermissionState::Granted); + assert_eq!(perms2.query_run(), PermissionState::Prompt); + assert_eq!(perms1.query_plugin(), PermissionState::Granted); + assert_eq!(perms2.query_plugin(), PermissionState::Prompt); + assert_eq!(perms1.query_hrtime(), PermissionState::Granted); + assert_eq!(perms2.query_hrtime(), PermissionState::Prompt); + }; + } + + #[test] + fn test_request() { + let mut perms = Permissions { + read: UnaryPermission { + global_state: PermissionState::Prompt, + ..Default::default() + }, + write: UnaryPermission { + global_state: PermissionState::Prompt, + ..Default::default() + }, + net: UnaryPermission { + global_state: PermissionState::Prompt, + ..Default::default() + }, + env: PermissionState::Prompt, + run: PermissionState::Prompt, + plugin: PermissionState::Prompt, + hrtime: PermissionState::Prompt, + }; + #[rustfmt::skip] + { + let _guard = PERMISSION_PROMPT_GUARD.lock().unwrap(); + set_prompt_result(true); + assert_eq!(perms.request_read(&Some(&Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms.query_read(&None), PermissionState::Prompt); + set_prompt_result(false); + assert_eq!(perms.request_read(&Some(&Path::new("/foo/bar"))), PermissionState::Granted); + set_prompt_result(false); + assert_eq!(perms.request_write(&Some(&Path::new("/foo"))), PermissionState::Denied); + assert_eq!(perms.query_write(&Some(&Path::new("/foo/bar"))), PermissionState::Prompt); + set_prompt_result(true); + assert_eq!(perms.request_write(&None), PermissionState::Denied); + set_prompt_result(true); + assert_eq!(perms.request_net(&None).unwrap(), PermissionState::Granted); + set_prompt_result(false); + assert_eq!(perms.request_net(&Some("http://127.0.0.1:8000")).unwrap(), PermissionState::Granted); + set_prompt_result(true); + assert_eq!(perms.request_env(), PermissionState::Granted); + set_prompt_result(false); + assert_eq!(perms.request_env(), PermissionState::Granted); + set_prompt_result(false); + assert_eq!(perms.request_run(), PermissionState::Denied); + set_prompt_result(true); + assert_eq!(perms.request_run(), PermissionState::Denied); + set_prompt_result(true); + assert_eq!(perms.request_plugin(), PermissionState::Granted); + set_prompt_result(false); + assert_eq!(perms.request_plugin(), PermissionState::Granted); + set_prompt_result(false); + assert_eq!(perms.request_hrtime(), PermissionState::Denied); + set_prompt_result(true); + assert_eq!(perms.request_hrtime(), PermissionState::Denied); + }; + } + + #[test] + fn test_revoke() { + let mut perms = Permissions { + read: UnaryPermission { + global_state: PermissionState::Prompt, + granted_list: resolve_fs_allowlist(&[PathBuf::from("/foo")]), + ..Default::default() + }, + write: UnaryPermission { + global_state: PermissionState::Prompt, + granted_list: resolve_fs_allowlist(&[PathBuf::from("/foo")]), + ..Default::default() + }, + net: UnaryPermission { + global_state: PermissionState::Denied, + ..Default::default() + }, + env: PermissionState::Granted, + run: PermissionState::Granted, + plugin: PermissionState::Prompt, + hrtime: PermissionState::Denied, + }; + #[rustfmt::skip] + { + assert_eq!(perms.revoke_read(&Some(&Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms.revoke_read(&Some(&Path::new("/foo"))), PermissionState::Prompt); + assert_eq!(perms.query_read(&Some(&Path::new("/foo/bar"))), PermissionState::Prompt); + assert_eq!(perms.revoke_write(&Some(&Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms.revoke_write(&None), PermissionState::Prompt); + assert_eq!(perms.query_write(&Some(&Path::new("/foo/bar"))), PermissionState::Prompt); + assert_eq!(perms.revoke_net(&None).unwrap(), PermissionState::Denied); + assert_eq!(perms.revoke_env(), PermissionState::Prompt); + assert_eq!(perms.revoke_run(), PermissionState::Prompt); + assert_eq!(perms.revoke_plugin(), PermissionState::Prompt); + assert_eq!(perms.revoke_hrtime(), PermissionState::Denied); + }; + } +} diff --git a/runtime/resolve_addr.rs b/runtime/resolve_addr.rs new file mode 100644 index 000000000..c3dc52f8f --- /dev/null +++ b/runtime/resolve_addr.rs @@ -0,0 +1,72 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::AnyError; +use std::net::SocketAddr; +use std::net::ToSocketAddrs; + +/// Resolve network address. Returns a future. +pub fn resolve_addr(hostname: &str, port: u16) -> Result { + // Default to localhost if given just the port. Example: ":80" + let addr: &str = if !hostname.is_empty() { + &hostname + } else { + "0.0.0.0" + }; + + // If this looks like an ipv6 IP address. Example: "[2001:db8::1]" + // Then we remove the brackets. + let addr = if addr.starts_with('[') && addr.ends_with(']') { + let l = addr.len() - 1; + addr.get(1..l).unwrap() + } else { + addr + }; + let addr_port_pair = (addr, port); + let mut iter = addr_port_pair.to_socket_addrs()?; + Ok(iter.next().unwrap()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::Ipv4Addr; + use std::net::Ipv6Addr; + use std::net::SocketAddrV4; + use std::net::SocketAddrV6; + + #[test] + fn resolve_addr1() { + let expected = + SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 80)); + let actual = resolve_addr("127.0.0.1", 80).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn resolve_addr2() { + let expected = + SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 80)); + let actual = resolve_addr("", 80).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn resolve_addr3() { + let expected = + SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 0, 2, 1), 25)); + let actual = resolve_addr("192.0.2.1", 25).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn resolve_addr_ipv6() { + let expected = SocketAddr::V6(SocketAddrV6::new( + Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1), + 8080, + 0, + 0, + )); + let actual = resolve_addr("[2001:db8::1]", 8080).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/runtime/rt/00_bootstrap_namespace.js b/runtime/rt/00_bootstrap_namespace.js new file mode 100644 index 000000000..514cbe3f0 --- /dev/null +++ b/runtime/rt/00_bootstrap_namespace.js @@ -0,0 +1,9 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// The only purpose of this file is to set up "globalThis.__bootstrap" namespace, +// that is used by scripts in this directory to reference exports between +// the files. + +// This namespace is removed during runtime bootstrapping process. + +globalThis.__bootstrap = globalThis.__bootstrap || {}; diff --git a/runtime/rt/01_build.js b/runtime/rt/01_build.js new file mode 100644 index 000000000..7c1dc817e --- /dev/null +++ b/runtime/rt/01_build.js @@ -0,0 +1,26 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const build = { + target: "unknown", + arch: "unknown", + os: "unknown", + vendor: "unknown", + env: undefined, + }; + + function setBuildInfo(target) { + const [arch, vendor, os, env] = target.split("-", 4); + build.target = target; + build.arch = arch; + build.vendor = vendor; + build.os = os; + build.env = env; + Object.freeze(build); + } + + window.__bootstrap.build = { + build, + setBuildInfo, + }; +})(this); diff --git a/runtime/rt/01_colors.js b/runtime/rt/01_colors.js new file mode 100644 index 000000000..39e4a7a18 --- /dev/null +++ b/runtime/rt/01_colors.js @@ -0,0 +1,92 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + function code(open, close) { + return { + open: `\x1b[${open}m`, + close: `\x1b[${close}m`, + regexp: new RegExp(`\\x1b\\[${close}m`, "g"), + }; + } + + function run(str, code) { + return `${code.open}${str.replace(code.regexp, code.open)}${code.close}`; + } + + function bold(str) { + return run(str, code(1, 22)); + } + + function italic(str) { + return run(str, code(3, 23)); + } + + function yellow(str) { + return run(str, code(33, 39)); + } + + function cyan(str) { + return run(str, code(36, 39)); + } + + function red(str) { + return run(str, code(31, 39)); + } + + function green(str) { + return run(str, code(32, 39)); + } + + function bgRed(str) { + return run(str, code(41, 49)); + } + + function white(str) { + return run(str, code(37, 39)); + } + + function gray(str) { + return run(str, code(90, 39)); + } + + function magenta(str) { + return run(str, code(35, 39)); + } + + function dim(str) { + return run(str, code(2, 22)); + } + + // https://github.com/chalk/ansi-regex/blob/2b56fb0c7a07108e5b54241e8faec160d393aedb/index.js + const ANSI_PATTERN = new RegExp( + [ + "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", + "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", + ].join("|"), + "g", + ); + + function stripColor(string) { + return string.replace(ANSI_PATTERN, ""); + } + + function maybeColor(fn) { + return !(globalThis.Deno?.noColor ?? false) ? fn : (s) => s; + } + + window.__bootstrap.colors = { + bold, + italic, + yellow, + cyan, + red, + green, + bgRed, + white, + gray, + magenta, + dim, + stripColor, + maybeColor, + }; +})(this); diff --git a/runtime/rt/01_errors.js b/runtime/rt/01_errors.js new file mode 100644 index 000000000..d5933069b --- /dev/null +++ b/runtime/rt/01_errors.js @@ -0,0 +1,162 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + class NotFound extends Error { + constructor(msg) { + super(msg); + this.name = "NotFound"; + } + } + + class PermissionDenied extends Error { + constructor(msg) { + super(msg); + this.name = "PermissionDenied"; + } + } + + class ConnectionRefused extends Error { + constructor(msg) { + super(msg); + this.name = "ConnectionRefused"; + } + } + + class ConnectionReset extends Error { + constructor(msg) { + super(msg); + this.name = "ConnectionReset"; + } + } + + class ConnectionAborted extends Error { + constructor(msg) { + super(msg); + this.name = "ConnectionAborted"; + } + } + + class NotConnected extends Error { + constructor(msg) { + super(msg); + this.name = "NotConnected"; + } + } + + class AddrInUse extends Error { + constructor(msg) { + super(msg); + this.name = "AddrInUse"; + } + } + + class AddrNotAvailable extends Error { + constructor(msg) { + super(msg); + this.name = "AddrNotAvailable"; + } + } + + class BrokenPipe extends Error { + constructor(msg) { + super(msg); + this.name = "BrokenPipe"; + } + } + + class AlreadyExists extends Error { + constructor(msg) { + super(msg); + this.name = "AlreadyExists"; + } + } + + class InvalidData extends Error { + constructor(msg) { + super(msg); + this.name = "InvalidData"; + } + } + + class TimedOut extends Error { + constructor(msg) { + super(msg); + this.name = "TimedOut"; + } + } + + class Interrupted extends Error { + constructor(msg) { + super(msg); + this.name = "Interrupted"; + } + } + + class WriteZero extends Error { + constructor(msg) { + super(msg); + this.name = "WriteZero"; + } + } + + class UnexpectedEof extends Error { + constructor(msg) { + super(msg); + this.name = "UnexpectedEof"; + } + } + + class BadResource extends Error { + constructor(msg) { + super(msg); + this.name = "BadResource"; + } + } + + class Http extends Error { + constructor(msg) { + super(msg); + this.name = "Http"; + } + } + + class Busy extends Error { + constructor(msg) { + super(msg); + this.name = "Busy"; + } + } + + class NotSupported extends Error { + constructor(msg) { + super(msg); + this.name = "NotSupported"; + } + } + + const errors = { + NotFound, + PermissionDenied, + ConnectionRefused, + ConnectionReset, + ConnectionAborted, + NotConnected, + AddrInUse, + AddrNotAvailable, + BrokenPipe, + AlreadyExists, + InvalidData, + TimedOut, + Interrupted, + WriteZero, + UnexpectedEof, + BadResource, + Http, + Busy, + NotSupported, + }; + + window.__bootstrap.errors = { + errors, + }; +})(this); diff --git a/runtime/rt/01_internals.js b/runtime/rt/01_internals.js new file mode 100644 index 000000000..eee9eeaf7 --- /dev/null +++ b/runtime/rt/01_internals.js @@ -0,0 +1,23 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const internalSymbol = Symbol("Deno.internal"); + + // The object where all the internal fields for testing will be living. + const internalObject = {}; + + // Register a field to internalObject for test access, + // through Deno[Deno.internal][name]. + function exposeForTest(name, value) { + Object.defineProperty(internalObject, name, { + value, + enumerable: false, + }); + } + + window.__bootstrap.internals = { + internalSymbol, + internalObject, + exposeForTest, + }; +})(this); diff --git a/runtime/rt/01_version.js b/runtime/rt/01_version.js new file mode 100644 index 000000000..325e1156f --- /dev/null +++ b/runtime/rt/01_version.js @@ -0,0 +1,26 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const version = { + deno: "", + v8: "", + typescript: "", + }; + + function setVersions( + denoVersion, + v8Version, + tsVersion, + ) { + version.deno = denoVersion; + version.v8 = v8Version; + version.typescript = tsVersion; + + Object.freeze(version); + } + + window.__bootstrap.version = { + version, + setVersions, + }; +})(this); diff --git a/runtime/rt/01_web_util.js b/runtime/rt/01_web_util.js new file mode 100644 index 000000000..a9573a71d --- /dev/null +++ b/runtime/rt/01_web_util.js @@ -0,0 +1,160 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const illegalConstructorKey = Symbol("illegalConstructorKey"); + + function requiredArguments( + name, + length, + required, + ) { + if (length < required) { + const errMsg = `${name} requires at least ${required} argument${ + required === 1 ? "" : "s" + }, but only ${length} present`; + throw new TypeError(errMsg); + } + } + + const objectCloneMemo = new WeakMap(); + + function cloneArrayBuffer( + srcBuffer, + srcByteOffset, + srcLength, + _cloneConstructor, + ) { + // this function fudges the return type but SharedArrayBuffer is disabled for a while anyway + return srcBuffer.slice( + srcByteOffset, + srcByteOffset + srcLength, + ); + } + + /** Clone a value in a similar way to structured cloning. It is similar to a + * StructureDeserialize(StructuredSerialize(...)). */ + function cloneValue(value) { + switch (typeof value) { + case "number": + case "string": + case "boolean": + case "undefined": + case "bigint": + return value; + case "object": { + if (objectCloneMemo.has(value)) { + return objectCloneMemo.get(value); + } + if (value === null) { + return value; + } + if (value instanceof Date) { + return new Date(value.valueOf()); + } + if (value instanceof RegExp) { + return new RegExp(value); + } + if (value instanceof SharedArrayBuffer) { + return value; + } + if (value instanceof ArrayBuffer) { + const cloned = cloneArrayBuffer( + value, + 0, + value.byteLength, + ArrayBuffer, + ); + objectCloneMemo.set(value, cloned); + return cloned; + } + if (ArrayBuffer.isView(value)) { + const clonedBuffer = cloneValue(value.buffer); + // Use DataViewConstructor type purely for type-checking, can be a + // DataView or TypedArray. They use the same constructor signature, + // only DataView has a length in bytes and TypedArrays use a length in + // terms of elements, so we adjust for that. + let length; + if (value instanceof DataView) { + length = value.byteLength; + } else { + length = value.length; + } + return new (value.constructor)( + clonedBuffer, + value.byteOffset, + length, + ); + } + if (value instanceof Map) { + const clonedMap = new Map(); + objectCloneMemo.set(value, clonedMap); + value.forEach((v, k) => { + clonedMap.set(cloneValue(k), cloneValue(v)); + }); + return clonedMap; + } + if (value instanceof Set) { + // assumes that cloneValue still takes only one argument + const clonedSet = new Set([...value].map(cloneValue)); + objectCloneMemo.set(value, clonedSet); + return clonedSet; + } + + // default for objects + const clonedObj = {}; + objectCloneMemo.set(value, clonedObj); + const sourceKeys = Object.getOwnPropertyNames(value); + for (const key of sourceKeys) { + clonedObj[key] = cloneValue(value[key]); + } + Reflect.setPrototypeOf(clonedObj, Reflect.getPrototypeOf(value)); + return clonedObj; + } + case "symbol": + case "function": + default: + throw new DOMException("Uncloneable value in stream", "DataCloneError"); + } + } + + const handlerSymbol = Symbol("eventHandlers"); + function makeWrappedHandler(handler) { + function wrappedHandler(...args) { + if (typeof wrappedHandler.handler !== "function") { + return; + } + return wrappedHandler.handler.call(this, ...args); + } + wrappedHandler.handler = handler; + return wrappedHandler; + } + function defineEventHandler(emitter, name, defaultValue = undefined) { + // HTML specification section 8.1.5.1 + Object.defineProperty(emitter, `on${name}`, { + get() { + return this[handlerSymbol]?.get(name)?.handler ?? defaultValue; + }, + set(value) { + if (!this[handlerSymbol]) { + this[handlerSymbol] = new Map(); + } + let handlerWrapper = this[handlerSymbol]?.get(name); + if (handlerWrapper) { + handlerWrapper.handler = value; + } else { + handlerWrapper = makeWrappedHandler(value); + this.addEventListener(name, handlerWrapper); + } + this[handlerSymbol].set(name, handlerWrapper); + }, + configurable: true, + enumerable: true, + }); + } + window.__bootstrap.webUtil = { + illegalConstructorKey, + requiredArguments, + defineEventHandler, + cloneValue, + }; +})(this); diff --git a/runtime/rt/02_console.js b/runtime/rt/02_console.js new file mode 100644 index 000000000..971837bd6 --- /dev/null +++ b/runtime/rt/02_console.js @@ -0,0 +1,1732 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + const exposeForTest = window.__bootstrap.internals.exposeForTest; + const colors = window.__bootstrap.colors; + + function isInvalidDate(x) { + return isNaN(x.getTime()); + } + + function hasOwnProperty(obj, v) { + if (obj == null) { + return false; + } + return Object.prototype.hasOwnProperty.call(obj, v); + } + + // Copyright Joyent, Inc. and other Node contributors. MIT license. + // Forked from Node's lib/internal/cli_table.js + + function isTypedArray(x) { + return ArrayBuffer.isView(x) && !(x instanceof DataView); + } + + const tableChars = { + middleMiddle: "─", + rowMiddle: "┼", + topRight: "┐", + topLeft: "┌", + leftMiddle: "├", + topMiddle: "┬", + bottomRight: "┘", + bottomLeft: "└", + bottomMiddle: "┴", + rightMiddle: "┤", + left: "│ ", + right: " │", + middle: " │ ", + }; + + function isFullWidthCodePoint(code) { + // Code points are partially derived from: + // http://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt + return ( + code >= 0x1100 && + (code <= 0x115f || // Hangul Jamo + code === 0x2329 || // LEFT-POINTING ANGLE BRACKET + code === 0x232a || // RIGHT-POINTING ANGLE BRACKET + // CJK Radicals Supplement .. Enclosed CJK Letters and Months + (code >= 0x2e80 && code <= 0x3247 && code !== 0x303f) || + // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A + (code >= 0x3250 && code <= 0x4dbf) || + // CJK Unified Ideographs .. Yi Radicals + (code >= 0x4e00 && code <= 0xa4c6) || + // Hangul Jamo Extended-A + (code >= 0xa960 && code <= 0xa97c) || + // Hangul Syllables + (code >= 0xac00 && code <= 0xd7a3) || + // CJK Compatibility Ideographs + (code >= 0xf900 && code <= 0xfaff) || + // Vertical Forms + (code >= 0xfe10 && code <= 0xfe19) || + // CJK Compatibility Forms .. Small Form Variants + (code >= 0xfe30 && code <= 0xfe6b) || + // Halfwidth and Fullwidth Forms + (code >= 0xff01 && code <= 0xff60) || + (code >= 0xffe0 && code <= 0xffe6) || + // Kana Supplement + (code >= 0x1b000 && code <= 0x1b001) || + // Enclosed Ideographic Supplement + (code >= 0x1f200 && code <= 0x1f251) || + // Miscellaneous Symbols and Pictographs 0x1f300 - 0x1f5ff + // Emoticons 0x1f600 - 0x1f64f + (code >= 0x1f300 && code <= 0x1f64f) || + // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane + (code >= 0x20000 && code <= 0x3fffd)) + ); + } + + function getStringWidth(str) { + str = colors.stripColor(str).normalize("NFC"); + let width = 0; + + for (const ch of str) { + width += isFullWidthCodePoint(ch.codePointAt(0)) ? 2 : 1; + } + + return width; + } + + function renderRow(row, columnWidths) { + let out = tableChars.left; + for (let i = 0; i < row.length; i++) { + const cell = row[i]; + const len = getStringWidth(cell); + const needed = (columnWidths[i] - len) / 2; + // round(needed) + ceil(needed) will always add up to the amount + // of spaces we need while also left justifying the output. + out += `${" ".repeat(needed)}${cell}${" ".repeat(Math.ceil(needed))}`; + if (i !== row.length - 1) { + out += tableChars.middle; + } + } + out += tableChars.right; + return out; + } + + function cliTable(head, columns) { + const rows = []; + const columnWidths = head.map((h) => getStringWidth(h)); + const longestColumn = columns.reduce( + (n, a) => Math.max(n, a.length), + 0, + ); + + for (let i = 0; i < head.length; i++) { + const column = columns[i]; + for (let j = 0; j < longestColumn; j++) { + if (rows[j] === undefined) { + rows[j] = []; + } + const value = (rows[j][i] = hasOwnProperty(column, j) ? column[j] : ""); + const width = columnWidths[i] || 0; + const counted = getStringWidth(value); + columnWidths[i] = Math.max(width, counted); + } + } + + const divider = columnWidths.map((i) => + tableChars.middleMiddle.repeat(i + 2) + ); + + let result = `${tableChars.topLeft}${divider.join(tableChars.topMiddle)}` + + `${tableChars.topRight}\n${renderRow(head, columnWidths)}\n` + + `${tableChars.leftMiddle}${divider.join(tableChars.rowMiddle)}` + + `${tableChars.rightMiddle}\n`; + + for (const row of rows) { + result += `${renderRow(row, columnWidths)}\n`; + } + + result += + `${tableChars.bottomLeft}${divider.join(tableChars.bottomMiddle)}` + + tableChars.bottomRight; + + return result; + } + /* End of forked part */ + + const DEFAULT_INSPECT_OPTIONS = { + depth: 4, + indentLevel: 0, + sorted: false, + trailingComma: false, + compact: true, + iterableLimit: 100, + showProxy: false, + colors: false, + getters: false, + }; + + const DEFAULT_INDENT = " "; // Default indent string + + const LINE_BREAKING_LENGTH = 80; + const MIN_GROUP_LENGTH = 6; + const STR_ABBREVIATE_SIZE = 100; + + const PROMISE_STRING_BASE_LENGTH = 12; + + class CSI { + static kClear = "\x1b[1;1H"; + static kClearScreenDown = "\x1b[0J"; + } + + function getClassInstanceName(instance) { + if (typeof instance != "object") { + return ""; + } + const constructor = instance?.constructor; + if (typeof constructor == "function") { + return constructor.name ?? ""; + } + return ""; + } + + function maybeColor(fn, inspectOptions) { + return inspectOptions.colors ? fn : (s) => s; + } + + function inspectFunction(value, _ctx) { + if (customInspect in value && typeof value[customInspect] === "function") { + try { + return String(value[customInspect]()); + } catch { + // pass + } + } + // Might be Function/AsyncFunction/GeneratorFunction/AsyncGeneratorFunction + let cstrName = Object.getPrototypeOf(value)?.constructor?.name; + if (!cstrName) { + // If prototype is removed or broken, + // use generic 'Function' instead. + cstrName = "Function"; + } + if (value.name && value.name !== "anonymous") { + // from MDN spec + return `[${cstrName}: ${value.name}]`; + } + return `[${cstrName}]`; + } + + function inspectIterable( + value, + ctx, + level, + options, + inspectOptions, + ) { + const cyan = maybeColor(colors.cyan, inspectOptions); + if (level >= inspectOptions.depth) { + return cyan(`[${options.typeName}]`); + } + ctx.add(value); + + const entries = []; + + const iter = value.entries(); + let entriesLength = 0; + const next = () => { + return iter.next(); + }; + for (const el of iter) { + if (entriesLength < inspectOptions.iterableLimit) { + entries.push( + options.entryHandler( + el, + ctx, + level + 1, + inspectOptions, + next.bind(iter), + ), + ); + } + entriesLength++; + } + ctx.delete(value); + + if (options.sort) { + entries.sort(); + } + + if (entriesLength > inspectOptions.iterableLimit) { + const nmore = entriesLength - inspectOptions.iterableLimit; + entries.push(`... ${nmore} more items`); + } + + const iPrefix = `${options.displayName ? options.displayName + " " : ""}`; + + const initIndentation = `\n${DEFAULT_INDENT.repeat(level + 1)}`; + const entryIndentation = `,\n${DEFAULT_INDENT.repeat(level + 1)}`; + const closingIndentation = `${inspectOptions.trailingComma ? "," : ""}\n${ + DEFAULT_INDENT.repeat(level) + }`; + + let iContent; + if (options.group && entries.length > MIN_GROUP_LENGTH) { + const groups = groupEntries(entries, level, value); + iContent = `${initIndentation}${ + groups.join(entryIndentation) + }${closingIndentation}`; + } else { + iContent = entries.length === 0 ? "" : ` ${entries.join(", ")} `; + if ( + colors.stripColor(iContent).length > LINE_BREAKING_LENGTH || + !inspectOptions.compact + ) { + iContent = `${initIndentation}${ + entries.join(entryIndentation) + }${closingIndentation}`; + } + } + + return `${iPrefix}${options.delims[0]}${iContent}${options.delims[1]}`; + } + + // Ported from Node.js + // Copyright Node.js contributors. All rights reserved. + function groupEntries( + entries, + level, + value, + iterableLimit = 100, + ) { + let totalLength = 0; + let maxLength = 0; + let entriesLength = entries.length; + if (iterableLimit < entriesLength) { + // This makes sure the "... n more items" part is not taken into account. + entriesLength--; + } + const separatorSpace = 2; // Add 1 for the space and 1 for the separator. + const dataLen = new Array(entriesLength); + // Calculate the total length of all output entries and the individual max + // entries length of all output entries. + // IN PROGRESS: Colors are being taken into account. + for (let i = 0; i < entriesLength; i++) { + // Taking colors into account: removing the ANSI color + // codes from the string before measuring its length + const len = colors.stripColor(entries[i]).length; + dataLen[i] = len; + totalLength += len + separatorSpace; + if (maxLength < len) maxLength = len; + } + // Add two to `maxLength` as we add a single whitespace character plus a comma + // in-between two entries. + const actualMax = maxLength + separatorSpace; + // Check if at least three entries fit next to each other and prevent grouping + // of arrays that contains entries of very different length (i.e., if a single + // entry is longer than 1/5 of all other entries combined). Otherwise the + // space in-between small entries would be enormous. + if ( + actualMax * 3 + (level + 1) < LINE_BREAKING_LENGTH && + (totalLength / actualMax > 5 || maxLength <= 6) + ) { + const approxCharHeights = 2.5; + const averageBias = Math.sqrt(actualMax - totalLength / entries.length); + const biasedMax = Math.max(actualMax - 3 - averageBias, 1); + // Dynamically check how many columns seem possible. + const columns = Math.min( + // Ideally a square should be drawn. We expect a character to be about 2.5 + // times as high as wide. This is the area formula to calculate a square + // which contains n rectangles of size `actualMax * approxCharHeights`. + // Divide that by `actualMax` to receive the correct number of columns. + // The added bias increases the columns for short entries. + Math.round( + Math.sqrt(approxCharHeights * biasedMax * entriesLength) / biasedMax, + ), + // Do not exceed the breakLength. + Math.floor((LINE_BREAKING_LENGTH - (level + 1)) / actualMax), + // Limit the columns to a maximum of fifteen. + 15, + ); + // Return with the original output if no grouping should happen. + if (columns <= 1) { + return entries; + } + const tmp = []; + const maxLineLength = []; + for (let i = 0; i < columns; i++) { + let lineMaxLength = 0; + for (let j = i; j < entries.length; j += columns) { + if (dataLen[j] > lineMaxLength) lineMaxLength = dataLen[j]; + } + lineMaxLength += separatorSpace; + maxLineLength[i] = lineMaxLength; + } + let order = "padStart"; + if (value !== undefined) { + for (let i = 0; i < entries.length; i++) { + if ( + typeof value[i] !== "number" && + typeof value[i] !== "bigint" + ) { + order = "padEnd"; + break; + } + } + } + // Each iteration creates a single line of grouped entries. + for (let i = 0; i < entriesLength; i += columns) { + // The last lines may contain less entries than columns. + const max = Math.min(i + columns, entriesLength); + let str = ""; + let j = i; + for (; j < max - 1; j++) { + const lengthOfColorCodes = entries[j].length - dataLen[j]; + const padding = maxLineLength[j - i] + lengthOfColorCodes; + str += `${entries[j]}, `[order](padding, " "); + } + if (order === "padStart") { + const lengthOfColorCodes = entries[j].length - dataLen[j]; + const padding = maxLineLength[j - i] + + lengthOfColorCodes - + separatorSpace; + str += entries[j].padStart(padding, " "); + } else { + str += entries[j]; + } + tmp.push(str); + } + if (iterableLimit < entries.length) { + tmp.push(entries[entriesLength]); + } + entries = tmp; + } + return entries; + } + + function inspectValue( + value, + ctx, + level, + inspectOptions, + ) { + const proxyDetails = core.getProxyDetails(value); + if (proxyDetails != null) { + return inspectOptions.showProxy + ? inspectProxy(proxyDetails, ctx, level, inspectOptions) + : inspectValue(proxyDetails[0], ctx, level, inspectOptions); + } + + const green = maybeColor(colors.green, inspectOptions); + const yellow = maybeColor(colors.yellow, inspectOptions); + const dim = maybeColor(colors.dim, inspectOptions); + const cyan = maybeColor(colors.cyan, inspectOptions); + const bold = maybeColor(colors.bold, inspectOptions); + const red = maybeColor(colors.red, inspectOptions); + + switch (typeof value) { + case "string": + return green(quoteString(value)); + case "number": // Numbers are yellow + // Special handling of -0 + return yellow(Object.is(value, -0) ? "-0" : `${value}`); + case "boolean": // booleans are yellow + return yellow(String(value)); + case "undefined": // undefined is dim + return dim(String(value)); + case "symbol": // Symbols are green + return green(maybeQuoteSymbol(value)); + case "bigint": // Bigints are yellow + return yellow(`${value}n`); + case "function": // Function string is cyan + return cyan(inspectFunction(value, ctx)); + case "object": // null is bold + if (value === null) { + return bold("null"); + } + + if (ctx.has(value)) { + // Circular string is cyan + return cyan("[Circular]"); + } + + return inspectObject(value, ctx, level, inspectOptions); + default: + // Not implemented is red + return red("[Not Implemented]"); + } + } + + // We can match Node's quoting behavior exactly by swapping the double quote and + // single quote in this array. That would give preference to single quotes. + // However, we prefer double quotes as the default. + const QUOTES = ['"', "'", "`"]; + + /** Surround the string in quotes. + * + * The quote symbol is chosen by taking the first of the `QUOTES` array which + * does not occur in the string. If they all occur, settle with `QUOTES[0]`. + * + * Insert a backslash before any occurrence of the chosen quote symbol and + * before any backslash. + */ + function quoteString(string) { + const quote = QUOTES.find((c) => !string.includes(c)) ?? QUOTES[0]; + const escapePattern = new RegExp(`(?=[${quote}\\\\])`, "g"); + string = string.replace(escapePattern, "\\"); + string = replaceEscapeSequences(string); + return `${quote}${string}${quote}`; + } + + // Replace escape sequences that can modify output. + function replaceEscapeSequences(string) { + return string + .replace(/[\b]/g, "\\b") + .replace(/\f/g, "\\f") + .replace(/\n/g, "\\n") + .replace(/\r/g, "\\r") + .replace(/\t/g, "\\t") + .replace(/\v/g, "\\v") + .replace( + // deno-lint-ignore no-control-regex + /[\x00-\x1f\x7f-\x9f]/g, + (c) => "\\x" + c.charCodeAt(0).toString(16).padStart(2, "0"), + ); + } + + // Surround a string with quotes when it is required (e.g the string not a valid identifier). + function maybeQuoteString(string) { + if (/^[a-zA-Z_][a-zA-Z_0-9]*$/.test(string)) { + return replaceEscapeSequences(string); + } + + return quoteString(string); + } + + // Surround a symbol's description in quotes when it is required (e.g the description has non printable characters). + function maybeQuoteSymbol(symbol) { + if (symbol.description === undefined) { + return symbol.toString(); + } + + if (/^[a-zA-Z_][a-zA-Z_.0-9]*$/.test(symbol.description)) { + return symbol.toString(); + } + + return `Symbol(${quoteString(symbol.description)})`; + } + + // Print strings when they are inside of arrays or objects with quotes + function inspectValueWithQuotes( + value, + ctx, + level, + inspectOptions, + ) { + const green = maybeColor(colors.green, inspectOptions); + switch (typeof value) { + case "string": { + const trunc = value.length > STR_ABBREVIATE_SIZE + ? value.slice(0, STR_ABBREVIATE_SIZE) + "..." + : value; + return green(quoteString(trunc)); // Quoted strings are green + } + default: + return inspectValue(value, ctx, level, inspectOptions); + } + } + + function inspectArray( + value, + ctx, + level, + inspectOptions, + ) { + const dim = maybeColor(colors.dim, inspectOptions); + const options = { + typeName: "Array", + displayName: "", + delims: ["[", "]"], + entryHandler: (entry, ctx, level, inspectOptions, next) => { + const [index, val] = entry; + let i = index; + if (!value.hasOwnProperty(i)) { + i++; + while (!value.hasOwnProperty(i) && i < value.length) { + next(); + i++; + } + const emptyItems = i - index; + const ending = emptyItems > 1 ? "s" : ""; + return dim(`<${emptyItems} empty item${ending}>`); + } else { + return inspectValueWithQuotes(val, ctx, level, inspectOptions); + } + }, + group: inspectOptions.compact, + sort: false, + }; + return inspectIterable(value, ctx, level, options, inspectOptions); + } + + function inspectTypedArray( + typedArrayName, + value, + ctx, + level, + inspectOptions, + ) { + const valueLength = value.length; + const options = { + typeName: typedArrayName, + displayName: `${typedArrayName}(${valueLength})`, + delims: ["[", "]"], + entryHandler: (entry, ctx, level, inspectOptions) => { + const val = entry[1]; + return inspectValueWithQuotes(val, ctx, level + 1, inspectOptions); + }, + group: inspectOptions.compact, + sort: false, + }; + return inspectIterable(value, ctx, level, options, inspectOptions); + } + + function inspectSet( + value, + ctx, + level, + inspectOptions, + ) { + const options = { + typeName: "Set", + displayName: "Set", + delims: ["{", "}"], + entryHandler: (entry, ctx, level, inspectOptions) => { + const val = entry[1]; + return inspectValueWithQuotes(val, ctx, level + 1, inspectOptions); + }, + group: false, + sort: inspectOptions.sorted, + }; + return inspectIterable(value, ctx, level, options, inspectOptions); + } + + function inspectMap( + value, + ctx, + level, + inspectOptions, + ) { + const options = { + typeName: "Map", + displayName: "Map", + delims: ["{", "}"], + entryHandler: (entry, ctx, level, inspectOptions) => { + const [key, val] = entry; + return `${ + inspectValueWithQuotes( + key, + ctx, + level + 1, + inspectOptions, + ) + } => ${inspectValueWithQuotes(val, ctx, level + 1, inspectOptions)}`; + }, + group: false, + sort: inspectOptions.sorted, + }; + return inspectIterable( + value, + ctx, + level, + options, + inspectOptions, + ); + } + + function inspectWeakSet(inspectOptions) { + const cyan = maybeColor(colors.cyan, inspectOptions); + return `WeakSet { ${cyan("[items unknown]")} }`; // as seen in Node, with cyan color + } + + function inspectWeakMap(inspectOptions) { + const cyan = maybeColor(colors.cyan, inspectOptions); + return `WeakMap { ${cyan("[items unknown]")} }`; // as seen in Node, with cyan color + } + + function inspectDate(value, inspectOptions) { + // without quotes, ISO format, in magenta like before + const magenta = maybeColor(colors.magenta, inspectOptions); + return magenta(isInvalidDate(value) ? "Invalid Date" : value.toISOString()); + } + + function inspectRegExp(value, inspectOptions) { + const red = maybeColor(colors.red, inspectOptions); + return red(value.toString()); // RegExps are red + } + + function inspectStringObject(value, inspectOptions) { + const cyan = maybeColor(colors.cyan, inspectOptions); + return cyan(`[String: "${value.toString()}"]`); // wrappers are in cyan + } + + function inspectBooleanObject(value, inspectOptions) { + const cyan = maybeColor(colors.cyan, inspectOptions); + return cyan(`[Boolean: ${value.toString()}]`); // wrappers are in cyan + } + + function inspectNumberObject(value, inspectOptions) { + const cyan = maybeColor(colors.cyan, inspectOptions); + return cyan(`[Number: ${value.toString()}]`); // wrappers are in cyan + } + + const PromiseState = { + Pending: 0, + Fulfilled: 1, + Rejected: 2, + }; + + function inspectPromise( + value, + ctx, + level, + inspectOptions, + ) { + const cyan = maybeColor(colors.cyan, inspectOptions); + const red = maybeColor(colors.red, inspectOptions); + + const [state, result] = core.getPromiseDetails(value); + + if (state === PromiseState.Pending) { + return `Promise { ${cyan("")} }`; + } + + const prefix = state === PromiseState.Fulfilled + ? "" + : `${red("")} `; + + const str = `${prefix}${ + inspectValueWithQuotes( + result, + ctx, + level + 1, + inspectOptions, + ) + }`; + + if (str.length + PROMISE_STRING_BASE_LENGTH > LINE_BREAKING_LENGTH) { + return `Promise {\n${DEFAULT_INDENT.repeat(level + 1)}${str}\n}`; + } + + return `Promise { ${str} }`; + } + + function inspectProxy( + targetAndHandler, + ctx, + level, + inspectOptions, + ) { + return `Proxy ${ + inspectArray(targetAndHandler, ctx, level, inspectOptions) + }`; + } + + function inspectRawObject( + value, + ctx, + level, + inspectOptions, + ) { + const cyan = maybeColor(colors.cyan, inspectOptions); + + if (level >= inspectOptions.depth) { + return cyan("[Object]"); // wrappers are in cyan + } + ctx.add(value); + + let baseString; + + let shouldShowDisplayName = false; + let displayName = value[ + Symbol.toStringTag + ]; + if (!displayName) { + displayName = getClassInstanceName(value); + } + if ( + displayName && displayName !== "Object" && displayName !== "anonymous" + ) { + shouldShowDisplayName = true; + } + + const entries = []; + const stringKeys = Object.keys(value); + const symbolKeys = Object.getOwnPropertySymbols(value); + if (inspectOptions.sorted) { + stringKeys.sort(); + symbolKeys.sort((s1, s2) => + (s1.description ?? "").localeCompare(s2.description ?? "") + ); + } + + const red = maybeColor(colors.red, inspectOptions); + + for (const key of stringKeys) { + if (inspectOptions.getters) { + let propertyValue; + let error = null; + try { + propertyValue = value[key]; + } catch (error_) { + error = error_; + } + const inspectedValue = error == null + ? inspectValueWithQuotes( + propertyValue, + ctx, + level + 1, + inspectOptions, + ) + : red(`[Thrown ${error.name}: ${error.message}]`); + entries.push(`${maybeQuoteString(key)}: ${inspectedValue}`); + } else { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (descriptor.get !== undefined && descriptor.set !== undefined) { + entries.push(`${maybeQuoteString(key)}: [Getter/Setter]`); + } else if (descriptor.get !== undefined) { + entries.push(`${maybeQuoteString(key)}: [Getter]`); + } else { + entries.push( + `${maybeQuoteString(key)}: ${ + inspectValueWithQuotes(value[key], ctx, level + 1, inspectOptions) + }`, + ); + } + } + } + + for (const key of symbolKeys) { + if (inspectOptions.getters) { + let propertyValue; + let error; + try { + propertyValue = value[key]; + } catch (error_) { + error = error_; + } + const inspectedValue = error == null + ? inspectValueWithQuotes( + propertyValue, + ctx, + level + 1, + inspectOptions, + ) + : red(`Thrown ${error.name}: ${error.message}`); + entries.push(`[${maybeQuoteSymbol(key)}]: ${inspectedValue}`); + } else { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (descriptor.get !== undefined && descriptor.set !== undefined) { + entries.push(`[${maybeQuoteSymbol(key)}]: [Getter/Setter]`); + } else if (descriptor.get !== undefined) { + entries.push(`[${maybeQuoteSymbol(key)}]: [Getter]`); + } else { + entries.push( + `[${maybeQuoteSymbol(key)}]: ${ + inspectValueWithQuotes(value[key], ctx, level + 1, inspectOptions) + }`, + ); + } + } + } + + // Making sure color codes are ignored when calculating the total length + const totalLength = entries.length + level + + colors.stripColor(entries.join("")).length; + + ctx.delete(value); + + if (entries.length === 0) { + baseString = "{}"; + } else if (totalLength > LINE_BREAKING_LENGTH || !inspectOptions.compact) { + const entryIndent = DEFAULT_INDENT.repeat(level + 1); + const closingIndent = DEFAULT_INDENT.repeat(level); + baseString = `{\n${entryIndent}${entries.join(`,\n${entryIndent}`)}${ + inspectOptions.trailingComma ? "," : "" + }\n${closingIndent}}`; + } else { + baseString = `{ ${entries.join(", ")} }`; + } + + if (shouldShowDisplayName) { + baseString = `${displayName} ${baseString}`; + } + + return baseString; + } + + function inspectObject( + value, + consoleContext, + level, + inspectOptions, + ) { + if (customInspect in value && typeof value[customInspect] === "function") { + try { + return String(value[customInspect]()); + } catch { + // pass + } + } + // This non-unique symbol is used to support op_crates, ie. + // in op_crates/web we don't want to depend on unique "Deno.customInspect" + // symbol defined in the public API. Internal only, shouldn't be used + // by users. + const nonUniqueCustomInspect = Symbol.for("Deno.customInspect"); + if ( + nonUniqueCustomInspect in value && + typeof value[nonUniqueCustomInspect] === "function" + ) { + try { + return String(value[nonUniqueCustomInspect]()); + } catch { + // pass + } + } + if (value instanceof Error) { + return String(value.stack); + } else if (Array.isArray(value)) { + return inspectArray(value, consoleContext, level, inspectOptions); + } else if (value instanceof Number) { + return inspectNumberObject(value, inspectOptions); + } else if (value instanceof Boolean) { + return inspectBooleanObject(value, inspectOptions); + } else if (value instanceof String) { + return inspectStringObject(value, inspectOptions); + } else if (value instanceof Promise) { + return inspectPromise(value, consoleContext, level, inspectOptions); + } else if (value instanceof RegExp) { + return inspectRegExp(value, inspectOptions); + } else if (value instanceof Date) { + return inspectDate(value, inspectOptions); + } else if (value instanceof Set) { + return inspectSet(value, consoleContext, level, inspectOptions); + } else if (value instanceof Map) { + return inspectMap(value, consoleContext, level, inspectOptions); + } else if (value instanceof WeakSet) { + return inspectWeakSet(inspectOptions); + } else if (value instanceof WeakMap) { + return inspectWeakMap(inspectOptions); + } else if (isTypedArray(value)) { + return inspectTypedArray( + Object.getPrototypeOf(value).constructor.name, + value, + consoleContext, + level, + inspectOptions, + ); + } else { + // Otherwise, default object formatting + return inspectRawObject(value, consoleContext, level, inspectOptions); + } + } + + const colorKeywords = new Map([ + ["black", "#000000"], + ["silver", "#c0c0c0"], + ["gray", "#808080"], + ["white", "#ffffff"], + ["maroon", "#800000"], + ["red", "#ff0000"], + ["purple", "#800080"], + ["fuchsia", "#ff00ff"], + ["green", "#008000"], + ["lime", "#00ff00"], + ["olive", "#808000"], + ["yellow", "#ffff00"], + ["navy", "#000080"], + ["blue", "#0000ff"], + ["teal", "#008080"], + ["aqua", "#00ffff"], + ["orange", "#ffa500"], + ["aliceblue", "#f0f8ff"], + ["antiquewhite", "#faebd7"], + ["aquamarine", "#7fffd4"], + ["azure", "#f0ffff"], + ["beige", "#f5f5dc"], + ["bisque", "#ffe4c4"], + ["blanchedalmond", "#ffebcd"], + ["blueviolet", "#8a2be2"], + ["brown", "#a52a2a"], + ["burlywood", "#deb887"], + ["cadetblue", "#5f9ea0"], + ["chartreuse", "#7fff00"], + ["chocolate", "#d2691e"], + ["coral", "#ff7f50"], + ["cornflowerblue", "#6495ed"], + ["cornsilk", "#fff8dc"], + ["crimson", "#dc143c"], + ["cyan", "#00ffff"], + ["darkblue", "#00008b"], + ["darkcyan", "#008b8b"], + ["darkgoldenrod", "#b8860b"], + ["darkgray", "#a9a9a9"], + ["darkgreen", "#006400"], + ["darkgrey", "#a9a9a9"], + ["darkkhaki", "#bdb76b"], + ["darkmagenta", "#8b008b"], + ["darkolivegreen", "#556b2f"], + ["darkorange", "#ff8c00"], + ["darkorchid", "#9932cc"], + ["darkred", "#8b0000"], + ["darksalmon", "#e9967a"], + ["darkseagreen", "#8fbc8f"], + ["darkslateblue", "#483d8b"], + ["darkslategray", "#2f4f4f"], + ["darkslategrey", "#2f4f4f"], + ["darkturquoise", "#00ced1"], + ["darkviolet", "#9400d3"], + ["deeppink", "#ff1493"], + ["deepskyblue", "#00bfff"], + ["dimgray", "#696969"], + ["dimgrey", "#696969"], + ["dodgerblue", "#1e90ff"], + ["firebrick", "#b22222"], + ["floralwhite", "#fffaf0"], + ["forestgreen", "#228b22"], + ["gainsboro", "#dcdcdc"], + ["ghostwhite", "#f8f8ff"], + ["gold", "#ffd700"], + ["goldenrod", "#daa520"], + ["greenyellow", "#adff2f"], + ["grey", "#808080"], + ["honeydew", "#f0fff0"], + ["hotpink", "#ff69b4"], + ["indianred", "#cd5c5c"], + ["indigo", "#4b0082"], + ["ivory", "#fffff0"], + ["khaki", "#f0e68c"], + ["lavender", "#e6e6fa"], + ["lavenderblush", "#fff0f5"], + ["lawngreen", "#7cfc00"], + ["lemonchiffon", "#fffacd"], + ["lightblue", "#add8e6"], + ["lightcoral", "#f08080"], + ["lightcyan", "#e0ffff"], + ["lightgoldenrodyellow", "#fafad2"], + ["lightgray", "#d3d3d3"], + ["lightgreen", "#90ee90"], + ["lightgrey", "#d3d3d3"], + ["lightpink", "#ffb6c1"], + ["lightsalmon", "#ffa07a"], + ["lightseagreen", "#20b2aa"], + ["lightskyblue", "#87cefa"], + ["lightslategray", "#778899"], + ["lightslategrey", "#778899"], + ["lightsteelblue", "#b0c4de"], + ["lightyellow", "#ffffe0"], + ["limegreen", "#32cd32"], + ["linen", "#faf0e6"], + ["magenta", "#ff00ff"], + ["mediumaquamarine", "#66cdaa"], + ["mediumblue", "#0000cd"], + ["mediumorchid", "#ba55d3"], + ["mediumpurple", "#9370db"], + ["mediumseagreen", "#3cb371"], + ["mediumslateblue", "#7b68ee"], + ["mediumspringgreen", "#00fa9a"], + ["mediumturquoise", "#48d1cc"], + ["mediumvioletred", "#c71585"], + ["midnightblue", "#191970"], + ["mintcream", "#f5fffa"], + ["mistyrose", "#ffe4e1"], + ["moccasin", "#ffe4b5"], + ["navajowhite", "#ffdead"], + ["oldlace", "#fdf5e6"], + ["olivedrab", "#6b8e23"], + ["orangered", "#ff4500"], + ["orchid", "#da70d6"], + ["palegoldenrod", "#eee8aa"], + ["palegreen", "#98fb98"], + ["paleturquoise", "#afeeee"], + ["palevioletred", "#db7093"], + ["papayawhip", "#ffefd5"], + ["peachpuff", "#ffdab9"], + ["peru", "#cd853f"], + ["pink", "#ffc0cb"], + ["plum", "#dda0dd"], + ["powderblue", "#b0e0e6"], + ["rosybrown", "#bc8f8f"], + ["royalblue", "#4169e1"], + ["saddlebrown", "#8b4513"], + ["salmon", "#fa8072"], + ["sandybrown", "#f4a460"], + ["seagreen", "#2e8b57"], + ["seashell", "#fff5ee"], + ["sienna", "#a0522d"], + ["skyblue", "#87ceeb"], + ["slateblue", "#6a5acd"], + ["slategray", "#708090"], + ["slategrey", "#708090"], + ["snow", "#fffafa"], + ["springgreen", "#00ff7f"], + ["steelblue", "#4682b4"], + ["tan", "#d2b48c"], + ["thistle", "#d8bfd8"], + ["tomato", "#ff6347"], + ["turquoise", "#40e0d0"], + ["violet", "#ee82ee"], + ["wheat", "#f5deb3"], + ["whitesmoke", "#f5f5f5"], + ["yellowgreen", "#9acd32"], + ["rebeccapurple", "#663399"], + ]); + + function parseCssColor(colorString) { + if (colorKeywords.has(colorString)) { + colorString = colorKeywords.get(colorString); + } + // deno-fmt-ignore + const hashMatch = colorString.match(/^#([\dA-Fa-f]{2})([\dA-Fa-f]{2})([\dA-Fa-f]{2})([\dA-Fa-f]{2})?$/); + if (hashMatch != null) { + return [ + Number(`0x${hashMatch[1]}`), + Number(`0x${hashMatch[2]}`), + Number(`0x${hashMatch[3]}`), + ]; + } + // deno-fmt-ignore + const smallHashMatch = colorString.match(/^#([\dA-Fa-f])([\dA-Fa-f])([\dA-Fa-f])([\dA-Fa-f])?$/); + if (smallHashMatch != null) { + return [ + Number(`0x${smallHashMatch[1]}0`), + Number(`0x${smallHashMatch[2]}0`), + Number(`0x${smallHashMatch[3]}0`), + ]; + } + // deno-fmt-ignore + const rgbMatch = colorString.match(/^rgba?\(\s*([+\-]?\d*\.?\d+)\s*,\s*([+\-]?\d*\.?\d+)\s*,\s*([+\-]?\d*\.?\d+)\s*(,\s*([+\-]?\d*\.?\d+)\s*)?\)$/); + if (rgbMatch != null) { + return [ + Math.round(Math.max(0, Math.min(255, Number(rgbMatch[1])))), + Math.round(Math.max(0, Math.min(255, Number(rgbMatch[2])))), + Math.round(Math.max(0, Math.min(255, Number(rgbMatch[3])))), + ]; + } + // deno-fmt-ignore + const hslMatch = colorString.match(/^hsla?\(\s*([+\-]?\d*\.?\d+)\s*,\s*([+\-]?\d*\.?\d+)%\s*,\s*([+\-]?\d*\.?\d+)%\s*(,\s*([+\-]?\d*\.?\d+)\s*)?\)$/); + if (hslMatch != null) { + // https://www.rapidtables.com/convert/color/hsl-to-rgb.html + let h = Number(hslMatch[1]) % 360; + if (h < 0) { + h += 360; + } + const s = Math.max(0, Math.min(100, Number(hslMatch[2]))) / 100; + const l = Math.max(0, Math.min(100, Number(hslMatch[3]))) / 100; + const c = (1 - Math.abs(2 * l - 1)) * s; + const x = c * (1 - Math.abs((h / 60) % 2 - 1)); + const m = l - c / 2; + let r_; + let g_; + let b_; + if (h < 60) { + [r_, g_, b_] = [c, x, 0]; + } else if (h < 120) { + [r_, g_, b_] = [x, c, 0]; + } else if (h < 180) { + [r_, g_, b_] = [0, c, x]; + } else if (h < 240) { + [r_, g_, b_] = [0, x, c]; + } else if (h < 300) { + [r_, g_, b_] = [x, 0, c]; + } else { + [r_, g_, b_] = [c, 0, x]; + } + return [ + Math.round((r_ + m) * 255), + Math.round((g_ + m) * 255), + Math.round((b_ + m) * 255), + ]; + } + return null; + } + + function getDefaultCss() { + return { + backgroundColor: null, + color: null, + fontWeight: null, + fontStyle: null, + textDecorationColor: null, + textDecorationLine: [], + }; + } + + function parseCss(cssString) { + const css = getDefaultCss(); + + const rawEntries = []; + let inValue = false; + let currentKey = null; + let parenthesesDepth = 0; + let currentPart = ""; + for (let i = 0; i < cssString.length; i++) { + const c = cssString[i]; + if (c == "(") { + parenthesesDepth++; + } else if (parenthesesDepth > 0) { + if (c == ")") { + parenthesesDepth--; + } + } else if (inValue) { + if (c == ";") { + const value = currentPart.trim(); + if (value != "") { + rawEntries.push([currentKey, value]); + } + currentKey = null; + currentPart = ""; + inValue = false; + continue; + } + } else if (c == ":") { + currentKey = currentPart.trim(); + currentPart = ""; + inValue = true; + continue; + } + currentPart += c; + } + if (inValue && parenthesesDepth == 0) { + const value = currentPart.trim(); + if (value != "") { + rawEntries.push([currentKey, value]); + } + currentKey = null; + currentPart = ""; + } + + for (const [key, value] of rawEntries) { + if (key == "background-color") { + const color = parseCssColor(value); + if (color != null) { + css.backgroundColor = color; + } + } else if (key == "color") { + const color = parseCssColor(value); + if (color != null) { + css.color = color; + } + } else if (key == "font-weight") { + if (value == "bold") { + css.fontWeight = value; + } + } else if (key == "font-style") { + if (["italic", "oblique", "oblique 14deg"].includes(value)) { + css.fontStyle = "italic"; + } + } else if (key == "text-decoration-line") { + css.textDecorationLine = []; + for (const lineType of value.split(/\s+/g)) { + if (["line-through", "overline", "underline"].includes(lineType)) { + css.textDecorationLine.push(lineType); + } + } + } else if (key == "text-decoration-color") { + const color = parseCssColor(value); + if (color != null) { + css.textDecorationColor = color; + } + } else if (key == "text-decoration") { + css.textDecorationColor = null; + css.textDecorationLine = []; + for (const arg of value.split(/\s+/g)) { + const maybeColor = parseCssColor(arg); + if (maybeColor != null) { + css.textDecorationColor = maybeColor; + } else if (["line-through", "overline", "underline"].includes(arg)) { + css.textDecorationLine.push(arg); + } + } + } + } + + return css; + } + + function colorEquals(color1, color2) { + return color1?.[0] == color2?.[0] && color1?.[1] == color2?.[1] && + color1?.[2] == color2?.[2]; + } + + function cssToAnsi(css, prevCss = null) { + prevCss = prevCss ?? getDefaultCss(); + let ansi = ""; + if (!colorEquals(css.backgroundColor, prevCss.backgroundColor)) { + if (css.backgroundColor != null) { + const [r, g, b] = css.backgroundColor; + ansi += `\x1b[48;2;${r};${g};${b}m`; + } else { + ansi += "\x1b[49m"; + } + } + if (!colorEquals(css.color, prevCss.color)) { + if (css.color != null) { + const [r, g, b] = css.color; + ansi += `\x1b[38;2;${r};${g};${b}m`; + } else { + ansi += "\x1b[39m"; + } + } + if (css.fontWeight != prevCss.fontWeight) { + if (css.fontWeight == "bold") { + ansi += `\x1b[1m`; + } else { + ansi += "\x1b[22m"; + } + } + if (css.fontStyle != prevCss.fontStyle) { + if (css.fontStyle == "italic") { + ansi += `\x1b[3m`; + } else { + ansi += "\x1b[23m"; + } + } + if (!colorEquals(css.textDecorationColor, prevCss.textDecorationColor)) { + if (css.textDecorationColor != null) { + const [r, g, b] = css.textDecorationColor; + ansi += `\x1b[58;2;${r};${g};${b}m`; + } else { + ansi += "\x1b[59m"; + } + } + if ( + css.textDecorationLine.includes("line-through") != + prevCss.textDecorationLine.includes("line-through") + ) { + if (css.textDecorationLine.includes("line-through")) { + ansi += "\x1b[9m"; + } else { + ansi += "\x1b[29m"; + } + } + if ( + css.textDecorationLine.includes("overline") != + prevCss.textDecorationLine.includes("overline") + ) { + if (css.textDecorationLine.includes("overline")) { + ansi += "\x1b[53m"; + } else { + ansi += "\x1b[55m"; + } + } + if ( + css.textDecorationLine.includes("underline") != + prevCss.textDecorationLine.includes("underline") + ) { + if (css.textDecorationLine.includes("underline")) { + ansi += "\x1b[4m"; + } else { + ansi += "\x1b[24m"; + } + } + return ansi; + } + + function inspectArgs(args, inspectOptions = {}) { + const noColor = globalThis.Deno?.noColor ?? true; + const rInspectOptions = { ...DEFAULT_INSPECT_OPTIONS, ...inspectOptions }; + const first = args[0]; + let a = 0; + let string = ""; + + if (typeof first == "string" && args.length > 1) { + a++; + // Index of the first not-yet-appended character. Use this so we only + // have to append to `string` when a substitution occurs / at the end. + let appendedChars = 0; + let usedStyle = false; + let prevCss = null; + for (let i = 0; i < first.length - 1; i++) { + if (first[i] == "%") { + const char = first[++i]; + if (a < args.length) { + let formattedArg = null; + if (char == "s") { + // Format as a string. + formattedArg = String(args[a++]); + } else if (["d", "i"].includes(char)) { + // Format as an integer. + const value = args[a++]; + if (typeof value == "bigint") { + formattedArg = `${value}n`; + } else if (typeof value == "number") { + formattedArg = `${parseInt(String(value))}`; + } else { + formattedArg = "NaN"; + } + } else if (char == "f") { + // Format as a floating point value. + const value = args[a++]; + if (typeof value == "number") { + formattedArg = `${value}`; + } else { + formattedArg = "NaN"; + } + } else if (["O", "o"].includes(char)) { + // Format as an object. + formattedArg = inspectValue( + args[a++], + new Set(), + 0, + rInspectOptions, + ); + } else if (char == "c") { + const value = args[a++]; + if (!noColor) { + const css = parseCss(value); + formattedArg = cssToAnsi(css, prevCss); + if (formattedArg != "") { + usedStyle = true; + prevCss = css; + } + } else { + formattedArg = ""; + } + } + + if (formattedArg != null) { + string += first.slice(appendedChars, i - 1) + formattedArg; + appendedChars = i + 1; + } + } + if (char == "%") { + string += first.slice(appendedChars, i - 1) + "%"; + appendedChars = i + 1; + } + } + } + string += first.slice(appendedChars); + if (usedStyle) { + string += "\x1b[0m"; + } + } + + for (; a < args.length; a++) { + if (a > 0) { + string += " "; + } + if (typeof args[a] == "string") { + string += args[a]; + } else { + // Use default maximum depth for null or undefined arguments. + string += inspectValue(args[a], new Set(), 0, rInspectOptions); + } + } + + if (rInspectOptions.indentLevel > 0) { + const groupIndent = DEFAULT_INDENT.repeat(rInspectOptions.indentLevel); + string = groupIndent + string.replaceAll("\n", `\n${groupIndent}`); + } + + return string; + } + + const countMap = new Map(); + const timerMap = new Map(); + const isConsoleInstance = Symbol("isConsoleInstance"); + + function getConsoleInspectOptions() { + return { + ...DEFAULT_INSPECT_OPTIONS, + colors: !(globalThis.Deno?.noColor ?? false), + }; + } + + class Console { + #printFunc = null; + [isConsoleInstance] = false; + + constructor(printFunc) { + this.#printFunc = printFunc; + this.indentLevel = 0; + this[isConsoleInstance] = true; + + // ref https://console.spec.whatwg.org/#console-namespace + // For historical web-compatibility reasons, the namespace object for + // console must have as its [[Prototype]] an empty object, created as if + // by ObjectCreate(%ObjectPrototype%), instead of %ObjectPrototype%. + const console = Object.create({}); + Object.assign(console, this); + return console; + } + + log = (...args) => { + this.#printFunc( + inspectArgs(args, { + ...getConsoleInspectOptions(), + indentLevel: this.indentLevel, + }) + "\n", + false, + ); + }; + + debug = this.log; + info = this.log; + + dir = (obj, options = {}) => { + this.#printFunc( + inspectArgs([obj], { ...getConsoleInspectOptions(), ...options }) + + "\n", + false, + ); + }; + + dirxml = this.dir; + + warn = (...args) => { + this.#printFunc( + inspectArgs(args, { + ...getConsoleInspectOptions(), + indentLevel: this.indentLevel, + }) + "\n", + true, + ); + }; + + error = this.warn; + + assert = (condition = false, ...args) => { + if (condition) { + return; + } + + if (args.length === 0) { + this.error("Assertion failed"); + return; + } + + const [first, ...rest] = args; + + if (typeof first === "string") { + this.error(`Assertion failed: ${first}`, ...rest); + return; + } + + this.error(`Assertion failed:`, ...args); + }; + + count = (label = "default") => { + label = String(label); + + if (countMap.has(label)) { + const current = countMap.get(label) || 0; + countMap.set(label, current + 1); + } else { + countMap.set(label, 1); + } + + this.info(`${label}: ${countMap.get(label)}`); + }; + + countReset = (label = "default") => { + label = String(label); + + if (countMap.has(label)) { + countMap.set(label, 0); + } else { + this.warn(`Count for '${label}' does not exist`); + } + }; + + table = (data, properties) => { + if (properties !== undefined && !Array.isArray(properties)) { + throw new Error( + "The 'properties' argument must be of type Array. " + + "Received type string", + ); + } + + if (data === null || typeof data !== "object") { + return this.log(data); + } + + const objectValues = {}; + const indexKeys = []; + const values = []; + + const stringifyValue = (value) => + inspectValueWithQuotes(value, new Set(), 0, { + ...DEFAULT_INSPECT_OPTIONS, + depth: 1, + }); + const toTable = (header, body) => this.log(cliTable(header, body)); + const createColumn = (value, shift) => [ + ...(shift ? [...new Array(shift)].map(() => "") : []), + stringifyValue(value), + ]; + + let resultData; + const isSet = data instanceof Set; + const isMap = data instanceof Map; + const valuesKey = "Values"; + const indexKey = isSet || isMap ? "(iter idx)" : "(idx)"; + + if (data instanceof Set) { + resultData = [...data]; + } else if (data instanceof Map) { + let idx = 0; + resultData = {}; + + data.forEach((v, k) => { + resultData[idx] = { Key: k, Values: v }; + idx++; + }); + } else { + resultData = data; + } + + let hasPrimitives = false; + Object.keys(resultData).forEach((k, idx) => { + const value = resultData[k]; + const primitive = value === null || + (typeof value !== "function" && typeof value !== "object"); + if (properties === undefined && primitive) { + hasPrimitives = true; + values.push(stringifyValue(value)); + } else { + const valueObj = value || {}; + const keys = properties || Object.keys(valueObj); + for (const k of keys) { + if (primitive || !valueObj.hasOwnProperty(k)) { + if (objectValues[k]) { + // fill with blanks for idx to avoid misplacing from later values + objectValues[k].push(""); + } + } else { + if (objectValues[k]) { + objectValues[k].push(stringifyValue(valueObj[k])); + } else { + objectValues[k] = createColumn(valueObj[k], idx); + } + } + } + values.push(""); + } + + indexKeys.push(k); + }); + + const headerKeys = Object.keys(objectValues); + const bodyValues = Object.values(objectValues); + const header = [ + indexKey, + ...(properties || + [...headerKeys, !isMap && hasPrimitives && valuesKey]), + ].filter(Boolean); + const body = [indexKeys, ...bodyValues, values]; + + toTable(header, body); + }; + + time = (label = "default") => { + label = String(label); + + if (timerMap.has(label)) { + this.warn(`Timer '${label}' already exists`); + return; + } + + timerMap.set(label, Date.now()); + }; + + timeLog = (label = "default", ...args) => { + label = String(label); + + if (!timerMap.has(label)) { + this.warn(`Timer '${label}' does not exists`); + return; + } + + const startTime = timerMap.get(label); + const duration = Date.now() - startTime; + + this.info(`${label}: ${duration}ms`, ...args); + }; + + timeEnd = (label = "default") => { + label = String(label); + + if (!timerMap.has(label)) { + this.warn(`Timer '${label}' does not exists`); + return; + } + + const startTime = timerMap.get(label); + timerMap.delete(label); + const duration = Date.now() - startTime; + + this.info(`${label}: ${duration}ms`); + }; + + group = (...label) => { + if (label.length > 0) { + this.log(...label); + } + this.indentLevel += 2; + }; + + groupCollapsed = this.group; + + groupEnd = () => { + if (this.indentLevel > 0) { + this.indentLevel -= 2; + } + }; + + clear = () => { + this.indentLevel = 0; + this.#printFunc(CSI.kClear, false); + this.#printFunc(CSI.kClearScreenDown, false); + }; + + trace = (...args) => { + const message = inspectArgs( + args, + { ...getConsoleInspectOptions(), indentLevel: 0 }, + ); + const err = { + name: "Trace", + message, + }; + Error.captureStackTrace(err, this.trace); + this.error(err.stack); + }; + + static [Symbol.hasInstance](instance) { + return instance[isConsoleInstance]; + } + } + + const customInspect = Symbol("Deno.customInspect"); + + function inspect( + value, + inspectOptions = {}, + ) { + return inspectValue(value, new Set(), 0, { + ...DEFAULT_INSPECT_OPTIONS, + ...inspectOptions, + // TODO(nayeemrmn): Indent level is not supported. + indentLevel: 0, + }); + } + + // Expose these fields to internalObject for tests. + exposeForTest("Console", Console); + exposeForTest("cssToAnsi", cssToAnsi); + exposeForTest("inspectArgs", inspectArgs); + exposeForTest("parseCss", parseCss); + exposeForTest("parseCssColor", parseCssColor); + + window.__bootstrap.console = { + CSI, + inspectArgs, + Console, + customInspect, + inspect, + }; +})(this); diff --git a/runtime/rt/06_util.js b/runtime/rt/06_util.js new file mode 100644 index 000000000..f4804c519 --- /dev/null +++ b/runtime/rt/06_util.js @@ -0,0 +1,148 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { build } = window.__bootstrap.build; + const internals = window.__bootstrap.internals; + let logDebug = false; + let logSource = "JS"; + + function setLogDebug(debug, source) { + logDebug = debug; + if (source) { + logSource = source; + } + } + + function log(...args) { + if (logDebug) { + // if we destructure `console` off `globalThis` too early, we don't bind to + // the right console, therefore we don't log anything out. + globalThis.console.log(`DEBUG ${logSource} -`, ...args); + } + } + + class AssertionError extends Error { + constructor(msg) { + super(msg); + this.name = "AssertionError"; + } + } + + function assert(cond, msg = "Assertion failed.") { + if (!cond) { + throw new AssertionError(msg); + } + } + + function createResolvable() { + let resolve; + let reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + promise.resolve = resolve; + promise.reject = reject; + return promise; + } + + function immutableDefine( + o, + p, + value, + ) { + Object.defineProperty(o, p, { + value, + configurable: false, + writable: false, + }); + } + + // Keep in sync with `fromFileUrl()` in `std/path/win32.ts`. + function pathFromURLWin32(url) { + let path = decodeURIComponent( + url.pathname + .replace(/^\/*([A-Za-z]:)(\/|$)/, "$1/") + .replace(/\//g, "\\") + .replace(/%(?![0-9A-Fa-f]{2})/g, "%25"), + ); + if (url.hostname != "") { + // Note: The `URL` implementation guarantees that the drive letter and + // hostname are mutually exclusive. Otherwise it would not have been valid + // to append the hostname and path like this. + path = `\\\\${url.hostname}${path}`; + } + return path; + } + + // Keep in sync with `fromFileUrl()` in `std/path/posix.ts`. + function pathFromURLPosix(url) { + if (url.hostname !== "") { + throw new TypeError(`Host must be empty.`); + } + + return decodeURIComponent( + url.pathname.replace(/%(?![0-9A-Fa-f]{2})/g, "%25"), + ); + } + + function pathFromURL(pathOrUrl) { + if (pathOrUrl instanceof URL) { + if (pathOrUrl.protocol != "file:") { + throw new TypeError("Must be a file URL."); + } + + return build.os == "windows" + ? pathFromURLWin32(pathOrUrl) + : pathFromURLPosix(pathOrUrl); + } + return pathOrUrl; + } + + internals.exposeForTest("pathFromURL", pathFromURL); + + function writable(value) { + return { + value, + writable: true, + enumerable: true, + configurable: true, + }; + } + + function nonEnumerable(value) { + return { + value, + writable: true, + configurable: true, + }; + } + + function readOnly(value) { + return { + value, + enumerable: true, + }; + } + + function getterOnly(getter) { + return { + get: getter, + enumerable: true, + }; + } + + window.__bootstrap.util = { + log, + setLogDebug, + createResolvable, + assert, + AssertionError, + immutableDefine, + pathFromURL, + writable, + nonEnumerable, + readOnly, + getterOnly, + }; +})(this); diff --git a/runtime/rt/10_dispatch_minimal.js b/runtime/rt/10_dispatch_minimal.js new file mode 100644 index 000000000..dceb23e5f --- /dev/null +++ b/runtime/rt/10_dispatch_minimal.js @@ -0,0 +1,114 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + const util = window.__bootstrap.util; + + // Using an object without a prototype because `Map` was causing GC problems. + const promiseTableMin = Object.create(null); + + const decoder = new TextDecoder(); + + // Note it's important that promiseId starts at 1 instead of 0, because sync + // messages are indicated with promiseId 0. If we ever add wrap around logic for + // overflows, this should be taken into account. + let _nextPromiseId = 1; + + function nextPromiseId() { + return _nextPromiseId++; + } + + function recordFromBufMinimal(ui8) { + const headerLen = 12; + const header = ui8.subarray(0, headerLen); + const buf32 = new Int32Array( + header.buffer, + header.byteOffset, + header.byteLength / 4, + ); + const promiseId = buf32[0]; + const arg = buf32[1]; + const result = buf32[2]; + let err; + + if (arg < 0) { + err = { + className: decoder.decode(ui8.subarray(headerLen, headerLen + result)), + message: decoder.decode(ui8.subarray(headerLen + result)), + }; + } else if (ui8.length != 12) { + throw new TypeError("Malformed response message"); + } + + return { + promiseId, + arg, + result, + err, + }; + } + + function unwrapResponse(res) { + if (res.err != null) { + const ErrorClass = core.getErrorClass(res.err.className); + if (!ErrorClass) { + throw new Error( + `Unregistered error class: "${res.err.className}"\n ${res.err.message}\n Classes of errors returned from ops should be registered via Deno.core.registerErrorClass().`, + ); + } + throw new ErrorClass(res.err.message); + } + return res.result; + } + + const scratch32 = new Int32Array(3); + const scratchBytes = new Uint8Array( + scratch32.buffer, + scratch32.byteOffset, + scratch32.byteLength, + ); + util.assert(scratchBytes.byteLength === scratch32.length * 4); + + function asyncMsgFromRust(ui8) { + const record = recordFromBufMinimal(ui8); + const { promiseId } = record; + const promise = promiseTableMin[promiseId]; + delete promiseTableMin[promiseId]; + util.assert(promise); + promise.resolve(record); + } + + async function sendAsync(opName, arg, zeroCopy) { + const promiseId = nextPromiseId(); // AKA cmdId + scratch32[0] = promiseId; + scratch32[1] = arg; + scratch32[2] = 0; // result + const promise = util.createResolvable(); + const buf = core.dispatchByName(opName, scratchBytes, zeroCopy); + if (buf != null) { + const record = recordFromBufMinimal(buf); + // Sync result. + promise.resolve(record); + } else { + // Async result. + promiseTableMin[promiseId] = promise; + } + + const res = await promise; + return unwrapResponse(res); + } + + function sendSync(opName, arg, zeroCopy) { + scratch32[0] = 0; // promiseId 0 indicates sync + scratch32[1] = arg; + const res = core.dispatchByName(opName, scratchBytes, zeroCopy); + const resRecord = recordFromBufMinimal(res); + return unwrapResponse(resRecord); + } + + window.__bootstrap.dispatchMinimal = { + asyncMsgFromRust, + sendSync, + sendAsync, + }; +})(this); diff --git a/runtime/rt/11_timers.js b/runtime/rt/11_timers.js new file mode 100644 index 000000000..5a59844a3 --- /dev/null +++ b/runtime/rt/11_timers.js @@ -0,0 +1,557 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const assert = window.__bootstrap.util.assert; + const core = window.Deno.core; + const { sendSync } = window.__bootstrap.dispatchMinimal; + + function opStopGlobalTimer() { + core.jsonOpSync("op_global_timer_stop"); + } + + function opStartGlobalTimer(timeout) { + return core.jsonOpSync("op_global_timer_start", { timeout }); + } + + async function opWaitGlobalTimer() { + await core.jsonOpAsync("op_global_timer"); + } + + const nowBytes = new Uint8Array(8); + function opNow() { + sendSync("op_now", 0, nowBytes); + return new DataView(nowBytes.buffer).getFloat64(); + } + + function sleepSync(millis = 0) { + return core.jsonOpSync("op_sleep_sync", { millis }); + } + + // Derived from https://github.com/vadimg/js_bintrees. MIT Licensed. + + class RBNode { + constructor(data) { + this.data = data; + this.left = null; + this.right = null; + this.red = true; + } + + getChild(dir) { + return dir ? this.right : this.left; + } + + setChild(dir, val) { + if (dir) { + this.right = val; + } else { + this.left = val; + } + } + } + + class RBTree { + #comparator = null; + #root = null; + + constructor(comparator) { + this.#comparator = comparator; + this.#root = null; + } + + /** Returns `null` if tree is empty. */ + min() { + let res = this.#root; + if (res === null) { + return null; + } + while (res.left !== null) { + res = res.left; + } + return res.data; + } + + /** Returns node `data` if found, `null` otherwise. */ + find(data) { + let res = this.#root; + while (res !== null) { + const c = this.#comparator(data, res.data); + if (c === 0) { + return res.data; + } else { + res = res.getChild(c > 0); + } + } + return null; + } + + /** returns `true` if inserted, `false` if duplicate. */ + insert(data) { + let ret = false; + + if (this.#root === null) { + // empty tree + this.#root = new RBNode(data); + ret = true; + } else { + const head = new RBNode(null); // fake tree root + + let dir = 0; + let last = 0; + + // setup + let gp = null; // grandparent + let ggp = head; // grand-grand-parent + let p = null; // parent + let node = this.#root; + ggp.right = this.#root; + + // search down + while (true) { + if (node === null) { + // insert new node at the bottom + node = new RBNode(data); + p.setChild(dir, node); + ret = true; + } else if (isRed(node.left) && isRed(node.right)) { + // color flip + node.red = true; + node.left.red = false; + node.right.red = false; + } + + // fix red violation + if (isRed(node) && isRed(p)) { + const dir2 = ggp.right === gp; + + assert(gp); + if (node === p.getChild(last)) { + ggp.setChild(dir2, singleRotate(gp, !last)); + } else { + ggp.setChild(dir2, doubleRotate(gp, !last)); + } + } + + const cmp = this.#comparator(node.data, data); + + // stop if found + if (cmp === 0) { + break; + } + + last = dir; + dir = Number(cmp < 0); // Fix type + + // update helpers + if (gp !== null) { + ggp = gp; + } + gp = p; + p = node; + node = node.getChild(dir); + } + + // update root + this.#root = head.right; + } + + // make root black + this.#root.red = false; + + return ret; + } + + /** Returns `true` if removed, `false` if not found. */ + remove(data) { + if (this.#root === null) { + return false; + } + + const head = new RBNode(null); // fake tree root + let node = head; + node.right = this.#root; + let p = null; // parent + let gp = null; // grand parent + let found = null; // found item + let dir = 1; + + while (node.getChild(dir) !== null) { + const last = dir; + + // update helpers + gp = p; + p = node; + node = node.getChild(dir); + + const cmp = this.#comparator(data, node.data); + + dir = cmp > 0; + + // save found node + if (cmp === 0) { + found = node; + } + + // push the red node down + if (!isRed(node) && !isRed(node.getChild(dir))) { + if (isRed(node.getChild(!dir))) { + const sr = singleRotate(node, dir); + p.setChild(last, sr); + p = sr; + } else if (!isRed(node.getChild(!dir))) { + const sibling = p.getChild(!last); + if (sibling !== null) { + if ( + !isRed(sibling.getChild(!last)) && + !isRed(sibling.getChild(last)) + ) { + // color flip + p.red = false; + sibling.red = true; + node.red = true; + } else { + assert(gp); + const dir2 = gp.right === p; + + if (isRed(sibling.getChild(last))) { + gp.setChild(dir2, doubleRotate(p, last)); + } else if (isRed(sibling.getChild(!last))) { + gp.setChild(dir2, singleRotate(p, last)); + } + + // ensure correct coloring + const gpc = gp.getChild(dir2); + assert(gpc); + gpc.red = true; + node.red = true; + assert(gpc.left); + gpc.left.red = false; + assert(gpc.right); + gpc.right.red = false; + } + } + } + } + } + + // replace and remove if found + if (found !== null) { + found.data = node.data; + assert(p); + p.setChild(p.right === node, node.getChild(node.left === null)); + } + + // update root and make it black + this.#root = head.right; + if (this.#root !== null) { + this.#root.red = false; + } + + return found !== null; + } + } + + function isRed(node) { + return node !== null && node.red; + } + + function singleRotate(root, dir) { + const save = root.getChild(!dir); + assert(save); + + root.setChild(!dir, save.getChild(dir)); + save.setChild(dir, root); + + root.red = true; + save.red = false; + + return save; + } + + function doubleRotate(root, dir) { + root.setChild(!dir, singleRotate(root.getChild(!dir), !dir)); + return singleRotate(root, dir); + } + + const { console } = globalThis; + const OriginalDate = Date; + + // Timeout values > TIMEOUT_MAX are set to 1. + const TIMEOUT_MAX = 2 ** 31 - 1; + + let globalTimeoutDue = null; + + let nextTimerId = 1; + const idMap = new Map(); + const dueTree = new RBTree((a, b) => a.due - b.due); + + function clearGlobalTimeout() { + globalTimeoutDue = null; + opStopGlobalTimer(); + } + + let pendingEvents = 0; + const pendingFireTimers = []; + + /** Process and run a single ready timer macrotask. + * This function should be registered through Deno.core.setMacrotaskCallback. + * Returns true when all ready macrotasks have been processed, false if more + * ready ones are available. The Isolate future would rely on the return value + * to repeatedly invoke this function until depletion. Multiple invocations + * of this function one at a time ensures newly ready microtasks are processed + * before next macrotask timer callback is invoked. */ + function handleTimerMacrotask() { + if (pendingFireTimers.length > 0) { + fire(pendingFireTimers.shift()); + return pendingFireTimers.length === 0; + } + return true; + } + + async function setGlobalTimeout(due, now) { + // Since JS and Rust don't use the same clock, pass the time to rust as a + // relative time value. On the Rust side we'll turn that into an absolute + // value again. + const timeout = due - now; + assert(timeout >= 0); + // Send message to the backend. + globalTimeoutDue = due; + pendingEvents++; + // FIXME(bartlomieju): this is problematic, because `clearGlobalTimeout` + // is synchronous. That means that timer is cancelled, but this promise is still pending + // until next turn of event loop. This leads to "leaking of async ops" in tests; + // because `clearTimeout/clearInterval` might be the last statement in test function + // `opSanitizer` will immediately complain that there is pending op going on, unless + // some timeout/defer is put in place to allow promise resolution. + // Ideally `clearGlobalTimeout` doesn't return until this op is resolved, but + // I'm not if that's possible. + opStartGlobalTimer(timeout); + await opWaitGlobalTimer(); + pendingEvents--; + // eslint-disable-next-line @typescript-eslint/no-use-before-define + prepareReadyTimers(); + } + + function prepareReadyTimers() { + const now = OriginalDate.now(); + // Bail out if we're not expecting the global timer to fire. + if (globalTimeoutDue === null || pendingEvents > 0) { + return; + } + // After firing the timers that are due now, this will hold the first timer + // list that hasn't fired yet. + let nextDueNode; + while ((nextDueNode = dueTree.min()) !== null && nextDueNode.due <= now) { + dueTree.remove(nextDueNode); + // Fire all the timers in the list. + for (const timer of nextDueNode.timers) { + // With the list dropped, the timer is no longer scheduled. + timer.scheduled = false; + // Place the callback to pending timers to fire. + pendingFireTimers.push(timer); + } + } + setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, now); + } + + function setOrClearGlobalTimeout(due, now) { + if (due == null) { + clearGlobalTimeout(); + } else { + setGlobalTimeout(due, now); + } + } + + function schedule(timer, now) { + assert(!timer.scheduled); + assert(now <= timer.due); + // Find or create the list of timers that will fire at point-in-time `due`. + const maybeNewDueNode = { due: timer.due, timers: [] }; + let dueNode = dueTree.find(maybeNewDueNode); + if (dueNode === null) { + dueTree.insert(maybeNewDueNode); + dueNode = maybeNewDueNode; + } + // Append the newly scheduled timer to the list and mark it as scheduled. + dueNode.timers.push(timer); + timer.scheduled = true; + // If the new timer is scheduled to fire before any timer that existed before, + // update the global timeout to reflect this. + if (globalTimeoutDue === null || globalTimeoutDue > timer.due) { + setOrClearGlobalTimeout(timer.due, now); + } + } + + function unschedule(timer) { + // Check if our timer is pending scheduling or pending firing. + // If either is true, they are not in tree, and their idMap entry + // will be deleted soon. Remove it from queue. + let index = -1; + if ((index = pendingFireTimers.indexOf(timer)) >= 0) { + pendingFireTimers.splice(index); + return; + } + // If timer is not in the 2 pending queues and is unscheduled, + // it is not in the tree. + if (!timer.scheduled) { + return; + } + const searchKey = { due: timer.due, timers: [] }; + // Find the list of timers that will fire at point-in-time `due`. + const list = dueTree.find(searchKey).timers; + if (list.length === 1) { + // Time timer is the only one in the list. Remove the entire list. + assert(list[0] === timer); + dueTree.remove(searchKey); + // If the unscheduled timer was 'next up', find when the next timer that + // still exists is due, and update the global alarm accordingly. + if (timer.due === globalTimeoutDue) { + const nextDueNode = dueTree.min(); + setOrClearGlobalTimeout( + nextDueNode && nextDueNode.due, + OriginalDate.now(), + ); + } + } else { + // Multiple timers that are due at the same point in time. + // Remove this timer from the list. + const index = list.indexOf(timer); + assert(index > -1); + list.splice(index, 1); + } + } + + function fire(timer) { + // If the timer isn't found in the ID map, that means it has been cancelled + // between the timer firing and the promise callback (this function). + if (!idMap.has(timer.id)) { + return; + } + // Reschedule the timer if it is a repeating one, otherwise drop it. + if (!timer.repeat) { + // One-shot timer: remove the timer from this id-to-timer map. + idMap.delete(timer.id); + } else { + // Interval timer: compute when timer was supposed to fire next. + // However make sure to never schedule the next interval in the past. + const now = OriginalDate.now(); + timer.due = Math.max(now, timer.due + timer.delay); + schedule(timer, now); + } + // Call the user callback. Intermediate assignment is to avoid leaking `this` + // to it, while also keeping the stack trace neat when it shows up in there. + const callback = timer.callback; + callback(); + } + + function checkThis(thisArg) { + if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis) { + throw new TypeError("Illegal invocation"); + } + } + + function checkBigInt(n) { + if (typeof n === "bigint") { + throw new TypeError("Cannot convert a BigInt value to a number"); + } + } + + function setTimer( + cb, + delay, + args, + repeat, + ) { + // Bind `args` to the callback and bind `this` to globalThis(global). + const callback = cb.bind(globalThis, ...args); + // In the browser, the delay value must be coercible to an integer between 0 + // and INT32_MAX. Any other value will cause the timer to fire immediately. + // We emulate this behavior. + const now = OriginalDate.now(); + if (delay > TIMEOUT_MAX) { + console.warn( + `${delay} does not fit into` + + " a 32-bit signed integer." + + "\nTimeout duration was set to 1.", + ); + delay = 1; + } + delay = Math.max(0, delay | 0); + + // Create a new, unscheduled timer object. + const timer = { + id: nextTimerId++, + callback, + args, + delay, + due: now + delay, + repeat, + scheduled: false, + }; + // Register the timer's existence in the id-to-timer map. + idMap.set(timer.id, timer); + // Schedule the timer in the due table. + schedule(timer, now); + return timer.id; + } + + function setTimeout( + cb, + delay = 0, + ...args + ) { + checkBigInt(delay); + checkThis(this); + return setTimer(cb, delay, args, false); + } + + function setInterval( + cb, + delay = 0, + ...args + ) { + checkBigInt(delay); + checkThis(this); + return setTimer(cb, delay, args, true); + } + + function clearTimer(id) { + id = Number(id); + const timer = idMap.get(id); + if (timer === undefined) { + // Timer doesn't exist any more or never existed. This is not an error. + return; + } + // Unschedule the timer if it is currently scheduled, and forget about it. + unschedule(timer); + idMap.delete(timer.id); + } + + function clearTimeout(id = 0) { + checkBigInt(id); + if (id === 0) { + return; + } + clearTimer(id); + } + + function clearInterval(id = 0) { + checkBigInt(id); + if (id === 0) { + return; + } + clearTimer(id); + } + + window.__bootstrap.timers = { + clearInterval, + setInterval, + clearTimeout, + setTimeout, + handleTimerMacrotask, + opStopGlobalTimer, + opStartGlobalTimer, + opNow, + sleepSync, + }; +})(this); diff --git a/runtime/rt/11_workers.js b/runtime/rt/11_workers.js new file mode 100644 index 000000000..62210dfae --- /dev/null +++ b/runtime/rt/11_workers.js @@ -0,0 +1,206 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + const { Window } = window.__bootstrap.globalInterfaces; + const { log } = window.__bootstrap.util; + const { defineEventHandler } = window.__bootstrap.webUtil; + + function createWorker( + specifier, + hasSourceCode, + sourceCode, + useDenoNamespace, + name, + ) { + return core.jsonOpSync("op_create_worker", { + specifier, + hasSourceCode, + sourceCode, + name, + useDenoNamespace, + }); + } + + function hostTerminateWorker(id) { + core.jsonOpSync("op_host_terminate_worker", { id }); + } + + function hostPostMessage(id, data) { + core.jsonOpSync("op_host_post_message", { id }, data); + } + + function hostGetMessage(id) { + return core.jsonOpAsync("op_host_get_message", { id }); + } + + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + + function encodeMessage(data) { + const dataJson = JSON.stringify(data); + return encoder.encode(dataJson); + } + + function decodeMessage(dataIntArray) { + const dataJson = decoder.decode(dataIntArray); + return JSON.parse(dataJson); + } + + class Worker extends EventTarget { + #id = 0; + #name = ""; + #terminated = false; + + constructor(specifier, options) { + super(); + const { type = "classic", name = "unknown" } = options ?? {}; + + if (type !== "module") { + throw new Error( + 'Not yet implemented: only "module" type workers are supported', + ); + } + + this.#name = name; + const hasSourceCode = false; + const sourceCode = decoder.decode(new Uint8Array()); + + const useDenoNamespace = options ? !!options.deno : false; + + const { id } = createWorker( + specifier, + hasSourceCode, + sourceCode, + useDenoNamespace, + options?.name, + ); + this.#id = id; + this.#poll(); + } + + #handleMessage = (msgData) => { + let data; + try { + data = decodeMessage(new Uint8Array(msgData)); + } catch (e) { + const msgErrorEvent = new MessageEvent("messageerror", { + cancelable: false, + data, + }); + return; + } + + const msgEvent = new MessageEvent("message", { + cancelable: false, + data, + }); + + this.dispatchEvent(msgEvent); + }; + + #handleError = (e) => { + const event = new ErrorEvent("error", { + cancelable: true, + message: e.message, + lineno: e.lineNumber ? e.lineNumber + 1 : undefined, + colno: e.columnNumber ? e.columnNumber + 1 : undefined, + filename: e.fileName, + error: null, + }); + + let handled = false; + + this.dispatchEvent(event); + if (event.defaultPrevented) { + handled = true; + } + + return handled; + }; + + #poll = async () => { + while (!this.#terminated) { + const event = await hostGetMessage(this.#id); + + // If terminate was called then we ignore all messages + if (this.#terminated) { + return; + } + + const type = event.type; + + if (type === "terminalError") { + this.#terminated = true; + if (!this.#handleError(event.error)) { + if (globalThis instanceof Window) { + throw new Error("Unhandled error event reached main worker."); + } else { + core.jsonOpSync( + "op_host_unhandled_error", + { message: event.error.message }, + ); + } + } + continue; + } + + if (type === "msg") { + this.#handleMessage(event.data); + continue; + } + + if (type === "error") { + if (!this.#handleError(event.error)) { + if (globalThis instanceof Window) { + throw new Error("Unhandled error event reached main worker."); + } else { + core.jsonOpSync( + "op_host_unhandled_error", + { message: event.error.message }, + ); + } + } + continue; + } + + if (type === "close") { + log(`Host got "close" message from worker: ${this.#name}`); + this.#terminated = true; + return; + } + + throw new Error(`Unknown worker event: "${type}"`); + } + }; + + postMessage(message, transferOrOptions) { + if (transferOrOptions) { + throw new Error( + "Not yet implemented: `transfer` and `options` are not supported.", + ); + } + + if (this.#terminated) { + return; + } + + hostPostMessage(this.#id, encodeMessage(message)); + } + + terminate() { + if (!this.#terminated) { + this.#terminated = true; + hostTerminateWorker(this.#id); + } + } + } + + defineEventHandler(Worker.prototype, "error"); + defineEventHandler(Worker.prototype, "message"); + defineEventHandler(Worker.prototype, "messageerror"); + + window.__bootstrap.worker = { + Worker, + }; +})(this); diff --git a/runtime/rt/12_io.js b/runtime/rt/12_io.js new file mode 100644 index 000000000..006d51cdd --- /dev/null +++ b/runtime/rt/12_io.js @@ -0,0 +1,135 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// Interfaces 100% copied from Go. +// Documentation liberally lifted from them too. +// Thank you! We love Go! <3 + +((window) => { + const DEFAULT_BUFFER_SIZE = 32 * 1024; + const { sendSync, sendAsync } = window.__bootstrap.dispatchMinimal; + // Seek whence values. + // https://golang.org/pkg/io/#pkg-constants + const SeekMode = { + 0: "Start", + 1: "Current", + 2: "End", + + Start: 0, + Current: 1, + End: 2, + }; + + async function copy( + src, + dst, + options, + ) { + let n = 0; + const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE; + const b = new Uint8Array(bufSize); + let gotEOF = false; + while (gotEOF === false) { + const result = await src.read(b); + if (result === null) { + gotEOF = true; + } else { + let nwritten = 0; + while (nwritten < result) { + nwritten += await dst.write(b.subarray(nwritten, result)); + } + n += nwritten; + } + } + return n; + } + + async function* iter( + r, + options, + ) { + const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE; + const b = new Uint8Array(bufSize); + while (true) { + const result = await r.read(b); + if (result === null) { + break; + } + + yield b.subarray(0, result); + } + } + + function* iterSync( + r, + options, + ) { + const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE; + const b = new Uint8Array(bufSize); + while (true) { + const result = r.readSync(b); + if (result === null) { + break; + } + + yield b.subarray(0, result); + } + } + + function readSync(rid, buffer) { + if (buffer.length === 0) { + return 0; + } + + const nread = sendSync("op_read", rid, buffer); + if (nread < 0) { + throw new Error("read error"); + } + + return nread === 0 ? null : nread; + } + + async function read( + rid, + buffer, + ) { + if (buffer.length === 0) { + return 0; + } + + const nread = await sendAsync("op_read", rid, buffer); + if (nread < 0) { + throw new Error("read error"); + } + + return nread === 0 ? null : nread; + } + + function writeSync(rid, data) { + const result = sendSync("op_write", rid, data); + if (result < 0) { + throw new Error("write error"); + } + + return result; + } + + async function write(rid, data) { + const result = await sendAsync("op_write", rid, data); + if (result < 0) { + throw new Error("write error"); + } + + return result; + } + + window.__bootstrap.io = { + iterSync, + iter, + copy, + SeekMode, + read, + readSync, + write, + writeSync, + }; +})(this); diff --git a/runtime/rt/13_buffer.js b/runtime/rt/13_buffer.js new file mode 100644 index 000000000..e06e2138b --- /dev/null +++ b/runtime/rt/13_buffer.js @@ -0,0 +1,241 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// This code has been ported almost directly from Go's src/bytes/buffer.go +// Copyright 2009 The Go Authors. All rights reserved. BSD license. +// https://github.com/golang/go/blob/master/LICENSE + +((window) => { + const { assert } = window.__bootstrap.util; + + // MIN_READ is the minimum ArrayBuffer size passed to a read call by + // buffer.ReadFrom. As long as the Buffer has at least MIN_READ bytes beyond + // what is required to hold the contents of r, readFrom() will not grow the + // underlying buffer. + const MIN_READ = 32 * 1024; + const MAX_SIZE = 2 ** 32 - 2; + + // `off` is the offset into `dst` where it will at which to begin writing values + // from `src`. + // Returns the number of bytes copied. + function copyBytes(src, dst, off = 0) { + const r = dst.byteLength - off; + if (src.byteLength > r) { + src = src.subarray(0, r); + } + dst.set(src, off); + return src.byteLength; + } + + class Buffer { + #buf = null; // contents are the bytes buf[off : len(buf)] + #off = 0; // read at buf[off], write at buf[buf.byteLength] + + constructor(ab) { + if (ab == null) { + this.#buf = new Uint8Array(0); + return; + } + + this.#buf = new Uint8Array(ab); + } + + bytes(options = { copy: true }) { + if (options.copy === false) return this.#buf.subarray(this.#off); + return this.#buf.slice(this.#off); + } + + empty() { + return this.#buf.byteLength <= this.#off; + } + + get length() { + return this.#buf.byteLength - this.#off; + } + + get capacity() { + return this.#buf.buffer.byteLength; + } + + truncate(n) { + if (n === 0) { + this.reset(); + return; + } + if (n < 0 || n > this.length) { + throw Error("bytes.Buffer: truncation out of range"); + } + this.#reslice(this.#off + n); + } + + reset() { + this.#reslice(0); + this.#off = 0; + } + + #tryGrowByReslice = (n) => { + const l = this.#buf.byteLength; + if (n <= this.capacity - l) { + this.#reslice(l + n); + return l; + } + return -1; + }; + + #reslice = (len) => { + assert(len <= this.#buf.buffer.byteLength); + this.#buf = new Uint8Array(this.#buf.buffer, 0, len); + }; + + readSync(p) { + if (this.empty()) { + // Buffer is empty, reset to recover space. + this.reset(); + if (p.byteLength === 0) { + // this edge case is tested in 'bufferReadEmptyAtEOF' test + return 0; + } + return null; + } + const nread = copyBytes(this.#buf.subarray(this.#off), p); + this.#off += nread; + return nread; + } + + read(p) { + const rr = this.readSync(p); + return Promise.resolve(rr); + } + + writeSync(p) { + const m = this.#grow(p.byteLength); + return copyBytes(p, this.#buf, m); + } + + write(p) { + const n = this.writeSync(p); + return Promise.resolve(n); + } + + #grow = (n) => { + const m = this.length; + // If buffer is empty, reset to recover space. + if (m === 0 && this.#off !== 0) { + this.reset(); + } + // Fast: Try to grow by means of a reslice. + const i = this.#tryGrowByReslice(n); + if (i >= 0) { + return i; + } + const c = this.capacity; + if (n <= Math.floor(c / 2) - m) { + // We can slide things down instead of allocating a new + // ArrayBuffer. We only need m+n <= c to slide, but + // we instead let capacity get twice as large so we + // don't spend all our time copying. + copyBytes(this.#buf.subarray(this.#off), this.#buf); + } else if (c + n > MAX_SIZE) { + throw new Error("The buffer cannot be grown beyond the maximum size."); + } else { + // Not enough space anywhere, we need to allocate. + const buf = new Uint8Array(Math.min(2 * c + n, MAX_SIZE)); + copyBytes(this.#buf.subarray(this.#off), buf); + this.#buf = buf; + } + // Restore this.#off and len(this.#buf). + this.#off = 0; + this.#reslice(Math.min(m + n, MAX_SIZE)); + return m; + }; + + grow(n) { + if (n < 0) { + throw Error("Buffer.grow: negative count"); + } + const m = this.#grow(n); + this.#reslice(m); + } + + async readFrom(r) { + let n = 0; + const tmp = new Uint8Array(MIN_READ); + while (true) { + const shouldGrow = this.capacity - this.length < MIN_READ; + // read into tmp buffer if there's not enough room + // otherwise read directly into the internal buffer + const buf = shouldGrow + ? tmp + : new Uint8Array(this.#buf.buffer, this.length); + + const nread = await r.read(buf); + if (nread === null) { + return n; + } + + // write will grow if needed + if (shouldGrow) this.writeSync(buf.subarray(0, nread)); + else this.#reslice(this.length + nread); + + n += nread; + } + } + + readFromSync(r) { + let n = 0; + const tmp = new Uint8Array(MIN_READ); + while (true) { + const shouldGrow = this.capacity - this.length < MIN_READ; + // read into tmp buffer if there's not enough room + // otherwise read directly into the internal buffer + const buf = shouldGrow + ? tmp + : new Uint8Array(this.#buf.buffer, this.length); + + const nread = r.readSync(buf); + if (nread === null) { + return n; + } + + // write will grow if needed + if (shouldGrow) this.writeSync(buf.subarray(0, nread)); + else this.#reslice(this.length + nread); + + n += nread; + } + } + } + + async function readAll(r) { + const buf = new Buffer(); + await buf.readFrom(r); + return buf.bytes(); + } + + function readAllSync(r) { + const buf = new Buffer(); + buf.readFromSync(r); + return buf.bytes(); + } + + async function writeAll(w, arr) { + let nwritten = 0; + while (nwritten < arr.length) { + nwritten += await w.write(arr.subarray(nwritten)); + } + } + + function writeAllSync(w, arr) { + let nwritten = 0; + while (nwritten < arr.length) { + nwritten += w.writeSync(arr.subarray(nwritten)); + } + } + + window.__bootstrap.buffer = { + writeAll, + writeAllSync, + readAll, + readAllSync, + Buffer, + }; +})(this); diff --git a/runtime/rt/27_websocket.js b/runtime/rt/27_websocket.js new file mode 100644 index 000000000..60428c24d --- /dev/null +++ b/runtime/rt/27_websocket.js @@ -0,0 +1,316 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + const { requiredArguments, defineEventHandler } = window.__bootstrap.webUtil; + const CONNECTING = 0; + const OPEN = 1; + const CLOSING = 2; + const CLOSED = 3; + + class WebSocket extends EventTarget { + #readyState = CONNECTING; + + constructor(url, protocols = []) { + super(); + requiredArguments("WebSocket", arguments.length, 1); + + const wsURL = new URL(url); + + if (wsURL.protocol !== "ws:" && wsURL.protocol !== "wss:") { + throw new DOMException( + "Only ws & wss schemes are allowed in a WebSocket URL.", + "SyntaxError", + ); + } + + if (wsURL.hash !== "" || wsURL.href.endsWith("#")) { + throw new DOMException( + "Fragments are not allowed in a WebSocket URL.", + "SyntaxError", + ); + } + + this.#url = wsURL.href; + + core.jsonOpSync("op_ws_check_permission", { + url: this.#url, + }); + + if (protocols && typeof protocols === "string") { + protocols = [protocols]; + } + + if ( + protocols.some((x) => protocols.indexOf(x) !== protocols.lastIndexOf(x)) + ) { + throw new DOMException( + "Can't supply multiple times the same protocol.", + "SyntaxError", + ); + } + + core.jsonOpAsync("op_ws_create", { + url: wsURL.href, + protocols: protocols.join(", "), + }).then((create) => { + if (create.success) { + this.#rid = create.rid; + this.#extensions = create.extensions; + this.#protocol = create.protocol; + + if (this.#readyState === CLOSING) { + core.jsonOpAsync("op_ws_close", { + rid: this.#rid, + }).then(() => { + this.#readyState = CLOSED; + + const errEvent = new ErrorEvent("error"); + errEvent.target = this; + this.dispatchEvent(errEvent); + + const event = new CloseEvent("close"); + event.target = this; + this.dispatchEvent(event); + core.close(this.#rid); + }); + } else { + this.#readyState = OPEN; + const event = new Event("open"); + event.target = this; + this.dispatchEvent(event); + + this.#eventLoop(); + } + } else { + this.#readyState = CLOSED; + + const errEvent = new ErrorEvent("error"); + errEvent.target = this; + this.dispatchEvent(errEvent); + + const closeEvent = new CloseEvent("close"); + closeEvent.target = this; + this.dispatchEvent(closeEvent); + } + }).catch((err) => { + this.#readyState = CLOSED; + + const errorEv = new ErrorEvent( + "error", + { error: err, message: err.toString() }, + ); + errorEv.target = this; + this.dispatchEvent(errorEv); + + const closeEv = new CloseEvent("close"); + closeEv.target = this; + this.dispatchEvent(closeEv); + }); + } + + get CONNECTING() { + return CONNECTING; + } + get OPEN() { + return OPEN; + } + get CLOSING() { + return CLOSING; + } + get CLOSED() { + return CLOSED; + } + + get readyState() { + return this.#readyState; + } + + #extensions = ""; + #protocol = ""; + #url = ""; + #rid; + + get extensions() { + return this.#extensions; + } + get protocol() { + return this.#protocol; + } + + #binaryType = "blob"; + get binaryType() { + return this.#binaryType; + } + set binaryType(value) { + if (value === "blob" || value === "arraybuffer") { + this.#binaryType = value; + } + } + #bufferedAmount = 0; + get bufferedAmount() { + return this.#bufferedAmount; + } + + get url() { + return this.#url; + } + + send(data) { + requiredArguments("WebSocket.send", arguments.length, 1); + + if (this.#readyState != OPEN) { + throw Error("readyState not OPEN"); + } + + const sendTypedArray = (ta) => { + this.#bufferedAmount += ta.size; + core.jsonOpAsync("op_ws_send", { + rid: this.#rid, + }, ta).then(() => { + this.#bufferedAmount -= ta.size; + }); + }; + + if (data instanceof Blob) { + data.slice().arrayBuffer().then((ab) => + sendTypedArray(new DataView(ab)) + ); + } else if ( + data instanceof Int8Array || data instanceof Int16Array || + data instanceof Int32Array || data instanceof Uint8Array || + data instanceof Uint16Array || data instanceof Uint32Array || + data instanceof Uint8ClampedArray || data instanceof Float32Array || + data instanceof Float64Array || data instanceof DataView + ) { + sendTypedArray(data); + } else if (data instanceof ArrayBuffer) { + sendTypedArray(new DataView(data)); + } else { + const string = String(data); + const encoder = new TextEncoder(); + const d = encoder.encode(string); + this.#bufferedAmount += d.size; + core.jsonOpAsync("op_ws_send", { + rid: this.#rid, + text: string, + }).then(() => { + this.#bufferedAmount -= d.size; + }); + } + } + + close(code, reason) { + if (code && (code !== 1000 && !(3000 <= code > 5000))) { + throw new DOMException( + "The close code must be either 1000 or in the range of 3000 to 4999.", + "NotSupportedError", + ); + } + + const encoder = new TextEncoder(); + if (reason && encoder.encode(reason).byteLength > 123) { + throw new DOMException( + "The close reason may not be longer than 123 bytes.", + "SyntaxError", + ); + } + + if (this.#readyState === CONNECTING) { + this.#readyState = CLOSING; + } else if (this.#readyState === OPEN) { + this.#readyState = CLOSING; + + core.jsonOpAsync("op_ws_close", { + rid: this.#rid, + code, + reason, + }).then(() => { + this.#readyState = CLOSED; + const event = new CloseEvent("close", { + wasClean: true, + code, + reason, + }); + event.target = this; + this.dispatchEvent(event); + core.close(this.#rid); + }); + } + } + + async #eventLoop() { + if (this.#readyState === OPEN) { + const message = await core.jsonOpAsync( + "op_ws_next_event", + { rid: this.#rid }, + ); + if (message.type === "string" || message.type === "binary") { + let data; + + if (message.type === "string") { + data = message.data; + } else { + if (this.binaryType === "blob") { + data = new Blob([new Uint8Array(message.data)]); + } else { + data = new Uint8Array(message.data).buffer; + } + } + + const event = new MessageEvent("message", { + data, + origin: this.#url, + }); + event.target = this; + this.dispatchEvent(event); + + this.#eventLoop(); + } else if (message.type === "close") { + this.#readyState = CLOSED; + const event = new CloseEvent("close", { + wasClean: true, + code: message.code, + reason: message.reason, + }); + event.target = this; + this.dispatchEvent(event); + } else if (message.type === "error") { + this.#readyState = CLOSED; + + const errorEv = new ErrorEvent("error"); + errorEv.target = this; + this.dispatchEvent(errorEv); + + this.#readyState = CLOSED; + const closeEv = new CloseEvent("close"); + closeEv.target = this; + this.dispatchEvent(closeEv); + } + } + } + } + + Object.defineProperties(WebSocket, { + CONNECTING: { + value: 0, + }, + OPEN: { + value: 1, + }, + CLOSING: { + value: 2, + }, + CLOSED: { + value: 3, + }, + }); + + defineEventHandler(WebSocket.prototype, "message"); + defineEventHandler(WebSocket.prototype, "error"); + defineEventHandler(WebSocket.prototype, "close"); + defineEventHandler(WebSocket.prototype, "open"); + window.__bootstrap.webSocket = { + WebSocket, + }; +})(this); diff --git a/runtime/rt/30_files.js b/runtime/rt/30_files.js new file mode 100644 index 000000000..679b184fd --- /dev/null +++ b/runtime/rt/30_files.js @@ -0,0 +1,209 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + const { read, readSync, write, writeSync } = window.__bootstrap.io; + const { pathFromURL } = window.__bootstrap.util; + + function seekSync( + rid, + offset, + whence, + ) { + return core.jsonOpSync("op_seek_sync", { rid, offset, whence }); + } + + function seek( + rid, + offset, + whence, + ) { + return core.jsonOpAsync("op_seek_async", { rid, offset, whence }); + } + + function openSync( + path, + options = { read: true }, + ) { + checkOpenOptions(options); + const mode = options?.mode; + const rid = core.jsonOpSync( + "op_open_sync", + { path: pathFromURL(path), options, mode }, + ); + + return new File(rid); + } + + async function open( + path, + options = { read: true }, + ) { + checkOpenOptions(options); + const mode = options?.mode; + const rid = await core.jsonOpAsync( + "op_open_async", + { path: pathFromURL(path), options, mode }, + ); + + return new File(rid); + } + + function createSync(path) { + return openSync(path, { + read: true, + write: true, + truncate: true, + create: true, + }); + } + + function create(path) { + return open(path, { + read: true, + write: true, + truncate: true, + create: true, + }); + } + + class File { + #rid = 0; + + constructor(rid) { + this.#rid = rid; + } + + get rid() { + return this.#rid; + } + + write(p) { + return write(this.rid, p); + } + + writeSync(p) { + return writeSync(this.rid, p); + } + + read(p) { + return read(this.rid, p); + } + + readSync(p) { + return readSync(this.rid, p); + } + + seek(offset, whence) { + return seek(this.rid, offset, whence); + } + + seekSync(offset, whence) { + return seekSync(this.rid, offset, whence); + } + + close() { + core.close(this.rid); + } + } + + class Stdin { + constructor() { + } + + get rid() { + return 0; + } + + read(p) { + return read(this.rid, p); + } + + readSync(p) { + return readSync(this.rid, p); + } + + close() { + core.close(this.rid); + } + } + + class Stdout { + constructor() { + } + + get rid() { + return 1; + } + + write(p) { + return write(this.rid, p); + } + + writeSync(p) { + return writeSync(this.rid, p); + } + + close() { + core.close(this.rid); + } + } + + class Stderr { + constructor() { + } + + get rid() { + return 2; + } + + write(p) { + return write(this.rid, p); + } + + writeSync(p) { + return writeSync(this.rid, p); + } + + close() { + core.close(this.rid); + } + } + + const stdin = new Stdin(); + const stdout = new Stdout(); + const stderr = new Stderr(); + + function checkOpenOptions(options) { + if (Object.values(options).filter((val) => val === true).length === 0) { + throw new Error("OpenOptions requires at least one option to be true"); + } + + if (options.truncate && !options.write) { + throw new Error("'truncate' option requires 'write' option"); + } + + const createOrCreateNewWithoutWriteOrAppend = + (options.create || options.createNew) && + !(options.write || options.append); + + if (createOrCreateNewWithoutWriteOrAppend) { + throw new Error( + "'create' or 'createNew' options require 'write' or 'append' option", + ); + } + } + + window.__bootstrap.files = { + stdin, + stdout, + stderr, + File, + create, + createSync, + open, + openSync, + seek, + seekSync, + }; +})(this); diff --git a/runtime/rt/30_fs.js b/runtime/rt/30_fs.js new file mode 100644 index 000000000..33fab01e4 --- /dev/null +++ b/runtime/rt/30_fs.js @@ -0,0 +1,425 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + const { pathFromURL } = window.__bootstrap.util; + const build = window.__bootstrap.build.build; + + function chmodSync(path, mode) { + core.jsonOpSync("op_chmod_sync", { path: pathFromURL(path), mode }); + } + + async function chmod(path, mode) { + await core.jsonOpAsync("op_chmod_async", { path: pathFromURL(path), mode }); + } + + function chownSync( + path, + uid, + gid, + ) { + core.jsonOpSync("op_chown_sync", { path: pathFromURL(path), uid, gid }); + } + + async function chown( + path, + uid, + gid, + ) { + await core.jsonOpAsync( + "op_chown_async", + { path: pathFromURL(path), uid, gid }, + ); + } + + function copyFileSync( + fromPath, + toPath, + ) { + core.jsonOpSync("op_copy_file_sync", { + from: pathFromURL(fromPath), + to: pathFromURL(toPath), + }); + } + + async function copyFile( + fromPath, + toPath, + ) { + await core.jsonOpAsync("op_copy_file_async", { + from: pathFromURL(fromPath), + to: pathFromURL(toPath), + }); + } + + function cwd() { + return core.jsonOpSync("op_cwd"); + } + + function chdir(directory) { + core.jsonOpSync("op_chdir", { directory }); + } + + function makeTempDirSync(options = {}) { + return core.jsonOpSync("op_make_temp_dir_sync", options); + } + + function makeTempDir(options = {}) { + return core.jsonOpAsync("op_make_temp_dir_async", options); + } + + function makeTempFileSync(options = {}) { + return core.jsonOpSync("op_make_temp_file_sync", options); + } + + function makeTempFile(options = {}) { + return core.jsonOpAsync("op_make_temp_file_async", options); + } + + function mkdirArgs(path, options) { + const args = { path: pathFromURL(path), recursive: false }; + if (options != null) { + if (typeof options.recursive == "boolean") { + args.recursive = options.recursive; + } + if (options.mode) { + args.mode = options.mode; + } + } + return args; + } + + function mkdirSync(path, options) { + core.jsonOpSync("op_mkdir_sync", mkdirArgs(path, options)); + } + + async function mkdir( + path, + options, + ) { + await core.jsonOpAsync("op_mkdir_async", mkdirArgs(path, options)); + } + + function res(response) { + return response.entries; + } + + function readDirSync(path) { + return res( + core.jsonOpSync("op_read_dir_sync", { path: pathFromURL(path) }), + )[ + Symbol.iterator + ](); + } + + function readDir(path) { + const array = core.jsonOpAsync( + "op_read_dir_async", + { path: pathFromURL(path) }, + ) + .then( + res, + ); + return { + async *[Symbol.asyncIterator]() { + yield* await array; + }, + }; + } + + function readLinkSync(path) { + return core.jsonOpSync("op_read_link_sync", { path: pathFromURL(path) }); + } + + function readLink(path) { + return core.jsonOpAsync("op_read_link_async", { path: pathFromURL(path) }); + } + + function realPathSync(path) { + return core.jsonOpSync("op_realpath_sync", { path }); + } + + function realPath(path) { + return core.jsonOpAsync("op_realpath_async", { path }); + } + + function removeSync( + path, + options = {}, + ) { + core.jsonOpSync("op_remove_sync", { + path: pathFromURL(path), + recursive: !!options.recursive, + }); + } + + async function remove( + path, + options = {}, + ) { + await core.jsonOpAsync("op_remove_async", { + path: pathFromURL(path), + recursive: !!options.recursive, + }); + } + + function renameSync(oldpath, newpath) { + core.jsonOpSync("op_rename_sync", { oldpath, newpath }); + } + + async function rename(oldpath, newpath) { + await core.jsonOpAsync("op_rename_async", { oldpath, newpath }); + } + + function parseFileInfo(response) { + const unix = build.os === "darwin" || build.os === "linux"; + return { + isFile: response.isFile, + isDirectory: response.isDirectory, + isSymlink: response.isSymlink, + size: response.size, + mtime: response.mtime != null ? new Date(response.mtime) : null, + atime: response.atime != null ? new Date(response.atime) : null, + birthtime: response.birthtime != null + ? new Date(response.birthtime) + : null, + // Only non-null if on Unix + dev: unix ? response.dev : null, + ino: unix ? response.ino : null, + mode: unix ? response.mode : null, + nlink: unix ? response.nlink : null, + uid: unix ? response.uid : null, + gid: unix ? response.gid : null, + rdev: unix ? response.rdev : null, + blksize: unix ? response.blksize : null, + blocks: unix ? response.blocks : null, + }; + } + + function fstatSync(rid) { + return parseFileInfo(core.jsonOpSync("op_fstat_sync", { rid })); + } + + async function fstat(rid) { + return parseFileInfo(await core.jsonOpAsync("op_fstat_async", { rid })); + } + + async function lstat(path) { + const res = await core.jsonOpAsync("op_stat_async", { + path: pathFromURL(path), + lstat: true, + }); + return parseFileInfo(res); + } + + function lstatSync(path) { + const res = core.jsonOpSync("op_stat_sync", { + path: pathFromURL(path), + lstat: true, + }); + return parseFileInfo(res); + } + + async function stat(path) { + const res = await core.jsonOpAsync("op_stat_async", { + path: pathFromURL(path), + lstat: false, + }); + return parseFileInfo(res); + } + + function statSync(path) { + const res = core.jsonOpSync("op_stat_sync", { + path: pathFromURL(path), + lstat: false, + }); + return parseFileInfo(res); + } + + function coerceLen(len) { + if (len == null || len < 0) { + return 0; + } + + return len; + } + + function ftruncateSync(rid, len) { + core.jsonOpSync("op_ftruncate_sync", { rid, len: coerceLen(len) }); + } + + async function ftruncate(rid, len) { + await core.jsonOpAsync("op_ftruncate_async", { rid, len: coerceLen(len) }); + } + + function truncateSync(path, len) { + core.jsonOpSync("op_truncate_sync", { path, len: coerceLen(len) }); + } + + async function truncate(path, len) { + await core.jsonOpAsync("op_truncate_async", { path, len: coerceLen(len) }); + } + + function umask(mask) { + return core.jsonOpSync("op_umask", { mask }); + } + + function linkSync(oldpath, newpath) { + core.jsonOpSync("op_link_sync", { oldpath, newpath }); + } + + async function link(oldpath, newpath) { + await core.jsonOpAsync("op_link_async", { oldpath, newpath }); + } + + function toUnixTimeFromEpoch(value) { + if (value instanceof Date) { + const time = value.valueOf(); + const seconds = Math.trunc(time / 1e3); + const nanoseconds = Math.trunc(time - (seconds * 1e3)) * 1e6; + + return [ + seconds, + nanoseconds, + ]; + } + + const seconds = value; + const nanoseconds = 0; + + return [ + seconds, + nanoseconds, + ]; + } + + function futimeSync( + rid, + atime, + mtime, + ) { + core.jsonOpSync("op_futime_sync", { + rid, + atime: toUnixTimeFromEpoch(atime), + mtime: toUnixTimeFromEpoch(mtime), + }); + } + + async function futime( + rid, + atime, + mtime, + ) { + await core.jsonOpAsync("op_futime_async", { + rid, + atime: toUnixTimeFromEpoch(atime), + mtime: toUnixTimeFromEpoch(mtime), + }); + } + + function utimeSync( + path, + atime, + mtime, + ) { + core.jsonOpSync("op_utime_sync", { + path, + atime: toUnixTimeFromEpoch(atime), + mtime: toUnixTimeFromEpoch(mtime), + }); + } + + async function utime( + path, + atime, + mtime, + ) { + await core.jsonOpAsync("op_utime_async", { + path, + atime: toUnixTimeFromEpoch(atime), + mtime: toUnixTimeFromEpoch(mtime), + }); + } + + function symlinkSync( + oldpath, + newpath, + options, + ) { + core.jsonOpSync("op_symlink_sync", { oldpath, newpath, options }); + } + + async function symlink( + oldpath, + newpath, + options, + ) { + await core.jsonOpAsync("op_symlink_async", { oldpath, newpath, options }); + } + + function fdatasyncSync(rid) { + core.jsonOpSync("op_fdatasync_sync", { rid }); + } + + async function fdatasync(rid) { + await core.jsonOpAsync("op_fdatasync_async", { rid }); + } + + function fsyncSync(rid) { + core.jsonOpSync("op_fsync_sync", { rid }); + } + + async function fsync(rid) { + await core.jsonOpAsync("op_fsync_async", { rid }); + } + + window.__bootstrap.fs = { + cwd, + chdir, + chmodSync, + chmod, + chown, + chownSync, + copyFile, + copyFileSync, + makeTempFile, + makeTempDir, + makeTempFileSync, + makeTempDirSync, + mkdir, + mkdirSync, + readDir, + readDirSync, + readLinkSync, + readLink, + realPathSync, + realPath, + remove, + removeSync, + renameSync, + rename, + fstatSync, + fstat, + lstat, + lstatSync, + stat, + statSync, + ftruncate, + ftruncateSync, + truncate, + truncateSync, + umask, + link, + linkSync, + futime, + futimeSync, + utime, + utimeSync, + symlink, + symlinkSync, + fdatasync, + fdatasyncSync, + fsync, + fsyncSync, + }; +})(this); diff --git a/runtime/rt/30_metrics.js b/runtime/rt/30_metrics.js new file mode 100644 index 000000000..d44a629cb --- /dev/null +++ b/runtime/rt/30_metrics.js @@ -0,0 +1,13 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + + function metrics() { + return core.jsonOpSync("op_metrics"); + } + + window.__bootstrap.metrics = { + metrics, + }; +})(this); diff --git a/runtime/rt/30_net.js b/runtime/rt/30_net.js new file mode 100644 index 000000000..9a71f0693 --- /dev/null +++ b/runtime/rt/30_net.js @@ -0,0 +1,245 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + const { errors } = window.__bootstrap.errors; + const { read, write } = window.__bootstrap.io; + + const ShutdownMode = { + // See http://man7.org/linux/man-pages/man2/shutdown.2.html + // Corresponding to SHUT_RD, SHUT_WR, SHUT_RDWR + 0: "Read", + 1: "Write", + 2: "ReadWrite", + Read: 0, + Write: 1, + ReadWrite: 2, // unused + }; + + function shutdown(rid, how) { + core.jsonOpSync("op_shutdown", { rid, how }); + return Promise.resolve(); + } + + function opAccept( + rid, + transport, + ) { + return core.jsonOpAsync("op_accept", { rid, transport }); + } + + function opListen(args) { + return core.jsonOpSync("op_listen", args); + } + + function opConnect(args) { + return core.jsonOpAsync("op_connect", args); + } + + function opReceive( + rid, + transport, + zeroCopy, + ) { + return core.jsonOpAsync( + "op_datagram_receive", + { rid, transport }, + zeroCopy, + ); + } + + function opSend(args, zeroCopy) { + return core.jsonOpAsync("op_datagram_send", args, zeroCopy); + } + + class Conn { + #rid = 0; + #remoteAddr = null; + #localAddr = null; + constructor( + rid, + remoteAddr, + localAddr, + ) { + this.#rid = rid; + this.#remoteAddr = remoteAddr; + this.#localAddr = localAddr; + } + + get rid() { + return this.#rid; + } + + get remoteAddr() { + return this.#remoteAddr; + } + + get localAddr() { + return this.#localAddr; + } + + write(p) { + return write(this.rid, p); + } + + read(p) { + return read(this.rid, p); + } + + close() { + core.close(this.rid); + } + + // TODO(lucacasonato): make this unavailable in stable + closeWrite() { + shutdown(this.rid, ShutdownMode.Write); + } + } + + class Listener { + #rid = 0; + #addr = null; + + constructor(rid, addr) { + this.#rid = rid; + this.#addr = addr; + } + + get rid() { + return this.#rid; + } + + get addr() { + return this.#addr; + } + + async accept() { + const res = await opAccept(this.rid, this.addr.transport); + return new Conn(res.rid, res.remoteAddr, res.localAddr); + } + + async next() { + let conn; + try { + conn = await this.accept(); + } catch (error) { + if (error instanceof errors.BadResource) { + return { value: undefined, done: true }; + } + throw error; + } + return { value: conn, done: false }; + } + + return(value) { + this.close(); + return Promise.resolve({ value, done: true }); + } + + close() { + core.close(this.rid); + } + + [Symbol.asyncIterator]() { + return this; + } + } + + class Datagram { + #rid = 0; + #addr = null; + + constructor( + rid, + addr, + bufSize = 1024, + ) { + this.#rid = rid; + this.#addr = addr; + this.bufSize = bufSize; + } + + get rid() { + return this.#rid; + } + + get addr() { + return this.#addr; + } + + async receive(p) { + const buf = p || new Uint8Array(this.bufSize); + const { size, remoteAddr } = await opReceive( + this.rid, + this.addr.transport, + buf, + ); + const sub = buf.subarray(0, size); + return [sub, remoteAddr]; + } + + send(p, addr) { + const remote = { hostname: "127.0.0.1", ...addr }; + + const args = { ...remote, rid: this.rid }; + return opSend(args, p); + } + + close() { + core.close(this.rid); + } + + async *[Symbol.asyncIterator]() { + while (true) { + try { + yield await this.receive(); + } catch (err) { + if (err instanceof errors.BadResource) { + break; + } + throw err; + } + } + } + } + + function listen({ hostname, ...options }) { + const res = opListen({ + transport: "tcp", + hostname: typeof hostname === "undefined" ? "0.0.0.0" : hostname, + ...options, + }); + + return new Listener(res.rid, res.localAddr); + } + + async function connect( + options, + ) { + let res; + + if (options.transport === "unix") { + res = await opConnect(options); + } else { + res = await opConnect({ + transport: "tcp", + hostname: "127.0.0.1", + ...options, + }); + } + + return new Conn(res.rid, res.remoteAddr, res.localAddr); + } + + window.__bootstrap.net = { + connect, + Conn, + opConnect, + listen, + opListen, + Listener, + shutdown, + ShutdownMode, + Datagram, + }; +})(this); diff --git a/runtime/rt/30_os.js b/runtime/rt/30_os.js new file mode 100644 index 000000000..ebc4e8916 --- /dev/null +++ b/runtime/rt/30_os.js @@ -0,0 +1,66 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + + function loadavg() { + return core.jsonOpSync("op_loadavg"); + } + + function hostname() { + return core.jsonOpSync("op_hostname"); + } + + function osRelease() { + return core.jsonOpSync("op_os_release"); + } + + function systemMemoryInfo() { + return core.jsonOpSync("op_system_memory_info"); + } + + function systemCpuInfo() { + return core.jsonOpSync("op_system_cpu_info"); + } + + function exit(code = 0) { + core.jsonOpSync("op_exit", { code }); + throw new Error("Code not reachable"); + } + + function setEnv(key, value) { + core.jsonOpSync("op_set_env", { key, value }); + } + + function getEnv(key) { + return core.jsonOpSync("op_get_env", { key })[0]; + } + + function deleteEnv(key) { + core.jsonOpSync("op_delete_env", { key }); + } + + const env = { + get: getEnv, + toObject() { + return core.jsonOpSync("op_env"); + }, + set: setEnv, + delete: deleteEnv, + }; + + function execPath() { + return core.jsonOpSync("op_exec_path"); + } + + window.__bootstrap.os = { + env, + execPath, + exit, + osRelease, + systemMemoryInfo, + systemCpuInfo, + hostname, + loadavg, + }; +})(this); diff --git a/runtime/rt/40_compiler_api.js b/runtime/rt/40_compiler_api.js new file mode 100644 index 000000000..ea963b67b --- /dev/null +++ b/runtime/rt/40_compiler_api.js @@ -0,0 +1,97 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// This file contains the runtime APIs which will dispatch work to the internal +// compiler within Deno. +((window) => { + const core = window.Deno.core; + const util = window.__bootstrap.util; + + function opCompile(request) { + return core.jsonOpAsync("op_compile", request); + } + + function opTranspile( + request, + ) { + return core.jsonOpAsync("op_transpile", request); + } + + function checkRelative(specifier) { + return specifier.match(/^([\.\/\\]|https?:\/{2}|file:\/{2})/) + ? specifier + : `./${specifier}`; + } + + // TODO(bartlomieju): change return type to interface? + function transpileOnly( + sources, + options = {}, + ) { + util.log("Deno.transpileOnly", { sources: Object.keys(sources), options }); + const payload = { + sources, + options: JSON.stringify(options), + }; + return opTranspile(payload); + } + + // TODO(bartlomieju): change return type to interface? + async function compile( + rootName, + sources, + options = {}, + ) { + const payload = { + rootName: sources ? rootName : checkRelative(rootName), + sources, + options: JSON.stringify(options), + bundle: false, + }; + util.log("Deno.compile", { + rootName: payload.rootName, + sources: !!sources, + options, + }); + /** @type {{ emittedFiles: Record, diagnostics: any[] }} */ + const result = await opCompile(payload); + util.assert(result.emittedFiles); + const maybeDiagnostics = result.diagnostics.length === 0 + ? undefined + : result.diagnostics; + + return [maybeDiagnostics, result.emittedFiles]; + } + + // TODO(bartlomieju): change return type to interface? + async function bundle( + rootName, + sources, + options = {}, + ) { + const payload = { + rootName: sources ? rootName : checkRelative(rootName), + sources, + options: JSON.stringify(options), + bundle: true, + }; + util.log("Deno.bundle", { + rootName: payload.rootName, + sources: !!sources, + options, + }); + /** @type {{ emittedFiles: Record, diagnostics: any[] }} */ + const result = await opCompile(payload); + const output = result.emittedFiles["deno:///bundle.js"]; + util.assert(output); + const maybeDiagnostics = result.diagnostics.length === 0 + ? undefined + : result.diagnostics; + return [maybeDiagnostics, output]; + } + + window.__bootstrap.compilerApi = { + bundle, + compile, + transpileOnly, + }; +})(this); diff --git a/runtime/rt/40_diagnostics.js b/runtime/rt/40_diagnostics.js new file mode 100644 index 000000000..2b7457853 --- /dev/null +++ b/runtime/rt/40_diagnostics.js @@ -0,0 +1,23 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// Diagnostic provides an abstraction for advice/errors received from a +// compiler, which is strongly influenced by the format of TypeScript +// diagnostics. + +((window) => { + const DiagnosticCategory = { + 0: "Warning", + 1: "Error", + 2: "Suggestion", + 3: "Message", + + Warning: 0, + Error: 1, + Suggestion: 2, + Message: 3, + }; + + window.__bootstrap.diagnostics = { + DiagnosticCategory, + }; +})(this); diff --git a/runtime/rt/40_error_stack.js b/runtime/rt/40_error_stack.js new file mode 100644 index 000000000..da2ee51f3 --- /dev/null +++ b/runtime/rt/40_error_stack.js @@ -0,0 +1,23 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + + function opFormatDiagnostics(diagnostics) { + return core.jsonOpSync("op_format_diagnostic", diagnostics); + } + + function opApplySourceMap(location) { + const res = core.jsonOpSync("op_apply_source_map", location); + return { + fileName: res.fileName, + lineNumber: res.lineNumber, + columnNumber: res.columnNumber, + }; + } + + window.__bootstrap.errorStack = { + opApplySourceMap, + opFormatDiagnostics, + }; +})(this); diff --git a/runtime/rt/40_fs_events.js b/runtime/rt/40_fs_events.js new file mode 100644 index 000000000..a36adecba --- /dev/null +++ b/runtime/rt/40_fs_events.js @@ -0,0 +1,52 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + const { errors } = window.__bootstrap.errors; + + class FsWatcher { + #rid = 0; + + constructor(paths, options) { + const { recursive } = options; + this.#rid = core.jsonOpSync("op_fs_events_open", { recursive, paths }); + } + + get rid() { + return this.#rid; + } + + async next() { + try { + return await core.jsonOpAsync("op_fs_events_poll", { + rid: this.rid, + }); + } catch (error) { + if (error instanceof errors.BadResource) { + return { value: undefined, done: true }; + } + throw error; + } + } + + return(value) { + core.close(this.rid); + return Promise.resolve({ value, done: true }); + } + + [Symbol.asyncIterator]() { + return this; + } + } + + function watchFs( + paths, + options = { recursive: true }, + ) { + return new FsWatcher(Array.isArray(paths) ? paths : [paths], options); + } + + window.__bootstrap.fsEvents = { + watchFs, + }; +})(this); diff --git a/runtime/rt/40_net_unstable.js b/runtime/rt/40_net_unstable.js new file mode 100644 index 000000000..fcc899a30 --- /dev/null +++ b/runtime/rt/40_net_unstable.js @@ -0,0 +1,48 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const net = window.__bootstrap.net; + + function listen(options) { + if (options.transport === "unix") { + const res = net.opListen(options); + return new net.Listener(res.rid, res.localAddr); + } else { + return net.listen(options); + } + } + + function listenDatagram( + options, + ) { + let res; + if (options.transport === "unixpacket") { + res = net.opListen(options); + } else { + res = net.opListen({ + transport: "udp", + hostname: "127.0.0.1", + ...options, + }); + } + + return new net.Datagram(res.rid, res.localAddr); + } + + async function connect( + options, + ) { + if (options.transport === "unix") { + const res = await net.opConnect(options); + return new net.Conn(res.rid, res.remoteAddr, res.localAddr); + } else { + return net.connect(options); + } + } + + window.__bootstrap.netUnstable = { + connect, + listenDatagram, + listen, + }; +})(this); diff --git a/runtime/rt/40_performance.js b/runtime/rt/40_performance.js new file mode 100644 index 000000000..0a63dc704 --- /dev/null +++ b/runtime/rt/40_performance.js @@ -0,0 +1,341 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { opNow } = window.__bootstrap.timers; + const { cloneValue, illegalConstructorKey } = window.__bootstrap.webUtil; + + const customInspect = Symbol.for("Deno.customInspect"); + let performanceEntries = []; + + function findMostRecent( + name, + type, + ) { + return performanceEntries + .slice() + .reverse() + .find((entry) => entry.name === name && entry.entryType === type); + } + + function convertMarkToTimestamp(mark) { + if (typeof mark === "string") { + const entry = findMostRecent(mark, "mark"); + if (!entry) { + throw new SyntaxError(`Cannot find mark: "${mark}".`); + } + return entry.startTime; + } + if (mark < 0) { + throw new TypeError("Mark cannot be negative."); + } + return mark; + } + + function filterByNameType( + name, + type, + ) { + return performanceEntries.filter( + (entry) => + (name ? entry.name === name : true) && + (type ? entry.entryType === type : true), + ); + } + + function now() { + return opNow(); + } + + class PerformanceEntry { + #name = ""; + #entryType = ""; + #startTime = 0; + #duration = 0; + + get name() { + return this.#name; + } + + get entryType() { + return this.#entryType; + } + + get startTime() { + return this.#startTime; + } + + get duration() { + return this.#duration; + } + + constructor( + name = null, + entryType = null, + startTime = null, + duration = null, + key = null, + ) { + if (key != illegalConstructorKey) { + throw new TypeError("Illegal constructor."); + } + this.#name = name; + this.#entryType = entryType; + this.#startTime = startTime; + this.#duration = duration; + } + + toJSON() { + return { + name: this.#name, + entryType: this.#entryType, + startTime: this.#startTime, + duration: this.#duration, + }; + } + + [customInspect]() { + return `${this.constructor.name} { name: "${this.name}", entryType: "${this.entryType}", startTime: ${this.startTime}, duration: ${this.duration} }`; + } + } + + class PerformanceMark extends PerformanceEntry { + #detail = null; + + get detail() { + return this.#detail; + } + + get entryType() { + return "mark"; + } + + constructor( + name, + { detail = null, startTime = now() } = {}, + ) { + super(name, "mark", startTime, 0, illegalConstructorKey); + if (startTime < 0) { + throw new TypeError("startTime cannot be negative"); + } + this.#detail = cloneValue(detail); + } + + toJSON() { + return { + name: this.name, + entryType: this.entryType, + startTime: this.startTime, + duration: this.duration, + detail: this.detail, + }; + } + + [customInspect]() { + return this.detail + ? `${this.constructor.name} {\n detail: ${ + JSON.stringify(this.detail, null, 2) + },\n name: "${this.name}",\n entryType: "${this.entryType}",\n startTime: ${this.startTime},\n duration: ${this.duration}\n}` + : `${this.constructor.name} { detail: ${this.detail}, name: "${this.name}", entryType: "${this.entryType}", startTime: ${this.startTime}, duration: ${this.duration} }`; + } + } + + class PerformanceMeasure extends PerformanceEntry { + #detail = null; + + get detail() { + return this.#detail; + } + + get entryType() { + return "measure"; + } + + constructor( + name, + startTime, + duration, + detail = null, + key, + ) { + if (key != illegalConstructorKey) { + throw new TypeError("Illegal constructor."); + } + super(name, "measure", startTime, duration, illegalConstructorKey); + this.#detail = cloneValue(detail); + } + + toJSON() { + return { + name: this.name, + entryType: this.entryType, + startTime: this.startTime, + duration: this.duration, + detail: this.detail, + }; + } + + [customInspect]() { + return this.detail + ? `${this.constructor.name} {\n detail: ${ + JSON.stringify(this.detail, null, 2) + },\n name: "${this.name}",\n entryType: "${this.entryType}",\n startTime: ${this.startTime},\n duration: ${this.duration}\n}` + : `${this.constructor.name} { detail: ${this.detail}, name: "${this.name}", entryType: "${this.entryType}", startTime: ${this.startTime}, duration: ${this.duration} }`; + } + } + + class Performance { + constructor(key = null) { + if (key != illegalConstructorKey) { + throw new TypeError("Illegal constructor."); + } + } + + clearMarks(markName) { + if (markName == null) { + performanceEntries = performanceEntries.filter( + (entry) => entry.entryType !== "mark", + ); + } else { + performanceEntries = performanceEntries.filter( + (entry) => !(entry.name === markName && entry.entryType === "mark"), + ); + } + } + + clearMeasures(measureName) { + if (measureName == null) { + performanceEntries = performanceEntries.filter( + (entry) => entry.entryType !== "measure", + ); + } else { + performanceEntries = performanceEntries.filter( + (entry) => + !(entry.name === measureName && entry.entryType === "measure"), + ); + } + } + + getEntries() { + return filterByNameType(); + } + + getEntriesByName( + name, + type, + ) { + return filterByNameType(name, type); + } + + getEntriesByType(type) { + return filterByNameType(undefined, type); + } + + mark( + markName, + options = {}, + ) { + // 3.1.1.1 If the global object is a Window object and markName uses the + // same name as a read only attribute in the PerformanceTiming interface, + // throw a SyntaxError. - not implemented + const entry = new PerformanceMark(markName, options); + // 3.1.1.7 Queue entry - not implemented + performanceEntries.push(entry); + return entry; + } + + measure( + measureName, + startOrMeasureOptions = {}, + endMark, + ) { + if ( + startOrMeasureOptions && typeof startOrMeasureOptions === "object" && + Object.keys(startOrMeasureOptions).length > 0 + ) { + if (endMark) { + throw new TypeError("Options cannot be passed with endMark."); + } + if ( + !("start" in startOrMeasureOptions) && + !("end" in startOrMeasureOptions) + ) { + throw new TypeError( + "A start or end mark must be supplied in options.", + ); + } + if ( + "start" in startOrMeasureOptions && + "duration" in startOrMeasureOptions && + "end" in startOrMeasureOptions + ) { + throw new TypeError( + "Cannot specify start, end, and duration together in options.", + ); + } + } + let endTime; + if (endMark) { + endTime = convertMarkToTimestamp(endMark); + } else if ( + typeof startOrMeasureOptions === "object" && + "end" in startOrMeasureOptions + ) { + endTime = convertMarkToTimestamp(startOrMeasureOptions.end); + } else if ( + typeof startOrMeasureOptions === "object" && + "start" in startOrMeasureOptions && + "duration" in startOrMeasureOptions + ) { + const start = convertMarkToTimestamp(startOrMeasureOptions.start); + const duration = convertMarkToTimestamp(startOrMeasureOptions.duration); + endTime = start + duration; + } else { + endTime = now(); + } + let startTime; + if ( + typeof startOrMeasureOptions === "object" && + "start" in startOrMeasureOptions + ) { + startTime = convertMarkToTimestamp(startOrMeasureOptions.start); + } else if ( + typeof startOrMeasureOptions === "object" && + "end" in startOrMeasureOptions && + "duration" in startOrMeasureOptions + ) { + const end = convertMarkToTimestamp(startOrMeasureOptions.end); + const duration = convertMarkToTimestamp(startOrMeasureOptions.duration); + startTime = end - duration; + } else if (typeof startOrMeasureOptions === "string") { + startTime = convertMarkToTimestamp(startOrMeasureOptions); + } else { + startTime = 0; + } + const entry = new PerformanceMeasure( + measureName, + startTime, + endTime - startTime, + typeof startOrMeasureOptions === "object" + ? startOrMeasureOptions.detail ?? null + : null, + illegalConstructorKey, + ); + performanceEntries.push(entry); + return entry; + } + + now() { + return now(); + } + } + + const performance = new Performance(illegalConstructorKey); + + window.__bootstrap.performance = { + PerformanceEntry, + PerformanceMark, + PerformanceMeasure, + Performance, + performance, + }; +})(this); diff --git a/runtime/rt/40_permissions.js b/runtime/rt/40_permissions.js new file mode 100644 index 000000000..50d471b6a --- /dev/null +++ b/runtime/rt/40_permissions.js @@ -0,0 +1,65 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + const { illegalConstructorKey } = window.__bootstrap.webUtil; + + function opQuery(desc) { + return core.jsonOpSync("op_query_permission", desc).state; + } + + function opRevoke(desc) { + return core.jsonOpSync("op_revoke_permission", desc).state; + } + + function opRequest(desc) { + return core.jsonOpSync("op_request_permission", desc).state; + } + + class PermissionStatus { + constructor(state = null, key = null) { + if (key != illegalConstructorKey) { + throw new TypeError("Illegal constructor."); + } + this.state = state; + } + // TODO(kt3k): implement onchange handler + } + + class Permissions { + constructor(key) { + if (key != illegalConstructorKey) { + throw new TypeError("Illegal constructor."); + } + } + + query(desc) { + const state = opQuery(desc); + return Promise.resolve( + new PermissionStatus(state, illegalConstructorKey), + ); + } + + revoke(desc) { + const state = opRevoke(desc); + return Promise.resolve( + new PermissionStatus(state, illegalConstructorKey), + ); + } + + request(desc) { + const state = opRequest(desc); + return Promise.resolve( + new PermissionStatus(state, illegalConstructorKey), + ); + } + } + + const permissions = new Permissions(illegalConstructorKey); + + window.__bootstrap.permissions = { + permissions, + Permissions, + PermissionStatus, + }; +})(this); diff --git a/runtime/rt/40_plugins.js b/runtime/rt/40_plugins.js new file mode 100644 index 000000000..f5aefd400 --- /dev/null +++ b/runtime/rt/40_plugins.js @@ -0,0 +1,13 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + + function openPlugin(filename) { + return core.jsonOpSync("op_open_plugin", { filename }); + } + + window.__bootstrap.plugins = { + openPlugin, + }; +})(this); diff --git a/runtime/rt/40_process.js b/runtime/rt/40_process.js new file mode 100644 index 000000000..b46a1aead --- /dev/null +++ b/runtime/rt/40_process.js @@ -0,0 +1,122 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + const { File } = window.__bootstrap.files; + const { readAll } = window.__bootstrap.buffer; + const { assert, pathFromURL } = window.__bootstrap.util; + + function opKill(pid, signo) { + core.jsonOpSync("op_kill", { pid, signo }); + } + + function opRunStatus(rid) { + return core.jsonOpAsync("op_run_status", { rid }); + } + + function opRun(request) { + assert(request.cmd.length > 0); + return core.jsonOpSync("op_run", request); + } + + async function runStatus(rid) { + const res = await opRunStatus(rid); + + if (res.gotSignal) { + const signal = res.exitSignal; + return { success: false, code: 128 + signal, signal }; + } else if (res.exitCode != 0) { + return { success: false, code: res.exitCode }; + } else { + return { success: true, code: 0 }; + } + } + + class Process { + constructor(res) { + this.rid = res.rid; + this.pid = res.pid; + + if (res.stdinRid && res.stdinRid > 0) { + this.stdin = new File(res.stdinRid); + } + + if (res.stdoutRid && res.stdoutRid > 0) { + this.stdout = new File(res.stdoutRid); + } + + if (res.stderrRid && res.stderrRid > 0) { + this.stderr = new File(res.stderrRid); + } + } + + status() { + return runStatus(this.rid); + } + + async output() { + if (!this.stdout) { + throw new TypeError("stdout was not piped"); + } + try { + return await readAll(this.stdout); + } finally { + this.stdout.close(); + } + } + + async stderrOutput() { + if (!this.stderr) { + throw new TypeError("stderr was not piped"); + } + try { + return await readAll(this.stderr); + } finally { + this.stderr.close(); + } + } + + close() { + core.close(this.rid); + } + + kill(signo) { + opKill(this.pid, signo); + } + } + + function isRid(arg) { + return !isNaN(arg); + } + + function run({ + cmd, + cwd = undefined, + env = {}, + stdout = "inherit", + stderr = "inherit", + stdin = "inherit", + }) { + if (cmd[0] != null) { + cmd[0] = pathFromURL(cmd[0]); + } + const res = opRun({ + cmd: cmd.map(String), + cwd, + env: Object.entries(env), + stdin: isRid(stdin) ? "" : stdin, + stdout: isRid(stdout) ? "" : stdout, + stderr: isRid(stderr) ? "" : stderr, + stdinRid: isRid(stdin) ? stdin : 0, + stdoutRid: isRid(stdout) ? stdout : 0, + stderrRid: isRid(stderr) ? stderr : 0, + }); + return new Process(res); + } + + window.__bootstrap.process = { + run, + Process, + kill: opKill, + }; +})(this); diff --git a/runtime/rt/40_read_file.js b/runtime/rt/40_read_file.js new file mode 100644 index 000000000..9a36f335b --- /dev/null +++ b/runtime/rt/40_read_file.js @@ -0,0 +1,43 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { open, openSync } = window.__bootstrap.files; + const { readAll, readAllSync } = window.__bootstrap.buffer; + + function readFileSync(path) { + const file = openSync(path); + const contents = readAllSync(file); + file.close(); + return contents; + } + + async function readFile(path) { + const file = await open(path); + const contents = await readAll(file); + file.close(); + return contents; + } + + function readTextFileSync(path) { + const file = openSync(path); + const contents = readAllSync(file); + file.close(); + const decoder = new TextDecoder(); + return decoder.decode(contents); + } + + async function readTextFile(path) { + const file = await open(path); + const contents = await readAll(file); + file.close(); + const decoder = new TextDecoder(); + return decoder.decode(contents); + } + + window.__bootstrap.readFile = { + readFile, + readFileSync, + readTextFileSync, + readTextFile, + }; +})(this); diff --git a/runtime/rt/40_signals.js b/runtime/rt/40_signals.js new file mode 100644 index 000000000..739c963fd --- /dev/null +++ b/runtime/rt/40_signals.js @@ -0,0 +1,256 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + const { build } = window.__bootstrap.build; + + function bindSignal(signo) { + return core.jsonOpSync("op_signal_bind", { signo }); + } + + function pollSignal(rid) { + return core.jsonOpAsync("op_signal_poll", { rid }); + } + + function unbindSignal(rid) { + core.jsonOpSync("op_signal_unbind", { rid }); + } + + // From `kill -l` + const LinuxSignal = { + 1: "SIGHUP", + 2: "SIGINT", + 3: "SIGQUIT", + 4: "SIGILL", + 5: "SIGTRAP", + 6: "SIGABRT", + 7: "SIGBUS", + 8: "SIGFPE", + 9: "SIGKILL", + 10: "SIGUSR1", + 11: "SIGSEGV", + 12: "SIGUSR2", + 13: "SIGPIPE", + 14: "SIGALRM", + 15: "SIGTERM", + 16: "SIGSTKFLT", + 17: "SIGCHLD", + 18: "SIGCONT", + 19: "SIGSTOP", + 20: "SIGTSTP", + 21: "SIGTTIN", + 22: "SIGTTOU", + 23: "SIGURG", + 24: "SIGXCPU", + 25: "SIGXFSZ", + 26: "SIGVTALRM", + 27: "SIGPROF", + 28: "SIGWINCH", + 29: "SIGIO", + 30: "SIGPWR", + 31: "SIGSYS", + SIGHUP: 1, + SIGINT: 2, + SIGQUIT: 3, + SIGILL: 4, + SIGTRAP: 5, + SIGABRT: 6, + SIGBUS: 7, + SIGFPE: 8, + SIGKILL: 9, + SIGUSR1: 10, + SIGSEGV: 11, + SIGUSR2: 12, + SIGPIPE: 13, + SIGALRM: 14, + SIGTERM: 15, + SIGSTKFLT: 16, + SIGCHLD: 17, + SIGCONT: 18, + SIGSTOP: 19, + SIGTSTP: 20, + SIGTTIN: 21, + SIGTTOU: 22, + SIGURG: 23, + SIGXCPU: 24, + SIGXFSZ: 25, + SIGVTALRM: 26, + SIGPROF: 27, + SIGWINCH: 28, + SIGIO: 29, + SIGPWR: 30, + SIGSYS: 31, + }; + + // From `kill -l` + const MacOSSignal = { + 1: "SIGHUP", + 2: "SIGINT", + 3: "SIGQUIT", + 4: "SIGILL", + 5: "SIGTRAP", + 6: "SIGABRT", + 7: "SIGEMT", + 8: "SIGFPE", + 9: "SIGKILL", + 10: "SIGBUS", + 11: "SIGSEGV", + 12: "SIGSYS", + 13: "SIGPIPE", + 14: "SIGALRM", + 15: "SIGTERM", + 16: "SIGURG", + 17: "SIGSTOP", + 18: "SIGTSTP", + 19: "SIGCONT", + 20: "SIGCHLD", + 21: "SIGTTIN", + 22: "SIGTTOU", + 23: "SIGIO", + 24: "SIGXCPU", + 25: "SIGXFSZ", + 26: "SIGVTALRM", + 27: "SIGPROF", + 28: "SIGWINCH", + 29: "SIGINFO", + 30: "SIGUSR1", + 31: "SIGUSR2", + SIGHUP: 1, + SIGINT: 2, + SIGQUIT: 3, + SIGILL: 4, + SIGTRAP: 5, + SIGABRT: 6, + SIGEMT: 7, + SIGFPE: 8, + SIGKILL: 9, + SIGBUS: 10, + SIGSEGV: 11, + SIGSYS: 12, + SIGPIPE: 13, + SIGALRM: 14, + SIGTERM: 15, + SIGURG: 16, + SIGSTOP: 17, + SIGTSTP: 18, + SIGCONT: 19, + SIGCHLD: 20, + SIGTTIN: 21, + SIGTTOU: 22, + SIGIO: 23, + SIGXCPU: 24, + SIGXFSZ: 25, + SIGVTALRM: 26, + SIGPROF: 27, + SIGWINCH: 28, + SIGINFO: 29, + SIGUSR1: 30, + SIGUSR2: 31, + }; + + const Signal = {}; + + function setSignals() { + if (build.os === "darwin") { + Object.assign(Signal, MacOSSignal); + } else { + Object.assign(Signal, LinuxSignal); + } + } + + function signal(signo) { + if (build.os === "windows") { + throw new Error("not implemented!"); + } + return new SignalStream(signo); + } + + const signals = { + alarm() { + return signal(Signal.SIGALRM); + }, + child() { + return signal(Signal.SIGCHLD); + }, + hungup() { + return signal(Signal.SIGHUP); + }, + interrupt() { + return signal(Signal.SIGINT); + }, + io() { + return signal(Signal.SIGIO); + }, + pipe() { + return signal(Signal.SIGPIPE); + }, + quit() { + return signal(Signal.SIGQUIT); + }, + terminate() { + return signal(Signal.SIGTERM); + }, + userDefined1() { + return signal(Signal.SIGUSR1); + }, + userDefined2() { + return signal(Signal.SIGUSR2); + }, + windowChange() { + return signal(Signal.SIGWINCH); + }, + }; + + class SignalStream { + #disposed = false; + #pollingPromise = Promise.resolve(false); + #rid = 0; + + constructor(signo) { + this.#rid = bindSignal(signo).rid; + this.#loop(); + } + + #pollSignal = async () => { + const res = await pollSignal(this.#rid); + return res.done; + }; + + #loop = async () => { + do { + this.#pollingPromise = this.#pollSignal(); + } while (!(await this.#pollingPromise) && !this.#disposed); + }; + + then( + f, + g, + ) { + return this.#pollingPromise.then(() => {}).then(f, g); + } + + async next() { + return { done: await this.#pollingPromise, value: undefined }; + } + + [Symbol.asyncIterator]() { + return this; + } + + dispose() { + if (this.#disposed) { + throw new Error("The stream has already been disposed."); + } + this.#disposed = true; + unbindSignal(this.#rid); + } + } + + window.__bootstrap.signals = { + signal, + signals, + Signal, + SignalStream, + setSignals, + }; +})(this); diff --git a/runtime/rt/40_testing.js b/runtime/rt/40_testing.js new file mode 100644 index 000000000..082d17fe0 --- /dev/null +++ b/runtime/rt/40_testing.js @@ -0,0 +1,350 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + const colors = window.__bootstrap.colors; + const { exit } = window.__bootstrap.os; + const { Console, inspectArgs } = window.__bootstrap.console; + const { stdout } = window.__bootstrap.files; + const { exposeForTest } = window.__bootstrap.internals; + const { metrics } = window.__bootstrap.metrics; + const { assert } = window.__bootstrap.util; + + const disabledConsole = new Console(() => {}); + + function delay(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + } + + function formatDuration(time = 0) { + const gray = colors.maybeColor(colors.gray); + const italic = colors.maybeColor(colors.italic); + const timeStr = `(${time}ms)`; + return gray(italic(timeStr)); + } + + // Wrap test function in additional assertion that makes sure + // the test case does not leak async "ops" - ie. number of async + // completed ops after the test is the same as number of dispatched + // ops. Note that "unref" ops are ignored since in nature that are + // optional. + function assertOps(fn) { + return async function asyncOpSanitizer() { + const pre = metrics(); + await fn(); + // Defer until next event loop turn - that way timeouts and intervals + // cleared can actually be removed from resource table, otherwise + // false positives may occur (https://github.com/denoland/deno/issues/4591) + await delay(0); + const post = metrics(); + // We're checking diff because one might spawn HTTP server in the background + // that will be a pending async op before test starts. + const dispatchedDiff = post.opsDispatchedAsync - pre.opsDispatchedAsync; + const completedDiff = post.opsCompletedAsync - pre.opsCompletedAsync; + assert( + dispatchedDiff === completedDiff, + `Test case is leaking async ops. +Before: + - dispatched: ${pre.opsDispatchedAsync} + - completed: ${pre.opsCompletedAsync} +After: + - dispatched: ${post.opsDispatchedAsync} + - completed: ${post.opsCompletedAsync} + +Make sure to await all promises returned from Deno APIs before +finishing test case.`, + ); + }; + } + + // Wrap test function in additional assertion that makes sure + // the test case does not "leak" resources - ie. resource table after + // the test has exactly the same contents as before the test. + function assertResources( + fn, + ) { + return async function resourceSanitizer() { + const pre = core.resources(); + await fn(); + const post = core.resources(); + + const preStr = JSON.stringify(pre, null, 2); + const postStr = JSON.stringify(post, null, 2); + const msg = `Test case is leaking resources. +Before: ${preStr} +After: ${postStr} + +Make sure to close all open resource handles returned from Deno APIs before +finishing test case.`; + assert(preStr === postStr, msg); + }; + } + + const TEST_REGISTRY = []; + + // Main test function provided by Deno, as you can see it merely + // creates a new object with "name" and "fn" fields. + function test( + t, + fn, + ) { + let testDef; + const defaults = { + ignore: false, + only: false, + sanitizeOps: true, + sanitizeResources: true, + }; + + if (typeof t === "string") { + if (!fn || typeof fn != "function") { + throw new TypeError("Missing test function"); + } + if (!t) { + throw new TypeError("The test name can't be empty"); + } + testDef = { fn: fn, name: t, ...defaults }; + } else { + if (!t.fn) { + throw new TypeError("Missing test function"); + } + if (!t.name) { + throw new TypeError("The test name can't be empty"); + } + testDef = { ...defaults, ...t }; + } + + if (testDef.sanitizeOps) { + testDef.fn = assertOps(testDef.fn); + } + + if (testDef.sanitizeResources) { + testDef.fn = assertResources(testDef.fn); + } + + TEST_REGISTRY.push(testDef); + } + + const encoder = new TextEncoder(); + + function log(msg, noNewLine = false) { + if (!noNewLine) { + msg += "\n"; + } + + // Using `stdout` here because it doesn't force new lines + // compared to `console.log`; `core.print` on the other hand + // is line-buffered and doesn't output message without newline + stdout.writeSync(encoder.encode(msg)); + } + + function reportToConsole(message) { + const green = colors.maybeColor(colors.green); + const red = colors.maybeColor(colors.red); + const yellow = colors.maybeColor(colors.yellow); + const redFailed = red("FAILED"); + const greenOk = green("ok"); + const yellowIgnored = yellow("ignored"); + if (message.start != null) { + log(`running ${message.start.tests.length} tests`); + } else if (message.testStart != null) { + const { name } = message.testStart; + + log(`test ${name} ... `, true); + return; + } else if (message.testEnd != null) { + switch (message.testEnd.status) { + case "passed": + log(`${greenOk} ${formatDuration(message.testEnd.duration)}`); + break; + case "failed": + log(`${redFailed} ${formatDuration(message.testEnd.duration)}`); + break; + case "ignored": + log(`${yellowIgnored} ${formatDuration(message.testEnd.duration)}`); + break; + } + } else if (message.end != null) { + const failures = message.end.results.filter((m) => m.error != null); + if (failures.length > 0) { + log(`\nfailures:\n`); + + for (const { name, error } of failures) { + log(name); + log(inspectArgs([error])); + log(""); + } + + log(`failures:\n`); + + for (const { name } of failures) { + log(`\t${name}`); + } + } + log( + `\ntest result: ${message.end.failed ? redFailed : greenOk}. ` + + `${message.end.passed} passed; ${message.end.failed} failed; ` + + `${message.end.ignored} ignored; ${message.end.measured} measured; ` + + `${message.end.filtered} filtered out ` + + `${formatDuration(message.end.duration)}\n`, + ); + + if (message.end.usedOnly && message.end.failed == 0) { + log(`${redFailed} because the "only" option was used\n`); + } + } + } + + exposeForTest("reportToConsole", reportToConsole); + + // TODO: already implements AsyncGenerator, but add as "implements to class" + // TODO: implements PromiseLike + class TestRunner { + #usedOnly = false; + + constructor( + tests, + filterFn, + failFast, + ) { + this.stats = { + filtered: 0, + ignored: 0, + measured: 0, + passed: 0, + failed: 0, + }; + this.filterFn = filterFn; + this.failFast = failFast; + const onlyTests = tests.filter(({ only }) => only); + this.#usedOnly = onlyTests.length > 0; + const unfilteredTests = this.#usedOnly ? onlyTests : tests; + this.testsToRun = unfilteredTests.filter(filterFn); + this.stats.filtered = unfilteredTests.length - this.testsToRun.length; + } + + async *[Symbol.asyncIterator]() { + yield { start: { tests: this.testsToRun } }; + + const results = []; + const suiteStart = +new Date(); + for (const test of this.testsToRun) { + const endMessage = { + name: test.name, + duration: 0, + }; + yield { testStart: { ...test } }; + if (test.ignore) { + endMessage.status = "ignored"; + this.stats.ignored++; + } else { + const start = +new Date(); + try { + await test.fn(); + endMessage.status = "passed"; + this.stats.passed++; + } catch (err) { + endMessage.status = "failed"; + endMessage.error = err; + this.stats.failed++; + } + endMessage.duration = +new Date() - start; + } + results.push(endMessage); + yield { testEnd: endMessage }; + if (this.failFast && endMessage.error != null) { + break; + } + } + + const duration = +new Date() - suiteStart; + + yield { + end: { ...this.stats, usedOnly: this.#usedOnly, duration, results }, + }; + } + } + + function createFilterFn( + filter, + skip, + ) { + return (def) => { + let passes = true; + + if (filter) { + if (filter instanceof RegExp) { + passes = passes && filter.test(def.name); + } else if (filter.startsWith("/") && filter.endsWith("/")) { + const filterAsRegex = new RegExp(filter.slice(1, filter.length - 1)); + passes = passes && filterAsRegex.test(def.name); + } else { + passes = passes && def.name.includes(filter); + } + } + + if (skip) { + if (skip instanceof RegExp) { + passes = passes && !skip.test(def.name); + } else { + passes = passes && !def.name.includes(skip); + } + } + + return passes; + }; + } + + exposeForTest("createFilterFn", createFilterFn); + + async function runTests({ + exitOnFail = true, + failFast = false, + filter = undefined, + skip = undefined, + disableLog = false, + reportToConsole: reportToConsole_ = true, + onMessage = undefined, + } = {}) { + const filterFn = createFilterFn(filter, skip); + const testRunner = new TestRunner(TEST_REGISTRY, filterFn, failFast); + + const originalConsole = globalThis.console; + + if (disableLog) { + globalThis.console = disabledConsole; + } + + let endMsg; + + for await (const message of testRunner) { + if (onMessage != null) { + await onMessage(message); + } + if (reportToConsole_) { + reportToConsole(message); + } + if (message.end != null) { + endMsg = message.end; + } + } + + if (disableLog) { + globalThis.console = originalConsole; + } + + if ((endMsg.failed > 0 || endMsg?.usedOnly) && exitOnFail) { + exit(1); + } + + return endMsg; + } + + exposeForTest("runTests", runTests); + + window.__bootstrap.testing = { + test, + }; +})(this); diff --git a/runtime/rt/40_tls.js b/runtime/rt/40_tls.js new file mode 100644 index 000000000..d66e0bd01 --- /dev/null +++ b/runtime/rt/40_tls.js @@ -0,0 +1,82 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + const { Listener, Conn } = window.__bootstrap.net; + + function opConnectTls( + args, + ) { + return core.jsonOpAsync("op_connect_tls", args); + } + + function opAcceptTLS(rid) { + return core.jsonOpAsync("op_accept_tls", { rid }); + } + + function opListenTls(args) { + return core.jsonOpSync("op_listen_tls", args); + } + + function opStartTls(args) { + return core.jsonOpAsync("op_start_tls", args); + } + + async function connectTls({ + port, + hostname = "127.0.0.1", + transport = "tcp", + certFile = undefined, + }) { + const res = await opConnectTls({ + port, + hostname, + transport, + certFile, + }); + return new Conn(res.rid, res.remoteAddr, res.localAddr); + } + + class TLSListener extends Listener { + async accept() { + const res = await opAcceptTLS(this.rid); + return new Conn(res.rid, res.remoteAddr, res.localAddr); + } + } + + function listenTls({ + port, + certFile, + keyFile, + hostname = "0.0.0.0", + transport = "tcp", + }) { + const res = opListenTls({ + port, + certFile, + keyFile, + hostname, + transport, + }); + return new TLSListener(res.rid, res.localAddr); + } + + async function startTls( + conn, + { hostname = "127.0.0.1", certFile } = {}, + ) { + const res = await opStartTls({ + rid: conn.rid, + hostname, + certFile, + }); + return new Conn(res.rid, res.remoteAddr, res.localAddr); + } + + window.__bootstrap.tls = { + startTls, + listenTls, + connectTls, + TLSListener, + }; +})(this); diff --git a/runtime/rt/40_tty.js b/runtime/rt/40_tty.js new file mode 100644 index 000000000..598d33237 --- /dev/null +++ b/runtime/rt/40_tty.js @@ -0,0 +1,28 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + + function consoleSize(rid) { + return core.jsonOpSync("op_console_size", { rid }); + } + + function isatty(rid) { + return core.jsonOpSync("op_isatty", { rid }); + } + + const DEFAULT_SET_RAW_OPTIONS = { + cbreak: false, + }; + + function setRaw(rid, mode, options = {}) { + const rOptions = { ...DEFAULT_SET_RAW_OPTIONS, ...options }; + core.jsonOpSync("op_set_raw", { rid, mode, options: rOptions }); + } + + window.__bootstrap.tty = { + consoleSize, + isatty, + setRaw, + }; +})(this); diff --git a/runtime/rt/40_write_file.js b/runtime/rt/40_write_file.js new file mode 100644 index 000000000..7a9cb1f40 --- /dev/null +++ b/runtime/rt/40_write_file.js @@ -0,0 +1,92 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +((window) => { + const { stat, statSync, chmod, chmodSync } = window.__bootstrap.fs; + const { open, openSync } = window.__bootstrap.files; + const { writeAll, writeAllSync } = window.__bootstrap.buffer; + const { build } = window.__bootstrap.build; + + function writeFileSync( + path, + data, + options = {}, + ) { + if (options.create !== undefined) { + const create = !!options.create; + if (!create) { + // verify that file exists + statSync(path); + } + } + + const openOptions = options.append + ? { write: true, create: true, append: true } + : { write: true, create: true, truncate: true }; + const file = openSync(path, openOptions); + + if ( + options.mode !== undefined && + options.mode !== null && + build.os !== "windows" + ) { + chmodSync(path, options.mode); + } + + writeAllSync(file, data); + file.close(); + } + + async function writeFile( + path, + data, + options = {}, + ) { + if (options.create !== undefined) { + const create = !!options.create; + if (!create) { + // verify that file exists + await stat(path); + } + } + + const openOptions = options.append + ? { write: true, create: true, append: true } + : { write: true, create: true, truncate: true }; + const file = await open(path, openOptions); + + if ( + options.mode !== undefined && + options.mode !== null && + build.os !== "windows" + ) { + await chmod(path, options.mode); + } + + await writeAll(file, data); + file.close(); + } + + function writeTextFileSync( + path, + data, + options = {}, + ) { + const encoder = new TextEncoder(); + return writeFileSync(path, encoder.encode(data), options); + } + + function writeTextFile( + path, + data, + options = {}, + ) { + const encoder = new TextEncoder(); + return writeFile(path, encoder.encode(data), options); + } + + window.__bootstrap.writeFile = { + writeTextFile, + writeTextFileSync, + writeFile, + writeFileSync, + }; +})(this); diff --git a/runtime/rt/41_prompt.js b/runtime/rt/41_prompt.js new file mode 100644 index 000000000..ec294668b --- /dev/null +++ b/runtime/rt/41_prompt.js @@ -0,0 +1,80 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +((window) => { + const { stdin } = window.__bootstrap.files; + const { isatty } = window.__bootstrap.tty; + const LF = "\n".charCodeAt(0); + const CR = "\r".charCodeAt(0); + const decoder = new TextDecoder(); + const core = window.Deno.core; + + function alert(message = "Alert") { + if (!isatty(stdin.rid)) { + return; + } + + core.print(`${message} [Enter] `, false); + + readLineFromStdinSync(); + } + + function confirm(message = "Confirm") { + if (!isatty(stdin.rid)) { + return false; + } + + core.print(`${message} [y/N] `, false); + + const answer = readLineFromStdinSync(); + + return answer === "Y" || answer === "y"; + } + + function prompt(message = "Prompt", defaultValue) { + defaultValue ??= null; + + if (!isatty(stdin.rid)) { + return null; + } + + core.print(`${message} `, false); + + if (defaultValue) { + core.print(`[${defaultValue}] `, false); + } + + return readLineFromStdinSync() || defaultValue; + } + + function readLineFromStdinSync() { + const c = new Uint8Array(1); + const buf = []; + + while (true) { + const n = stdin.readSync(c); + if (n === null || n === 0) { + break; + } + if (c[0] === CR) { + const n = stdin.readSync(c); + if (c[0] === LF) { + break; + } + buf.push(CR); + if (n === null || n === 0) { + break; + } + } + if (c[0] === LF) { + break; + } + buf.push(c[0]); + } + return decoder.decode(new Uint8Array(buf)); + } + + window.__bootstrap.prompt = { + alert, + confirm, + prompt, + }; +})(this); diff --git a/runtime/rt/90_deno_ns.js b/runtime/rt/90_deno_ns.js new file mode 100644 index 000000000..9188788ec --- /dev/null +++ b/runtime/rt/90_deno_ns.js @@ -0,0 +1,137 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const __bootstrap = window.__bootstrap; + __bootstrap.denoNs = { + test: __bootstrap.testing.test, + metrics: __bootstrap.metrics.metrics, + Process: __bootstrap.process.Process, + run: __bootstrap.process.run, + isatty: __bootstrap.tty.isatty, + writeFileSync: __bootstrap.writeFile.writeFileSync, + writeFile: __bootstrap.writeFile.writeFile, + writeTextFileSync: __bootstrap.writeFile.writeTextFileSync, + writeTextFile: __bootstrap.writeFile.writeTextFile, + readTextFile: __bootstrap.readFile.readTextFile, + readTextFileSync: __bootstrap.readFile.readTextFileSync, + readFile: __bootstrap.readFile.readFile, + readFileSync: __bootstrap.readFile.readFileSync, + watchFs: __bootstrap.fsEvents.watchFs, + chmodSync: __bootstrap.fs.chmodSync, + chmod: __bootstrap.fs.chmod, + chown: __bootstrap.fs.chown, + chownSync: __bootstrap.fs.chownSync, + copyFileSync: __bootstrap.fs.copyFileSync, + cwd: __bootstrap.fs.cwd, + makeTempDirSync: __bootstrap.fs.makeTempDirSync, + makeTempDir: __bootstrap.fs.makeTempDir, + makeTempFileSync: __bootstrap.fs.makeTempFileSync, + makeTempFile: __bootstrap.fs.makeTempFile, + mkdirSync: __bootstrap.fs.mkdirSync, + mkdir: __bootstrap.fs.mkdir, + chdir: __bootstrap.fs.chdir, + copyFile: __bootstrap.fs.copyFile, + readDirSync: __bootstrap.fs.readDirSync, + readDir: __bootstrap.fs.readDir, + readLinkSync: __bootstrap.fs.readLinkSync, + readLink: __bootstrap.fs.readLink, + realPathSync: __bootstrap.fs.realPathSync, + realPath: __bootstrap.fs.realPath, + removeSync: __bootstrap.fs.removeSync, + remove: __bootstrap.fs.remove, + renameSync: __bootstrap.fs.renameSync, + rename: __bootstrap.fs.rename, + version: __bootstrap.version.version, + build: __bootstrap.build.build, + statSync: __bootstrap.fs.statSync, + lstatSync: __bootstrap.fs.lstatSync, + stat: __bootstrap.fs.stat, + lstat: __bootstrap.fs.lstat, + truncateSync: __bootstrap.fs.truncateSync, + truncate: __bootstrap.fs.truncate, + errors: __bootstrap.errors.errors, + customInspect: __bootstrap.console.customInspect, + inspect: __bootstrap.console.inspect, + env: __bootstrap.os.env, + exit: __bootstrap.os.exit, + execPath: __bootstrap.os.execPath, + Buffer: __bootstrap.buffer.Buffer, + readAll: __bootstrap.buffer.readAll, + readAllSync: __bootstrap.buffer.readAllSync, + writeAll: __bootstrap.buffer.writeAll, + writeAllSync: __bootstrap.buffer.writeAllSync, + copy: __bootstrap.io.copy, + iter: __bootstrap.io.iter, + iterSync: __bootstrap.io.iterSync, + SeekMode: __bootstrap.io.SeekMode, + read: __bootstrap.io.read, + readSync: __bootstrap.io.readSync, + write: __bootstrap.io.write, + writeSync: __bootstrap.io.writeSync, + File: __bootstrap.files.File, + open: __bootstrap.files.open, + openSync: __bootstrap.files.openSync, + create: __bootstrap.files.create, + createSync: __bootstrap.files.createSync, + stdin: __bootstrap.files.stdin, + stdout: __bootstrap.files.stdout, + stderr: __bootstrap.files.stderr, + seek: __bootstrap.files.seek, + seekSync: __bootstrap.files.seekSync, + connect: __bootstrap.net.connect, + listen: __bootstrap.net.listen, + connectTls: __bootstrap.tls.connectTls, + listenTls: __bootstrap.tls.listenTls, + sleepSync: __bootstrap.timers.sleepSync, + fsyncSync: __bootstrap.fs.fsyncSync, + fsync: __bootstrap.fs.fsync, + fdatasyncSync: __bootstrap.fs.fdatasyncSync, + fdatasync: __bootstrap.fs.fdatasync, + }; + + __bootstrap.denoNsUnstable = { + signal: __bootstrap.signals.signal, + signals: __bootstrap.signals.signals, + Signal: __bootstrap.signals.Signal, + SignalStream: __bootstrap.signals.SignalStream, + transpileOnly: __bootstrap.compilerApi.transpileOnly, + compile: __bootstrap.compilerApi.compile, + bundle: __bootstrap.compilerApi.bundle, + permissions: __bootstrap.permissions.permissions, + Permissions: __bootstrap.permissions.Permissions, + PermissionStatus: __bootstrap.permissions.PermissionStatus, + openPlugin: __bootstrap.plugins.openPlugin, + kill: __bootstrap.process.kill, + setRaw: __bootstrap.tty.setRaw, + consoleSize: __bootstrap.tty.consoleSize, + DiagnosticCategory: __bootstrap.diagnostics.DiagnosticCategory, + loadavg: __bootstrap.os.loadavg, + hostname: __bootstrap.os.hostname, + osRelease: __bootstrap.os.osRelease, + systemMemoryInfo: __bootstrap.os.systemMemoryInfo, + systemCpuInfo: __bootstrap.os.systemCpuInfo, + applySourceMap: __bootstrap.errorStack.opApplySourceMap, + formatDiagnostics: __bootstrap.errorStack.opFormatDiagnostics, + shutdown: __bootstrap.net.shutdown, + ShutdownMode: __bootstrap.net.ShutdownMode, + listen: __bootstrap.netUnstable.listen, + connect: __bootstrap.netUnstable.connect, + listenDatagram: __bootstrap.netUnstable.listenDatagram, + startTls: __bootstrap.tls.startTls, + fstatSync: __bootstrap.fs.fstatSync, + fstat: __bootstrap.fs.fstat, + ftruncateSync: __bootstrap.fs.ftruncateSync, + ftruncate: __bootstrap.fs.ftruncate, + umask: __bootstrap.fs.umask, + link: __bootstrap.fs.link, + linkSync: __bootstrap.fs.linkSync, + futime: __bootstrap.fs.futime, + futimeSync: __bootstrap.fs.futimeSync, + utime: __bootstrap.fs.utime, + utimeSync: __bootstrap.fs.utimeSync, + symlink: __bootstrap.fs.symlink, + symlinkSync: __bootstrap.fs.symlinkSync, + HttpClient: __bootstrap.fetch.HttpClient, + createHttpClient: __bootstrap.fetch.createHttpClient, + }; +})(this); diff --git a/runtime/rt/99_main.js b/runtime/rt/99_main.js new file mode 100644 index 000000000..f38d51936 --- /dev/null +++ b/runtime/rt/99_main.js @@ -0,0 +1,395 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +// Removes the `__proto__` for security reasons. This intentionally makes +// Deno non compliant with ECMA-262 Annex B.2.2.1 +// +delete Object.prototype.__proto__; + +((window) => { + const core = Deno.core; + const util = window.__bootstrap.util; + const eventTarget = window.__bootstrap.eventTarget; + const globalInterfaces = window.__bootstrap.globalInterfaces; + const dispatchMinimal = window.__bootstrap.dispatchMinimal; + const build = window.__bootstrap.build; + const version = window.__bootstrap.version; + const errorStack = window.__bootstrap.errorStack; + const os = window.__bootstrap.os; + const timers = window.__bootstrap.timers; + const Console = window.__bootstrap.console.Console; + const worker = window.__bootstrap.worker; + const signals = window.__bootstrap.signals; + const { internalSymbol, internalObject } = window.__bootstrap.internals; + const performance = window.__bootstrap.performance; + const crypto = window.__bootstrap.crypto; + const url = window.__bootstrap.url; + const headers = window.__bootstrap.headers; + const streams = window.__bootstrap.streams; + const fileReader = window.__bootstrap.fileReader; + const webSocket = window.__bootstrap.webSocket; + const fetch = window.__bootstrap.fetch; + const prompt = window.__bootstrap.prompt; + const denoNs = window.__bootstrap.denoNs; + const denoNsUnstable = window.__bootstrap.denoNsUnstable; + const errors = window.__bootstrap.errors.errors; + const { defineEventHandler } = window.__bootstrap.webUtil; + + let windowIsClosing = false; + + function windowClose() { + if (!windowIsClosing) { + windowIsClosing = true; + // Push a macrotask to exit after a promise resolve. + // This is not perfect, but should be fine for first pass. + Promise.resolve().then(() => + timers.setTimeout.call( + null, + () => { + // This should be fine, since only Window/MainWorker has .close() + os.exit(0); + }, + 0, + ) + ); + } + } + + const encoder = new TextEncoder(); + + function workerClose() { + if (isClosing) { + return; + } + + isClosing = true; + opCloseWorker(); + } + + // TODO(bartlomieju): remove these functions + // Stuff for workers + const onmessage = () => {}; + const onerror = () => {}; + + function postMessage(data) { + const dataJson = JSON.stringify(data); + const dataIntArray = encoder.encode(dataJson); + opPostMessage(dataIntArray); + } + + let isClosing = false; + async function workerMessageRecvCallback(data) { + const msgEvent = new MessageEvent("message", { + cancelable: false, + data, + }); + + try { + if (globalThis["onmessage"]) { + const result = globalThis.onmessage(msgEvent); + if (result && "then" in result) { + await result; + } + } + globalThis.dispatchEvent(msgEvent); + } catch (e) { + let handled = false; + + const errorEvent = new ErrorEvent("error", { + cancelable: true, + message: e.message, + lineno: e.lineNumber ? e.lineNumber + 1 : undefined, + colno: e.columnNumber ? e.columnNumber + 1 : undefined, + filename: e.fileName, + error: null, + }); + + if (globalThis["onerror"]) { + const ret = globalThis.onerror( + e.message, + e.fileName, + e.lineNumber, + e.columnNumber, + e, + ); + handled = ret === true; + } + + globalThis.dispatchEvent(errorEvent); + if (errorEvent.defaultPrevented) { + handled = true; + } + + if (!handled) { + throw e; + } + } + } + + function opPostMessage(data) { + core.jsonOpSync("op_worker_post_message", {}, data); + } + + function opCloseWorker() { + core.jsonOpSync("op_worker_close"); + } + + function opMainModule() { + return core.jsonOpSync("op_main_module"); + } + + function runtimeStart(runtimeOptions, source) { + const opsMap = core.ops(); + for (const [name, opId] of Object.entries(opsMap)) { + if (name === "op_write" || name === "op_read") { + core.setAsyncHandler(opId, dispatchMinimal.asyncMsgFromRust); + } + } + + core.setMacrotaskCallback(timers.handleTimerMacrotask); + version.setVersions( + runtimeOptions.denoVersion, + runtimeOptions.v8Version, + runtimeOptions.tsVersion, + ); + build.setBuildInfo(runtimeOptions.target); + util.setLogDebug(runtimeOptions.debugFlag, source); + // TODO(bartlomieju): a very crude way to disable + // source mapping of errors. This condition is true + // only for compiled standalone binaries. + let prepareStackTrace; + if (runtimeOptions.applySourceMaps) { + prepareStackTrace = core.createPrepareStackTrace( + errorStack.opApplySourceMap, + ); + } else { + prepareStackTrace = core.createPrepareStackTrace(); + } + Error.prepareStackTrace = prepareStackTrace; + } + + function registerErrors() { + core.registerErrorClass("NotFound", errors.NotFound); + core.registerErrorClass("PermissionDenied", errors.PermissionDenied); + core.registerErrorClass("ConnectionRefused", errors.ConnectionRefused); + core.registerErrorClass("ConnectionReset", errors.ConnectionReset); + core.registerErrorClass("ConnectionAborted", errors.ConnectionAborted); + core.registerErrorClass("NotConnected", errors.NotConnected); + core.registerErrorClass("AddrInUse", errors.AddrInUse); + core.registerErrorClass("AddrNotAvailable", errors.AddrNotAvailable); + core.registerErrorClass("BrokenPipe", errors.BrokenPipe); + core.registerErrorClass("AlreadyExists", errors.AlreadyExists); + core.registerErrorClass("InvalidData", errors.InvalidData); + core.registerErrorClass("TimedOut", errors.TimedOut); + core.registerErrorClass("Interrupted", errors.Interrupted); + core.registerErrorClass("WriteZero", errors.WriteZero); + core.registerErrorClass("UnexpectedEof", errors.UnexpectedEof); + core.registerErrorClass("BadResource", errors.BadResource); + core.registerErrorClass("Http", errors.Http); + core.registerErrorClass("Busy", errors.Busy); + core.registerErrorClass("NotSupported", errors.NotSupported); + core.registerErrorClass("Error", Error); + core.registerErrorClass("RangeError", RangeError); + core.registerErrorClass("ReferenceError", ReferenceError); + core.registerErrorClass("SyntaxError", SyntaxError); + core.registerErrorClass("TypeError", TypeError); + core.registerErrorClass("URIError", URIError); + } + + // https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope + const windowOrWorkerGlobalScope = { + Blob: util.nonEnumerable(fetch.Blob), + ByteLengthQueuingStrategy: util.nonEnumerable( + streams.ByteLengthQueuingStrategy, + ), + CloseEvent: util.nonEnumerable(CloseEvent), + CountQueuingStrategy: util.nonEnumerable( + streams.CountQueuingStrategy, + ), + CustomEvent: util.nonEnumerable(CustomEvent), + DOMException: util.nonEnumerable(DOMException), + ErrorEvent: util.nonEnumerable(ErrorEvent), + Event: util.nonEnumerable(Event), + EventTarget: util.nonEnumerable(EventTarget), + File: util.nonEnumerable(fetch.DomFile), + FileReader: util.nonEnumerable(fileReader.FileReader), + FormData: util.nonEnumerable(fetch.FormData), + Headers: util.nonEnumerable(headers.Headers), + MessageEvent: util.nonEnumerable(MessageEvent), + Performance: util.nonEnumerable(performance.Performance), + PerformanceEntry: util.nonEnumerable(performance.PerformanceEntry), + PerformanceMark: util.nonEnumerable(performance.PerformanceMark), + PerformanceMeasure: util.nonEnumerable(performance.PerformanceMeasure), + ProgressEvent: util.nonEnumerable(ProgressEvent), + ReadableStream: util.nonEnumerable(streams.ReadableStream), + Request: util.nonEnumerable(fetch.Request), + Response: util.nonEnumerable(fetch.Response), + TextDecoder: util.nonEnumerable(TextDecoder), + TextEncoder: util.nonEnumerable(TextEncoder), + TransformStream: util.nonEnumerable(streams.TransformStream), + URL: util.nonEnumerable(url.URL), + URLSearchParams: util.nonEnumerable(url.URLSearchParams), + WebSocket: util.nonEnumerable(webSocket.WebSocket), + Worker: util.nonEnumerable(worker.Worker), + WritableStream: util.nonEnumerable(streams.WritableStream), + atob: util.writable(atob), + btoa: util.writable(btoa), + clearInterval: util.writable(timers.clearInterval), + clearTimeout: util.writable(timers.clearTimeout), + console: util.writable(new Console(core.print)), + crypto: util.readOnly(crypto), + fetch: util.writable(fetch.fetch), + performance: util.writable(performance.performance), + setInterval: util.writable(timers.setInterval), + setTimeout: util.writable(timers.setTimeout), + }; + + const mainRuntimeGlobalProperties = { + Window: globalInterfaces.windowConstructorDescriptor, + window: util.readOnly(globalThis), + self: util.readOnly(globalThis), + // TODO(bartlomieju): from MDN docs (https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope) + // it seems those two properties should be available to workers as well + onload: util.writable(null), + onunload: util.writable(null), + close: util.writable(windowClose), + closed: util.getterOnly(() => windowIsClosing), + alert: util.writable(prompt.alert), + confirm: util.writable(prompt.confirm), + prompt: util.writable(prompt.prompt), + }; + + const workerRuntimeGlobalProperties = { + WorkerGlobalScope: globalInterfaces.workerGlobalScopeConstructorDescriptor, + DedicatedWorkerGlobalScope: + globalInterfaces.dedicatedWorkerGlobalScopeConstructorDescriptor, + self: util.readOnly(globalThis), + onmessage: util.writable(onmessage), + onerror: util.writable(onerror), + // TODO: should be readonly? + close: util.nonEnumerable(workerClose), + postMessage: util.writable(postMessage), + workerMessageRecvCallback: util.nonEnumerable(workerMessageRecvCallback), + }; + + let hasBootstrapped = false; + + function bootstrapMainRuntime(runtimeOptions) { + if (hasBootstrapped) { + throw new Error("Worker runtime already bootstrapped"); + } + // Remove bootstrapping data from the global scope + delete globalThis.__bootstrap; + delete globalThis.bootstrap; + util.log("bootstrapMainRuntime"); + hasBootstrapped = true; + Object.defineProperties(globalThis, windowOrWorkerGlobalScope); + Object.defineProperties(globalThis, mainRuntimeGlobalProperties); + Object.setPrototypeOf(globalThis, Window.prototype); + eventTarget.setEventTargetData(globalThis); + + defineEventHandler(window, "load", null); + defineEventHandler(window, "unload", null); + + runtimeStart(runtimeOptions); + const { args, noColor, pid, ppid, unstableFlag } = runtimeOptions; + + registerErrors(); + + const finalDenoNs = { + core, + internal: internalSymbol, + [internalSymbol]: internalObject, + resources: core.resources, + close: core.close, + ...denoNs, + }; + Object.defineProperties(finalDenoNs, { + pid: util.readOnly(pid), + ppid: util.readOnly(ppid), + noColor: util.readOnly(noColor), + args: util.readOnly(Object.freeze(args)), + mainModule: util.getterOnly(opMainModule), + }); + + if (unstableFlag) { + Object.assign(finalDenoNs, denoNsUnstable); + } + + // Setup `Deno` global - we're actually overriding already + // existing global `Deno` with `Deno` namespace from "./deno.ts". + util.immutableDefine(globalThis, "Deno", finalDenoNs); + Object.freeze(globalThis.Deno); + Object.freeze(globalThis.Deno.core); + Object.freeze(globalThis.Deno.core.sharedQueue); + signals.setSignals(); + + util.log("args", args); + } + + function bootstrapWorkerRuntime( + runtimeOptions, + name, + useDenoNamespace, + internalName, + ) { + if (hasBootstrapped) { + throw new Error("Worker runtime already bootstrapped"); + } + // Remove bootstrapping data from the global scope + delete globalThis.__bootstrap; + delete globalThis.bootstrap; + util.log("bootstrapWorkerRuntime"); + hasBootstrapped = true; + Object.defineProperties(globalThis, windowOrWorkerGlobalScope); + Object.defineProperties(globalThis, workerRuntimeGlobalProperties); + Object.defineProperties(globalThis, { name: util.readOnly(name) }); + Object.setPrototypeOf(globalThis, DedicatedWorkerGlobalScope.prototype); + eventTarget.setEventTargetData(globalThis); + + runtimeStart( + runtimeOptions, + internalName ?? name, + ); + const { unstableFlag, pid, noColor, args } = runtimeOptions; + + registerErrors(); + + const finalDenoNs = { + core, + internal: internalSymbol, + [internalSymbol]: internalObject, + resources: core.resources, + close: core.close, + ...denoNs, + }; + if (useDenoNamespace) { + if (unstableFlag) { + Object.assign(finalDenoNs, denoNsUnstable); + } + Object.defineProperties(finalDenoNs, { + pid: util.readOnly(pid), + noColor: util.readOnly(noColor), + args: util.readOnly(Object.freeze(args)), + }); + // Setup `Deno` global - we're actually overriding already + // existing global `Deno` with `Deno` namespace from "./deno.ts". + util.immutableDefine(globalThis, "Deno", finalDenoNs); + Object.freeze(globalThis.Deno); + Object.freeze(globalThis.Deno.core); + Object.freeze(globalThis.Deno.core.sharedQueue); + signals.setSignals(); + } else { + delete globalThis.Deno; + util.assert(globalThis.Deno === undefined); + } + } + + Object.defineProperties(globalThis, { + bootstrap: { + value: { + mainRuntime: bootstrapMainRuntime, + workerRuntime: bootstrapWorkerRuntime, + }, + configurable: true, + }, + }); +})(this); diff --git a/runtime/rt/README.md b/runtime/rt/README.md new file mode 100644 index 000000000..b17fa22e5 --- /dev/null +++ b/runtime/rt/README.md @@ -0,0 +1,59 @@ +# Runtime JavaScript Code + +This directory contains Deno runtime code written in plain JavaScript. + +Each file is a plain, old **script**, not ES modules. The reason is that +snapshotting ES modules is much harder, especially if one needs to manipulate +global scope (like in case of Deno). + +Each file is prefixed with a number, telling in which order scripts should be +loaded into V8 isolate. This is temporary solution and we're striving not to +require specific order (though it's not 100% obvious if that's feasible). + +## Deno Web APIs + +This directory facilities Web APIs that are available in Deno. + +Please note, that some implementations might not be completely aligned with +specification. + +Some Web APIs are using ops under the hood, eg. `console`, `performance`. + +## Implemented Web APIs + +- [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob): for + representing opaque binary data. +- [Console](https://developer.mozilla.org/en-US/docs/Web/API/Console): for + logging purposes. +- [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent), + [EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) + and + [EventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventListener): + to work with DOM events. + - **Implementation notes:** There is no DOM hierarchy in Deno, so there is no + tree for Events to bubble/capture through. +- [fetch](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch), + [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request), + [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response), + [Body](https://developer.mozilla.org/en-US/docs/Web/API/Body) and + [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers): modern + Promise-based HTTP Request API. +- [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData): access + to a `multipart/form-data` serialization. +- [Performance](https://developer.mozilla.org/en-US/docs/Web/API/Performance): + retrieving current time with a high precision. +- [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout), + [setInterval](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval), + [clearTimeout](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearTimeout): + scheduling callbacks in future and + [clearInterval](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearInterval). +- [Stream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) for + creating, composing, and consuming streams of data. +- [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) and + [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams): + to construct and parse URLSs. +- [Worker](https://developer.mozilla.org/en-US/docs/Web/API/Worker): executing + additional code in a separate thread. + - **Implementation notes:** Blob URLs are not supported, object ownership + cannot be transferred, posted data is serialized to JSON instead of + [structured cloning](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). diff --git a/runtime/tokio_util.rs b/runtime/tokio_util.rs new file mode 100644 index 000000000..b25a2994f --- /dev/null +++ b/runtime/tokio_util.rs @@ -0,0 +1,25 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +pub fn create_basic_runtime() -> tokio::runtime::Runtime { + tokio::runtime::Builder::new() + .basic_scheduler() + .enable_io() + .enable_time() + // This limits the number of threads for blocking operations (like for + // synchronous fs ops) or CPU bound tasks like when we run dprint in + // parallel for deno fmt. + // The default value is 512, which is an unhelpfully large thread pool. We + // don't ever want to have more than a couple dozen threads. + .max_threads(32) + .build() + .unwrap() +} + +// TODO(ry) rename to run_local ? +pub fn run_basic(future: F) -> R +where + F: std::future::Future, +{ + let mut rt = create_basic_runtime(); + rt.block_on(future) +} diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs new file mode 100644 index 000000000..db97e3604 --- /dev/null +++ b/runtime/web_worker.rs @@ -0,0 +1,595 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::colors; +use crate::inspector::DenoInspector; +use crate::inspector::InspectorServer; +use crate::js; +use crate::metrics::Metrics; +use crate::ops; +use crate::permissions::Permissions; +use crate::tokio_util::create_basic_runtime; +use deno_core::error::AnyError; +use deno_core::futures::channel::mpsc; +use deno_core::futures::future::poll_fn; +use deno_core::futures::future::FutureExt; +use deno_core::futures::stream::StreamExt; +use deno_core::futures::task::AtomicWaker; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::url::Url; +use deno_core::v8; +use deno_core::GetErrorClassFn; +use deno_core::JsErrorCreateFn; +use deno_core::JsRuntime; +use deno_core::ModuleLoader; +use deno_core::ModuleSpecifier; +use deno_core::RuntimeOptions; +use std::env; +use std::rc::Rc; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::task::Context; +use std::task::Poll; +use tokio::sync::Mutex as AsyncMutex; + +/// Events that are sent to host from child +/// worker. +pub enum WorkerEvent { + Message(Box<[u8]>), + Error(AnyError), + TerminalError(AnyError), +} + +pub struct WorkerChannelsInternal { + pub sender: mpsc::Sender, + pub receiver: mpsc::Receiver>, +} + +/// Wrapper for `WorkerHandle` that adds functionality +/// for terminating workers. +/// +/// This struct is used by host as well as worker itself. +/// +/// Host uses it to communicate with worker and terminate it, +/// while worker uses it only to finish execution on `self.close()`. +#[derive(Clone)] +pub struct WebWorkerHandle { + pub sender: mpsc::Sender>, + pub receiver: Arc>>, + terminate_tx: mpsc::Sender<()>, + terminated: Arc, + isolate_handle: v8::IsolateHandle, +} + +impl WebWorkerHandle { + /// Post message to worker as a host. + pub fn post_message(&self, buf: Box<[u8]>) -> Result<(), AnyError> { + let mut sender = self.sender.clone(); + sender.try_send(buf)?; + Ok(()) + } + + /// Get the event with lock. + /// Return error if more than one listener tries to get event + pub async fn get_event(&self) -> Result, AnyError> { + let mut receiver = self.receiver.try_lock()?; + Ok(receiver.next().await) + } + + pub fn terminate(&self) { + // This function can be called multiple times by whomever holds + // the handle. However only a single "termination" should occur so + // we need a guard here. + let already_terminated = self.terminated.swap(true, Ordering::SeqCst); + + if !already_terminated { + self.isolate_handle.terminate_execution(); + let mut sender = self.terminate_tx.clone(); + // This call should be infallible hence the `expect`. + // This might change in the future. + sender.try_send(()).expect("Failed to terminate"); + } + } +} + +fn create_channels( + isolate_handle: v8::IsolateHandle, + terminate_tx: mpsc::Sender<()>, +) -> (WorkerChannelsInternal, WebWorkerHandle) { + let (in_tx, in_rx) = mpsc::channel::>(1); + let (out_tx, out_rx) = mpsc::channel::(1); + let internal_channels = WorkerChannelsInternal { + sender: out_tx, + receiver: in_rx, + }; + let external_channels = WebWorkerHandle { + sender: in_tx, + receiver: Arc::new(AsyncMutex::new(out_rx)), + terminated: Arc::new(AtomicBool::new(false)), + terminate_tx, + isolate_handle, + }; + (internal_channels, external_channels) +} + +/// This struct is an implementation of `Worker` Web API +/// +/// Each `WebWorker` is either a child of `MainWorker` or other +/// `WebWorker`. +pub struct WebWorker { + id: u32, + inspector: Option>, + // Following fields are pub because they are accessed + // when creating a new WebWorker instance. + pub(crate) internal_channels: WorkerChannelsInternal, + pub js_runtime: JsRuntime, + pub name: String, + waker: AtomicWaker, + event_loop_idle: bool, + terminate_rx: mpsc::Receiver<()>, + handle: WebWorkerHandle, + pub use_deno_namespace: bool, +} + +pub struct WebWorkerOptions { + /// Sets `Deno.args` in JS runtime. + pub args: Vec, + pub debug_flag: bool, + pub unstable: bool, + pub ca_filepath: Option, + pub user_agent: String, + pub seed: Option, + pub module_loader: Rc, + pub create_web_worker_cb: Arc, + pub js_error_create_fn: Option>, + pub use_deno_namespace: bool, + pub attach_inspector: bool, + pub maybe_inspector_server: Option>, + pub apply_source_maps: bool, + /// Sets `Deno.version.deno` in JS runtime. + pub runtime_version: String, + /// Sets `Deno.version.typescript` in JS runtime. + pub ts_version: String, + /// Sets `Deno.noColor` in JS runtime. + pub no_color: bool, + pub get_error_class_fn: Option, +} + +impl WebWorker { + pub fn from_options( + name: String, + permissions: Permissions, + main_module: ModuleSpecifier, + worker_id: u32, + options: &WebWorkerOptions, + ) -> Self { + let mut js_runtime = JsRuntime::new(RuntimeOptions { + module_loader: Some(options.module_loader.clone()), + startup_snapshot: Some(js::deno_isolate_init()), + js_error_create_fn: options.js_error_create_fn.clone(), + get_error_class_fn: options.get_error_class_fn, + ..Default::default() + }); + + let inspector = if options.attach_inspector { + Some(DenoInspector::new( + &mut js_runtime, + options.maybe_inspector_server.clone(), + )) + } else { + None + }; + + let (terminate_tx, terminate_rx) = mpsc::channel::<()>(1); + let isolate_handle = js_runtime.v8_isolate().thread_safe_handle(); + let (internal_channels, handle) = + create_channels(isolate_handle, terminate_tx); + + let mut worker = Self { + id: worker_id, + inspector, + internal_channels, + js_runtime, + name, + waker: AtomicWaker::new(), + event_loop_idle: false, + terminate_rx, + handle, + use_deno_namespace: options.use_deno_namespace, + }; + + { + let handle = worker.thread_safe_handle(); + let sender = worker.internal_channels.sender.clone(); + let js_runtime = &mut worker.js_runtime; + // All ops registered in this function depend on these + { + let op_state = js_runtime.op_state(); + let mut op_state = op_state.borrow_mut(); + op_state.put::(Default::default()); + op_state.put::(permissions); + op_state.put::(ops::UnstableChecker { + unstable: options.unstable, + }); + } + + ops::web_worker::init(js_runtime, sender.clone(), handle); + ops::runtime::init(js_runtime, main_module); + ops::fetch::init( + js_runtime, + options.user_agent.clone(), + options.ca_filepath.as_deref(), + ); + ops::timers::init(js_runtime); + ops::worker_host::init( + js_runtime, + Some(sender), + options.create_web_worker_cb.clone(), + ); + ops::reg_json_sync(js_runtime, "op_close", deno_core::op_close); + ops::reg_json_sync(js_runtime, "op_resources", deno_core::op_resources); + ops::reg_json_sync( + js_runtime, + "op_domain_to_ascii", + deno_web::op_domain_to_ascii, + ); + ops::io::init(js_runtime); + ops::websocket::init( + js_runtime, + options.ca_filepath.as_deref(), + options.user_agent.clone(), + ); + + if options.use_deno_namespace { + ops::fs_events::init(js_runtime); + ops::fs::init(js_runtime); + ops::net::init(js_runtime); + ops::os::init(js_runtime); + ops::permissions::init(js_runtime); + ops::plugin::init(js_runtime); + ops::process::init(js_runtime); + ops::crypto::init(js_runtime, options.seed); + ops::signal::init(js_runtime); + ops::tls::init(js_runtime); + ops::tty::init(js_runtime); + + let op_state = js_runtime.op_state(); + let mut op_state = op_state.borrow_mut(); + let (stdin, stdout, stderr) = ops::io::get_stdio(); + if let Some(stream) = stdin { + op_state.resource_table.add("stdin", Box::new(stream)); + } + if let Some(stream) = stdout { + op_state.resource_table.add("stdout", Box::new(stream)); + } + if let Some(stream) = stderr { + op_state.resource_table.add("stderr", Box::new(stream)); + } + } + + worker + } + } + + pub fn bootstrap(&mut self, options: &WebWorkerOptions) { + let runtime_options = json!({ + "args": options.args, + "applySourceMaps": options.apply_source_maps, + "debugFlag": options.debug_flag, + "denoVersion": options.runtime_version, + "noColor": options.no_color, + "pid": std::process::id(), + "ppid": ops::runtime::ppid(), + "target": env!("TARGET"), + "tsVersion": options.ts_version, + "unstableFlag": options.unstable, + "v8Version": deno_core::v8_version(), + }); + + let runtime_options_str = + serde_json::to_string_pretty(&runtime_options).unwrap(); + + // Instead of using name for log we use `worker-${id}` because + // WebWorkers can have empty string as name. + let script = format!( + "bootstrap.workerRuntime({}, \"{}\", {}, \"worker-{}\")", + runtime_options_str, self.name, options.use_deno_namespace, self.id + ); + self + .execute(&script) + .expect("Failed to execute worker bootstrap script"); + } + + /// Same as execute2() but the filename defaults to "$CWD/__anonymous__". + pub fn execute(&mut self, js_source: &str) -> Result<(), AnyError> { + let path = env::current_dir().unwrap().join("__anonymous__"); + let url = Url::from_file_path(path).unwrap(); + self.js_runtime.execute(url.as_str(), js_source) + } + + /// Loads, instantiates and executes specified JavaScript module. + pub async fn execute_module( + &mut self, + module_specifier: &ModuleSpecifier, + ) -> Result<(), AnyError> { + let id = self.js_runtime.load_module(module_specifier, None).await?; + self.js_runtime.mod_evaluate(id).await + } + + /// Returns a way to communicate with the Worker from other threads. + pub fn thread_safe_handle(&self) -> WebWorkerHandle { + self.handle.clone() + } + + pub fn has_been_terminated(&self) -> bool { + self.handle.terminated.load(Ordering::SeqCst) + } + + pub fn poll_event_loop( + &mut self, + cx: &mut Context, + ) -> Poll> { + if self.has_been_terminated() { + return Poll::Ready(Ok(())); + } + + if !self.event_loop_idle { + let poll_result = { + // We always poll the inspector if it exists. + let _ = self.inspector.as_mut().map(|i| i.poll_unpin(cx)); + self.waker.register(cx.waker()); + self.js_runtime.poll_event_loop(cx) + }; + + if let Poll::Ready(r) = poll_result { + if self.has_been_terminated() { + return Poll::Ready(Ok(())); + } + + if let Err(e) = r { + print_worker_error(e.to_string(), &self.name); + let mut sender = self.internal_channels.sender.clone(); + sender + .try_send(WorkerEvent::Error(e)) + .expect("Failed to post message to host"); + } + self.event_loop_idle = true; + } + } + + if let Poll::Ready(r) = self.terminate_rx.poll_next_unpin(cx) { + // terminate_rx should never be closed + assert!(r.is_some()); + return Poll::Ready(Ok(())); + } + + let maybe_msg_poll_result = + self.internal_channels.receiver.poll_next_unpin(cx); + + if let Poll::Ready(maybe_msg) = maybe_msg_poll_result { + let msg = + maybe_msg.expect("Received `None` instead of message in worker"); + let msg = String::from_utf8(msg.to_vec()).unwrap(); + let script = format!("workerMessageRecvCallback({})", msg); + + if let Err(e) = self.execute(&script) { + // If execution was terminated during message callback then + // just ignore it + if self.has_been_terminated() { + return Poll::Ready(Ok(())); + } + + // Otherwise forward error to host + let mut sender = self.internal_channels.sender.clone(); + sender + .try_send(WorkerEvent::Error(e)) + .expect("Failed to post message to host"); + } + + // Let event loop be polled again + self.event_loop_idle = false; + self.waker.wake(); + } + + Poll::Pending + } + + pub async fn run_event_loop(&mut self) -> Result<(), AnyError> { + poll_fn(|cx| self.poll_event_loop(cx)).await + } +} + +impl Drop for WebWorker { + fn drop(&mut self) { + // The Isolate object must outlive the Inspector object, but this is + // currently not enforced by the type system. + self.inspector.take(); + } +} + +fn print_worker_error(error_str: String, name: &str) { + eprintln!( + "{}: Uncaught (in worker \"{}\") {}", + colors::red_bold("error"), + name, + error_str.trim_start_matches("Uncaught "), + ); +} + +/// This function should be called from a thread dedicated to this worker. +// TODO(bartlomieju): check if order of actions is aligned to Worker spec +pub fn run_web_worker( + mut worker: WebWorker, + specifier: ModuleSpecifier, + maybe_source_code: Option, +) -> Result<(), AnyError> { + let name = worker.name.to_string(); + + let mut rt = create_basic_runtime(); + + // TODO(bartlomieju): run following block using "select!" + // with terminate + + // Execute provided source code immediately + let result = if let Some(source_code) = maybe_source_code { + worker.execute(&source_code) + } else { + // TODO(bartlomieju): add "type": "classic", ie. ability to load + // script instead of module + let load_future = worker.execute_module(&specifier).boxed_local(); + + rt.block_on(load_future) + }; + + let mut sender = worker.internal_channels.sender.clone(); + + // If sender is closed it means that worker has already been closed from + // within using "globalThis.close()" + if sender.is_closed() { + return Ok(()); + } + + if let Err(e) = result { + print_worker_error(e.to_string(), &name); + sender + .try_send(WorkerEvent::TerminalError(e)) + .expect("Failed to post message to host"); + + // Failure to execute script is a terminal error, bye, bye. + return Ok(()); + } + + let result = rt.block_on(worker.run_event_loop()); + debug!("Worker thread shuts down {}", &name); + result +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tokio_util; + use deno_core::serde_json::json; + + fn create_test_web_worker() -> WebWorker { + let main_module = + ModuleSpecifier::resolve_url_or_path("./hello.js").unwrap(); + let module_loader = Rc::new(deno_core::NoopModuleLoader); + let create_web_worker_cb = Arc::new(|_| unreachable!()); + + let options = WebWorkerOptions { + args: vec![], + apply_source_maps: false, + debug_flag: false, + unstable: false, + ca_filepath: None, + user_agent: "x".to_string(), + seed: None, + module_loader, + create_web_worker_cb, + js_error_create_fn: None, + use_deno_namespace: false, + attach_inspector: false, + maybe_inspector_server: None, + runtime_version: "x".to_string(), + ts_version: "x".to_string(), + no_color: true, + get_error_class_fn: None, + }; + + let mut worker = WebWorker::from_options( + "TEST".to_string(), + Permissions::allow_all(), + main_module, + 1, + &options, + ); + worker.bootstrap(&options); + worker + } + + #[tokio::test] + async fn test_worker_messages() { + let (handle_sender, handle_receiver) = + std::sync::mpsc::sync_channel::(1); + + let join_handle = std::thread::spawn(move || { + let mut worker = create_test_web_worker(); + let source = r#" + onmessage = function(e) { + console.log("msg from main script", e.data); + if (e.data == "exit") { + return close(); + } else { + console.assert(e.data === "hi"); + } + postMessage([1, 2, 3]); + console.log("after postMessage"); + } + "#; + worker.execute(source).unwrap(); + let handle = worker.thread_safe_handle(); + handle_sender.send(handle).unwrap(); + let r = tokio_util::run_basic(worker.run_event_loop()); + assert!(r.is_ok()) + }); + + let mut handle = handle_receiver.recv().unwrap(); + + let msg = json!("hi").to_string().into_boxed_str().into_boxed_bytes(); + let r = handle.post_message(msg.clone()); + assert!(r.is_ok()); + + let maybe_msg = handle.get_event().await.unwrap(); + assert!(maybe_msg.is_some()); + + let r = handle.post_message(msg.clone()); + assert!(r.is_ok()); + + let maybe_msg = handle.get_event().await.unwrap(); + assert!(maybe_msg.is_some()); + match maybe_msg { + Some(WorkerEvent::Message(buf)) => { + assert_eq!(*buf, *b"[1,2,3]"); + } + _ => unreachable!(), + } + + let msg = json!("exit") + .to_string() + .into_boxed_str() + .into_boxed_bytes(); + let r = handle.post_message(msg); + assert!(r.is_ok()); + let event = handle.get_event().await.unwrap(); + assert!(event.is_none()); + handle.sender.close_channel(); + join_handle.join().expect("Failed to join worker thread"); + } + + #[tokio::test] + async fn removed_from_resource_table_on_close() { + let (handle_sender, handle_receiver) = + std::sync::mpsc::sync_channel::(1); + + let join_handle = std::thread::spawn(move || { + let mut worker = create_test_web_worker(); + worker.execute("onmessage = () => { close(); }").unwrap(); + let handle = worker.thread_safe_handle(); + handle_sender.send(handle).unwrap(); + let r = tokio_util::run_basic(worker.run_event_loop()); + assert!(r.is_ok()) + }); + + let mut handle = handle_receiver.recv().unwrap(); + + let msg = json!("hi").to_string().into_boxed_str().into_boxed_bytes(); + let r = handle.post_message(msg.clone()); + assert!(r.is_ok()); + let event = handle.get_event().await.unwrap(); + assert!(event.is_none()); + handle.sender.close_channel(); + + join_handle.join().expect("Failed to join worker thread"); + } +} diff --git a/runtime/worker.rs b/runtime/worker.rs new file mode 100644 index 000000000..a0e63afad --- /dev/null +++ b/runtime/worker.rs @@ -0,0 +1,349 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::inspector::DenoInspector; +use crate::inspector::InspectorServer; +use crate::inspector::InspectorSession; +use crate::js; +use crate::metrics::Metrics; +use crate::ops; +use crate::permissions::Permissions; +use deno_core::error::AnyError; +use deno_core::futures::future::poll_fn; +use deno_core::futures::future::FutureExt; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::url::Url; +use deno_core::GetErrorClassFn; +use deno_core::JsErrorCreateFn; +use deno_core::JsRuntime; +use deno_core::ModuleId; +use deno_core::ModuleLoader; +use deno_core::ModuleSpecifier; +use deno_core::RuntimeOptions; +use std::env; +use std::rc::Rc; +use std::sync::Arc; +use std::task::Context; +use std::task::Poll; + +/// This worker is created and used by almost all +/// subcommands in Deno executable. +/// +/// It provides ops available in the `Deno` namespace. +/// +/// All `WebWorker`s created during program execution +/// are descendants of this worker. +pub struct MainWorker { + inspector: Option>, + pub js_runtime: JsRuntime, + should_break_on_first_statement: bool, +} + +pub struct WorkerOptions { + pub apply_source_maps: bool, + /// Sets `Deno.args` in JS runtime. + pub args: Vec, + pub debug_flag: bool, + pub unstable: bool, + pub ca_filepath: Option, + pub user_agent: String, + pub seed: Option, + pub module_loader: Rc, + // Callback that will be invoked when creating new instance + // of WebWorker + pub create_web_worker_cb: Arc, + pub js_error_create_fn: Option>, + pub attach_inspector: bool, + pub maybe_inspector_server: Option>, + pub should_break_on_first_statement: bool, + /// Sets `Deno.version.deno` in JS runtime. + pub runtime_version: String, + /// Sets `Deno.version.typescript` in JS runtime. + pub ts_version: String, + /// Sets `Deno.noColor` in JS runtime. + pub no_color: bool, + pub get_error_class_fn: Option, +} + +impl MainWorker { + pub fn from_options( + main_module: ModuleSpecifier, + permissions: Permissions, + options: &WorkerOptions, + ) -> Self { + let mut js_runtime = JsRuntime::new(RuntimeOptions { + module_loader: Some(options.module_loader.clone()), + startup_snapshot: Some(js::deno_isolate_init()), + js_error_create_fn: options.js_error_create_fn.clone(), + get_error_class_fn: options.get_error_class_fn, + ..Default::default() + }); + + let inspector = if options.attach_inspector { + Some(DenoInspector::new( + &mut js_runtime, + options.maybe_inspector_server.clone(), + )) + } else { + None + }; + let should_break_on_first_statement = + inspector.is_some() && options.should_break_on_first_statement; + + let mut worker = Self { + inspector, + js_runtime, + should_break_on_first_statement, + }; + + let js_runtime = &mut worker.js_runtime; + { + // All ops registered in this function depend on these + { + let op_state = js_runtime.op_state(); + let mut op_state = op_state.borrow_mut(); + op_state.put::(Default::default()); + op_state.put::(permissions); + op_state.put::(ops::UnstableChecker { + unstable: options.unstable, + }); + } + + ops::runtime::init(js_runtime, main_module); + ops::fetch::init( + js_runtime, + options.user_agent.clone(), + options.ca_filepath.as_deref(), + ); + ops::timers::init(js_runtime); + ops::worker_host::init( + js_runtime, + None, + options.create_web_worker_cb.clone(), + ); + ops::crypto::init(js_runtime, options.seed); + ops::reg_json_sync(js_runtime, "op_close", deno_core::op_close); + ops::reg_json_sync(js_runtime, "op_resources", deno_core::op_resources); + ops::reg_json_sync( + js_runtime, + "op_domain_to_ascii", + deno_web::op_domain_to_ascii, + ); + ops::fs_events::init(js_runtime); + ops::fs::init(js_runtime); + ops::io::init(js_runtime); + ops::net::init(js_runtime); + ops::os::init(js_runtime); + ops::permissions::init(js_runtime); + ops::plugin::init(js_runtime); + ops::process::init(js_runtime); + ops::signal::init(js_runtime); + ops::tls::init(js_runtime); + ops::tty::init(js_runtime); + ops::websocket::init( + js_runtime, + options.ca_filepath.as_deref(), + options.user_agent.clone(), + ); + } + { + let op_state = js_runtime.op_state(); + let mut op_state = op_state.borrow_mut(); + let t = &mut op_state.resource_table; + let (stdin, stdout, stderr) = ops::io::get_stdio(); + if let Some(stream) = stdin { + t.add("stdin", Box::new(stream)); + } + if let Some(stream) = stdout { + t.add("stdout", Box::new(stream)); + } + if let Some(stream) = stderr { + t.add("stderr", Box::new(stream)); + } + } + + worker + } + + pub fn bootstrap(&mut self, options: &WorkerOptions) { + let runtime_options = json!({ + "args": options.args, + "applySourceMaps": options.apply_source_maps, + "debugFlag": options.debug_flag, + "denoVersion": options.runtime_version, + "noColor": options.no_color, + "pid": std::process::id(), + "ppid": ops::runtime::ppid(), + "target": env!("TARGET"), + "tsVersion": options.ts_version, + "unstableFlag": options.unstable, + "v8Version": deno_core::v8_version(), + }); + + let script = format!( + "bootstrap.mainRuntime({})", + serde_json::to_string_pretty(&runtime_options).unwrap() + ); + self + .execute(&script) + .expect("Failed to execute bootstrap script"); + } + + /// Same as execute2() but the filename defaults to "$CWD/__anonymous__". + pub fn execute(&mut self, js_source: &str) -> Result<(), AnyError> { + let path = env::current_dir().unwrap().join("__anonymous__"); + let url = Url::from_file_path(path).unwrap(); + self.js_runtime.execute(url.as_str(), js_source) + } + + /// Loads and instantiates specified JavaScript module. + pub async fn preload_module( + &mut self, + module_specifier: &ModuleSpecifier, + ) -> Result { + self.js_runtime.load_module(module_specifier, None).await + } + + /// Loads, instantiates and executes specified JavaScript module. + pub async fn execute_module( + &mut self, + module_specifier: &ModuleSpecifier, + ) -> Result<(), AnyError> { + let id = self.preload_module(module_specifier).await?; + self.wait_for_inspector_session(); + self.js_runtime.mod_evaluate(id).await + } + + fn wait_for_inspector_session(&mut self) { + if self.should_break_on_first_statement { + self + .inspector + .as_mut() + .unwrap() + .wait_for_session_and_break_on_next_statement() + } + } + + /// Create new inspector session. This function panics if Worker + /// was not configured to create inspector. + pub fn create_inspector_session(&mut self) -> Box { + let inspector = self.inspector.as_mut().unwrap(); + + InspectorSession::new(&mut **inspector) + } + + pub fn poll_event_loop( + &mut self, + cx: &mut Context, + ) -> Poll> { + // We always poll the inspector if it exists. + let _ = self.inspector.as_mut().map(|i| i.poll_unpin(cx)); + self.js_runtime.poll_event_loop(cx) + } + + pub async fn run_event_loop(&mut self) -> Result<(), AnyError> { + poll_fn(|cx| self.poll_event_loop(cx)).await + } +} + +impl Drop for MainWorker { + fn drop(&mut self) { + // The Isolate object must outlive the Inspector object, but this is + // currently not enforced by the type system. + self.inspector.take(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_test_worker() -> MainWorker { + let main_module = + ModuleSpecifier::resolve_url_or_path("./hello.js").unwrap(); + let permissions = Permissions::default(); + + let options = WorkerOptions { + apply_source_maps: false, + user_agent: "x".to_string(), + args: vec![], + debug_flag: false, + unstable: false, + ca_filepath: None, + seed: None, + js_error_create_fn: None, + create_web_worker_cb: Arc::new(|_| unreachable!()), + attach_inspector: false, + maybe_inspector_server: None, + should_break_on_first_statement: false, + module_loader: Rc::new(deno_core::FsModuleLoader), + runtime_version: "x".to_string(), + ts_version: "x".to_string(), + no_color: true, + get_error_class_fn: None, + }; + + MainWorker::from_options(main_module, permissions, &options) + } + + #[tokio::test] + async fn execute_mod_esm_imports_a() { + let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("cli/tests/esm_imports_a.js"); + let module_specifier = + ModuleSpecifier::resolve_url_or_path(&p.to_string_lossy()).unwrap(); + let mut worker = create_test_worker(); + let result = worker.execute_module(&module_specifier).await; + if let Err(err) = result { + eprintln!("execute_mod err {:?}", err); + } + if let Err(e) = worker.run_event_loop().await { + panic!("Future got unexpected error: {:?}", e); + } + } + + #[tokio::test] + async fn execute_mod_circular() { + let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("tests/circular1.js"); + let module_specifier = + ModuleSpecifier::resolve_url_or_path(&p.to_string_lossy()).unwrap(); + let mut worker = create_test_worker(); + let result = worker.execute_module(&module_specifier).await; + if let Err(err) = result { + eprintln!("execute_mod err {:?}", err); + } + if let Err(e) = worker.run_event_loop().await { + panic!("Future got unexpected error: {:?}", e); + } + } + + #[tokio::test] + async fn execute_mod_resolve_error() { + // "foo" is not a valid module specifier so this should return an error. + let mut worker = create_test_worker(); + let module_specifier = + ModuleSpecifier::resolve_url_or_path("does-not-exist").unwrap(); + let result = worker.execute_module(&module_specifier).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn execute_mod_002_hello() { + // This assumes cwd is project root (an assumption made throughout the + // tests). + let mut worker = create_test_worker(); + let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("cli/tests/001_hello.js"); + let module_specifier = + ModuleSpecifier::resolve_url_or_path(&p.to_string_lossy()).unwrap(); + let result = worker.execute_module(&module_specifier).await; + assert!(result.is_ok()); + } +} -- cgit v1.2.3