diff options
Diffstat (limited to 'cli')
-rw-r--r-- | cli/ansi.rs | 70 | ||||
-rw-r--r-- | cli/cli.rs | 90 | ||||
-rw-r--r-- | cli/compiler.rs | 151 | ||||
-rw-r--r-- | cli/deno_dir.rs | 1369 | ||||
-rw-r--r-- | cli/errors.rs | 207 | ||||
-rw-r--r-- | cli/flags.rs | 291 | ||||
-rw-r--r-- | cli/fs.rs | 110 | ||||
-rw-r--r-- | cli/global_timer.rs | 49 | ||||
-rw-r--r-- | cli/http_body.rs | 112 | ||||
-rw-r--r-- | cli/http_util.rs | 166 | ||||
-rw-r--r-- | cli/isolate.rs | 236 | ||||
-rw-r--r-- | cli/isolate_state.rs | 110 | ||||
-rw-r--r-- | cli/js_errors.rs | 424 | ||||
-rw-r--r-- | cli/main.rs | 140 | ||||
-rw-r--r-- | cli/modules.rs | 204 | ||||
-rw-r--r-- | cli/msg.fbs | 524 | ||||
-rw-r--r-- | cli/msg.rs | 26 | ||||
-rw-r--r-- | cli/msg_util.rs | 127 | ||||
-rw-r--r-- | cli/ops.rs | 2020 | ||||
-rw-r--r-- | cli/permissions.rs | 343 | ||||
-rw-r--r-- | cli/repl.rs | 114 | ||||
-rw-r--r-- | cli/resolve_addr.rs | 156 | ||||
-rw-r--r-- | cli/resources.rs | 494 | ||||
-rw-r--r-- | cli/startup_data.rs | 57 | ||||
-rw-r--r-- | cli/tokio_util.rs | 118 | ||||
-rw-r--r-- | cli/tokio_write.rs | 62 | ||||
-rw-r--r-- | cli/version.rs | 6 | ||||
-rw-r--r-- | cli/workers.rs | 181 |
28 files changed, 7957 insertions, 0 deletions
diff --git a/cli/ansi.rs b/cli/ansi.rs new file mode 100644 index 000000000..95b5e0694 --- /dev/null +++ b/cli/ansi.rs @@ -0,0 +1,70 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use ansi_term::Color::Fixed; +use ansi_term::Color::Red; +use ansi_term::Style; +use regex::Regex; +use std::env; +use std::fmt; + +lazy_static! { + // STRIP_ANSI_RE and strip_ansi_codes are lifted from the "console" crate. + // Copyright 2017 Armin Ronacher <armin.ronacher@active-4.com>. 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<str> { + STRIP_ANSI_RE.replace_all(s, "") +} + +pub fn use_color() -> bool { + !(*NO_COLOR) +} + +pub fn red_bold(s: String) -> impl fmt::Display { + let mut style = Style::new(); + if use_color() { + style = style.bold().fg(Red); + } + style.paint(s) +} + +pub fn italic_bold(s: String) -> impl fmt::Display { + let mut style = Style::new(); + if use_color() { + style = style.italic().bold(); + } + style.paint(s) +} + +pub fn yellow(s: String) -> impl fmt::Display { + let mut style = Style::new(); + if use_color() { + // matches TypeScript's ForegroundColorEscapeSequences.Yellow + style = style.fg(Fixed(11)); + } + style.paint(s) +} + +pub fn cyan(s: String) -> impl fmt::Display { + let mut style = Style::new(); + if use_color() { + // matches TypeScript's ForegroundColorEscapeSequences.Cyan + style = style.fg(Fixed(14)); + } + style.paint(s) +} + +pub fn bold(s: String) -> impl fmt::Display { + let mut style = Style::new(); + if use_color() { + style = style.bold(); + } + style.paint(s) +} diff --git a/cli/cli.rs b/cli/cli.rs new file mode 100644 index 000000000..42b2b29f8 --- /dev/null +++ b/cli/cli.rs @@ -0,0 +1,90 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +#![allow(unused_variables)] +#![allow(dead_code)] + +use crate::errors::DenoResult; +use crate::isolate_state::IsolateState; +use crate::ops; +use crate::permissions::DenoPermissions; +use deno_core::deno_buf; +use deno_core::deno_mod; +use deno_core::Behavior; +use deno_core::Op; +use deno_core::StartupData; +use std::sync::atomic::Ordering; +use std::sync::Arc; + +// Buf represents a byte array returned from a "Op". The message might be empty +// (which will be translated into a null object on the javascript side) or it is +// a heap allocated opaque sequence of bytes. Usually a flatbuffer message. +pub type Buf = Box<[u8]>; + +/// Implements deno_core::Behavior for the main Deno command-line. +pub struct Cli { + startup_data: Option<StartupData>, + pub state: Arc<IsolateState>, + pub permissions: Arc<DenoPermissions>, // TODO(ry) move to IsolateState +} + +impl Cli { + pub fn new( + startup_data: Option<StartupData>, + state: Arc<IsolateState>, + permissions: DenoPermissions, + ) -> Self { + Self { + startup_data, + state, + permissions: Arc::new(permissions), + } + } + + #[inline] + pub fn check_read(&self, filename: &str) -> DenoResult<()> { + self.permissions.check_read(filename) + } + + #[inline] + pub fn check_write(&self, filename: &str) -> DenoResult<()> { + self.permissions.check_write(filename) + } + + #[inline] + pub fn check_env(&self) -> DenoResult<()> { + self.permissions.check_env() + } + + #[inline] + pub fn check_net(&self, filename: &str) -> DenoResult<()> { + self.permissions.check_net(filename) + } + + #[inline] + pub fn check_run(&self) -> DenoResult<()> { + self.permissions.check_run() + } +} + +impl Behavior for Cli { + fn startup_data(&mut self) -> Option<StartupData> { + self.startup_data.take() + } + + fn resolve(&mut self, specifier: &str, referrer: deno_mod) -> deno_mod { + self + .state + .metrics + .resolve_count + .fetch_add(1, Ordering::Relaxed); + let mut modules = self.state.modules.lock().unwrap(); + modules.resolve_cb(&self.state.dir, specifier, referrer) + } + + fn dispatch( + &mut self, + control: &[u8], + zero_copy: deno_buf, + ) -> (bool, Box<Op>) { + ops::dispatch(self, control, zero_copy) + } +} diff --git a/cli/compiler.rs b/cli/compiler.rs new file mode 100644 index 000000000..2d6e4a4b7 --- /dev/null +++ b/cli/compiler.rs @@ -0,0 +1,151 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use crate::cli::Buf; +use crate::isolate_state::IsolateState; +use crate::msg; +use crate::permissions::{DenoPermissions, PermissionAccessor}; +use crate::resources; +use crate::resources::Resource; +use crate::resources::ResourceId; +use crate::startup_data; +use crate::workers; +use futures::Future; +use serde_json; +use std::str; +use std::sync::Mutex; + +lazy_static! { + static ref C_RID: Mutex<Option<ResourceId>> = Mutex::new(None); +} + +// This corresponds to JS ModuleMetaData. +// TODO Rename one or the other so they correspond. +#[derive(Debug)] +pub struct ModuleMetaData { + pub module_name: String, + pub filename: String, + pub media_type: msg::MediaType, + pub source_code: Vec<u8>, + pub maybe_output_code_filename: Option<String>, + pub maybe_output_code: Option<Vec<u8>>, + pub maybe_source_map_filename: Option<String>, + pub maybe_source_map: Option<Vec<u8>>, +} + +impl ModuleMetaData { + pub fn js_source(&self) -> String { + if self.media_type == msg::MediaType::Json { + return format!( + "export default {};", + str::from_utf8(&self.source_code).unwrap() + ); + } + match self.maybe_output_code { + None => str::from_utf8(&self.source_code).unwrap().to_string(), + Some(ref output_code) => str::from_utf8(output_code).unwrap().to_string(), + } + } +} + +fn lazy_start(parent_state: &IsolateState) -> Resource { + let mut cell = C_RID.lock().unwrap(); + let startup_data = startup_data::compiler_isolate_init(); + let permissions = DenoPermissions { + allow_read: PermissionAccessor::from(true), + allow_write: PermissionAccessor::from(true), + allow_net: PermissionAccessor::from(true), + ..Default::default() + }; + + let rid = cell.get_or_insert_with(|| { + let resource = workers::spawn( + Some(startup_data), + parent_state, + "compilerMain()".to_string(), + permissions, + ); + resource.rid + }); + Resource { rid: *rid } +} + +fn req(specifier: &str, referrer: &str) -> Buf { + json!({ + "specifier": specifier, + "referrer": referrer, + }).to_string() + .into_boxed_str() + .into_boxed_bytes() +} + +pub fn compile_sync( + parent_state: &IsolateState, + specifier: &str, + referrer: &str, + module_meta_data: &ModuleMetaData, +) -> ModuleMetaData { + let req_msg = req(specifier, referrer); + + let compiler = lazy_start(parent_state); + + let send_future = resources::worker_post_message(compiler.rid, req_msg); + send_future.wait().unwrap(); + + let recv_future = resources::worker_recv_message(compiler.rid); + let result = recv_future.wait().unwrap(); + assert!(result.is_some()); + let res_msg = result.unwrap(); + + let res_json = std::str::from_utf8(&res_msg).unwrap(); + match serde_json::from_str::<serde_json::Value>(res_json) { + Ok(serde_json::Value::Object(map)) => ModuleMetaData { + module_name: module_meta_data.module_name.clone(), + filename: module_meta_data.filename.clone(), + media_type: module_meta_data.media_type, + source_code: module_meta_data.source_code.clone(), + maybe_output_code: match map["outputCode"].as_str() { + Some(str) => Some(str.as_bytes().to_owned()), + _ => None, + }, + maybe_output_code_filename: None, + maybe_source_map: match map["sourceMap"].as_str() { + Some(str) => Some(str.as_bytes().to_owned()), + _ => None, + }, + maybe_source_map_filename: None, + }, + _ => panic!("error decoding compiler response"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_compile_sync() { + let cwd = std::env::current_dir().unwrap(); + let cwd_string = cwd.to_str().unwrap().to_owned(); + + let specifier = "./tests/002_hello.ts"; + let referrer = cwd_string + "/"; + + let mut out = ModuleMetaData { + module_name: "xxx".to_owned(), + filename: "/tests/002_hello.ts".to_owned(), + media_type: msg::MediaType::TypeScript, + source_code: "console.log(\"Hello World\");".as_bytes().to_owned(), + maybe_output_code_filename: None, + maybe_output_code: None, + maybe_source_map_filename: None, + maybe_source_map: None, + }; + + out = compile_sync(&IsolateState::mock(), specifier, &referrer, &mut out); + assert!( + out + .maybe_output_code + .unwrap() + .starts_with("console.log(\"Hello World\");".as_bytes()) + ); + } +} diff --git a/cli/deno_dir.rs b/cli/deno_dir.rs new file mode 100644 index 000000000..829af5aa2 --- /dev/null +++ b/cli/deno_dir.rs @@ -0,0 +1,1369 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use crate::compiler::ModuleMetaData; +use crate::errors; +use crate::errors::DenoError; +use crate::errors::DenoResult; +use crate::errors::ErrorKind; +use crate::fs as deno_fs; +use crate::http_util; +use crate::js_errors::SourceMapGetter; +use crate::msg; +use crate::version; + +use dirs; +use ring; +use std; +use std::fmt::Write; +use std::fs; +use std::path::Path; +use std::path::PathBuf; +use std::result::Result; +use std::str; +use url; +use url::Url; + +/// Gets corresponding MediaType given extension +fn extmap(ext: &str) -> msg::MediaType { + match ext { + "ts" => msg::MediaType::TypeScript, + "js" => msg::MediaType::JavaScript, + "json" => msg::MediaType::Json, + _ => msg::MediaType::Unknown, + } +} + +pub struct DenoDir { + // Example: /Users/rld/.deno/ + pub root: PathBuf, + // In the Go code this was called SrcDir. + // This is where we cache http resources. Example: + // /Users/rld/.deno/deps/github.com/ry/blah.js + pub gen: PathBuf, + // In the Go code this was called CacheDir. + // This is where we cache compilation outputs. Example: + // /Users/rld/.deno/gen/f39a473452321cacd7c346a870efb0e3e1264b43.js + pub deps: PathBuf, + // This splits to http and https deps + pub deps_http: PathBuf, + pub deps_https: PathBuf, + // If remote resources should be reloaded. + reload: bool, + // recompile the typescript files. + // if true, not load cache files + // else, load cache files + recompile: bool, +} + +impl DenoDir { + // Must be called before using any function from this module. + // https://github.com/denoland/deno/blob/golang/deno_dir.go#L99-L111 + pub fn new( + reload: bool, + recompile: bool, + custom_root: Option<PathBuf>, + ) -> std::io::Result<Self> { + // Only setup once. + let home_dir = dirs::home_dir().expect("Could not get home directory."); + let fallback = home_dir.join(".deno"); + // We use the OS cache dir because all files deno writes are cache files + // Once that changes we need to start using different roots if DENO_DIR + // is not set, and keep a single one if it is. + let default = dirs::cache_dir() + .map(|d| d.join("deno")) + .unwrap_or(fallback); + + let root: PathBuf = custom_root.unwrap_or(default); + let gen = root.as_path().join("gen"); + let deps = root.as_path().join("deps"); + let deps_http = deps.join("http"); + let deps_https = deps.join("https"); + + let deno_dir = Self { + root, + gen, + deps, + deps_http, + deps_https, + reload, + recompile, + }; + + // TODO Lazily create these directories. + deno_fs::mkdir(deno_dir.gen.as_ref(), 0o755, true)?; + deno_fs::mkdir(deno_dir.deps.as_ref(), 0o755, true)?; + deno_fs::mkdir(deno_dir.deps_http.as_ref(), 0o755, true)?; + deno_fs::mkdir(deno_dir.deps_https.as_ref(), 0o755, true)?; + + debug!("root {}", deno_dir.root.display()); + debug!("gen {}", deno_dir.gen.display()); + debug!("deps {}", deno_dir.deps.display()); + debug!("deps_http {}", deno_dir.deps_http.display()); + debug!("deps_https {}", deno_dir.deps_https.display()); + + Ok(deno_dir) + } + + // https://github.com/denoland/deno/blob/golang/deno_dir.go#L32-L35 + pub fn cache_path( + self: &Self, + filename: &str, + source_code: &[u8], + ) -> (PathBuf, PathBuf) { + let cache_key = source_code_hash(filename, source_code, version::DENO); + ( + self.gen.join(cache_key.to_string() + ".js"), + self.gen.join(cache_key.to_string() + ".js.map"), + ) + } + + fn load_cache( + self: &Self, + filename: &str, + source_code: &[u8], + ) -> Result<(Vec<u8>, Vec<u8>), std::io::Error> { + let (output_code, source_map) = self.cache_path(filename, source_code); + debug!( + "load_cache code: {} map: {}", + output_code.display(), + source_map.display() + ); + let read_output_code = fs::read(&output_code)?; + let read_source_map = fs::read(&source_map)?; + Ok((read_output_code, read_source_map)) + } + + pub fn code_cache( + self: &Self, + module_meta_data: &ModuleMetaData, + ) -> std::io::Result<()> { + let (cache_path, source_map_path) = self + .cache_path(&module_meta_data.filename, &module_meta_data.source_code); + // TODO(ry) This is a race condition w.r.t to exists() -- probably should + // create the file in exclusive mode. A worry is what might happen is there + // are two processes and one reads the cache file while the other is in the + // midst of writing it. + if cache_path.exists() && source_map_path.exists() { + Ok(()) + } else { + match &module_meta_data.maybe_output_code { + Some(output_code) => fs::write(cache_path, output_code), + _ => Ok(()), + }?; + match &module_meta_data.maybe_source_map { + Some(source_map) => fs::write(source_map_path, source_map), + _ => Ok(()), + }?; + Ok(()) + } + } + + // Prototype https://github.com/denoland/deno/blob/golang/deno_dir.go#L37-L73 + /// Fetch remote source code. + fn fetch_remote_source( + self: &Self, + module_name: &str, + filename: &str, + ) -> DenoResult<Option<ModuleMetaData>> { + let p = Path::new(&filename); + // We write a special ".mime" file into the `.deno/deps` directory along side the + // cached file, containing just the media type. + let media_type_filename = [&filename, ".mime"].concat(); + let mt = Path::new(&media_type_filename); + eprint!("Downloading {}...", &module_name); // no newline + let maybe_source = http_util::fetch_sync_string(&module_name); + if let Ok((source, content_type)) = maybe_source { + eprintln!(""); // next line + match p.parent() { + Some(ref parent) => fs::create_dir_all(parent), + None => Ok(()), + }?; + deno_fs::write_file(&p, &source, 0o666)?; + // Remove possibly existing stale .mime file + // may not exist. DON'T unwrap + let _ = std::fs::remove_file(&media_type_filename); + // Create .mime file only when content type different from extension + let resolved_content_type = map_content_type(&p, Some(&content_type)); + let ext = p + .extension() + .map(|x| x.to_str().unwrap_or("")) + .unwrap_or(""); + let media_type = extmap(&ext); + if media_type == msg::MediaType::Unknown + || media_type != resolved_content_type + { + deno_fs::write_file(&mt, content_type.as_bytes(), 0o666)? + } + return Ok(Some(ModuleMetaData { + module_name: module_name.to_string(), + filename: filename.to_string(), + media_type: map_content_type(&p, Some(&content_type)), + source_code: source.as_bytes().to_owned(), + maybe_output_code_filename: None, + maybe_output_code: None, + maybe_source_map_filename: None, + maybe_source_map: None, + })); + } else { + eprintln!(" NOT FOUND"); + } + Ok(None) + } + + /// Fetch local or cached source code. + fn fetch_local_source( + self: &Self, + module_name: &str, + filename: &str, + ) -> DenoResult<Option<ModuleMetaData>> { + let p = Path::new(&filename); + let media_type_filename = [&filename, ".mime"].concat(); + let mt = Path::new(&media_type_filename); + let source_code = match fs::read(p) { + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + return Ok(None); + } else { + return Err(e.into()); + } + } + Ok(c) => c, + }; + // .mime file might not exists + // this is okay for local source: maybe_content_type_str will be None + let maybe_content_type_string = fs::read_to_string(&mt).ok(); + // Option<String> -> Option<&str> + let maybe_content_type_str = + maybe_content_type_string.as_ref().map(String::as_str); + Ok(Some(ModuleMetaData { + module_name: module_name.to_string(), + filename: filename.to_string(), + media_type: map_content_type(&p, maybe_content_type_str), + source_code, + maybe_output_code_filename: None, + maybe_output_code: None, + maybe_source_map_filename: None, + maybe_source_map: None, + })) + } + + // Prototype: https://github.com/denoland/deno/blob/golang/os.go#L122-L138 + fn get_source_code( + self: &Self, + module_name: &str, + filename: &str, + ) -> DenoResult<ModuleMetaData> { + let is_module_remote = is_remote(module_name); + // We try fetch local. Two cases: + // 1. This is a remote module, but no reload provided + // 2. This is a local module + if !is_module_remote || !self.reload { + debug!( + "fetch local or reload {} is_module_remote {}", + module_name, is_module_remote + ); + match self.fetch_local_source(&module_name, &filename)? { + Some(output) => { + debug!("found local source "); + return Ok(output); + } + None => { + debug!("fetch_local_source returned None"); + } + } + } + + // If not remote file, stop here! + if !is_module_remote { + debug!("not remote file stop here"); + return Err(DenoError::from(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("cannot find local file '{}'", filename), + ))); + } + + debug!("is remote but didn't find module"); + + // not cached/local, try remote + let maybe_remote_source = + self.fetch_remote_source(&module_name, &filename)?; + if let Some(output) = maybe_remote_source { + return Ok(output); + } + Err(DenoError::from(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("cannot find remote file '{}'", filename), + ))) + } + + pub fn fetch_module_meta_data( + self: &Self, + specifier: &str, + referrer: &str, + ) -> Result<ModuleMetaData, errors::DenoError> { + debug!( + "fetch_module_meta_data. specifier {} referrer {}", + specifier, referrer + ); + + let (module_name, filename) = self.resolve_module(specifier, referrer)?; + + let result = self.get_source_code(module_name.as_str(), filename.as_str()); + let mut out = match result { + Ok(out) => out, + Err(err) => { + if err.kind() == ErrorKind::NotFound { + // For NotFound, change the message to something better. + return Err(errors::new( + ErrorKind::NotFound, + format!( + "Cannot resolve module \"{}\" from \"{}\"", + specifier, referrer + ), + )); + } else { + return Err(err); + } + } + }; + + if out.source_code.starts_with("#!".as_bytes()) { + out.source_code = filter_shebang(out.source_code); + } + + if out.media_type != msg::MediaType::TypeScript { + return Ok(out); + } + + let (output_code_filename, output_source_map_filename) = + self.cache_path(&out.filename, &out.source_code); + let mut maybe_output_code = None; + let mut maybe_source_map = None; + + if !self.recompile { + let result = self.load_cache(out.filename.as_str(), &out.source_code); + match result { + Err(err) => { + if err.kind() == std::io::ErrorKind::NotFound { + return Ok(out); + } else { + return Err(err.into()); + } + } + Ok((output_code, source_map)) => { + maybe_output_code = Some(output_code); + maybe_source_map = Some(source_map); + } + } + } + + Ok(ModuleMetaData { + module_name: out.module_name, + filename: out.filename, + media_type: out.media_type, + source_code: out.source_code, + maybe_output_code_filename: output_code_filename + .to_str() + .map(String::from), + maybe_output_code, + maybe_source_map_filename: output_source_map_filename + .to_str() + .map(String::from), + maybe_source_map, + }) + } + + // Prototype: https://github.com/denoland/deno/blob/golang/os.go#L56-L68 + fn src_file_to_url(self: &Self, filename: &str) -> String { + let filename_path = Path::new(filename); + if filename_path.starts_with(&self.deps) { + let (rest, prefix) = if filename_path.starts_with(&self.deps_https) { + let rest = filename_path.strip_prefix(&self.deps_https).unwrap(); + let prefix = "https://".to_string(); + (rest, prefix) + } else if filename_path.starts_with(&self.deps_http) { + let rest = filename_path.strip_prefix(&self.deps_http).unwrap(); + let prefix = "http://".to_string(); + (rest, prefix) + } else { + // TODO(kevinkassimo): change this to support other protocols than http + unimplemented!() + }; + // Windows doesn't support ":" in filenames, so we represent port using a + // special string. + // TODO(ry) This current implementation will break on a URL that has + // the default port but contains "_PORT" in the path. + let rest = rest.to_str().unwrap().replacen("_PORT", ":", 1); + prefix + &rest + } else { + String::from(filename) + } + } + + /// Returns (module name, local filename) + pub fn resolve_module_url( + self: &Self, + specifier: &str, + referrer: &str, + ) -> Result<Url, url::ParseError> { + let specifier = self.src_file_to_url(specifier); + let mut referrer = self.src_file_to_url(referrer); + + debug!( + "resolve_module specifier {} referrer {}", + specifier, referrer + ); + + if referrer.starts_with('.') { + let cwd = std::env::current_dir().unwrap(); + let referrer_path = cwd.join(referrer); + referrer = referrer_path.to_str().unwrap().to_string() + "/"; + } + + let j = if is_remote(&specifier) || Path::new(&specifier).is_absolute() { + parse_local_or_remote(&specifier)? + } else if referrer.ends_with('/') { + let r = Url::from_directory_path(&referrer); + // TODO(ry) Properly handle error. + if r.is_err() { + error!("Url::from_directory_path error {}", referrer); + } + let base = r.unwrap(); + base.join(specifier.as_ref())? + } else { + let base = parse_local_or_remote(&referrer)?; + base.join(specifier.as_ref())? + }; + Ok(j) + } + + /// Returns (module name, local filename) + pub fn resolve_module( + self: &Self, + specifier: &str, + referrer: &str, + ) -> Result<(String, String), url::ParseError> { + let j = self.resolve_module_url(specifier, referrer)?; + + let module_name = j.to_string(); + let filename; + match j.scheme() { + "file" => { + filename = deno_fs::normalize_path(j.to_file_path().unwrap().as_ref()); + } + "https" => { + filename = deno_fs::normalize_path( + get_cache_filename(self.deps_https.as_path(), &j).as_ref(), + ) + } + "http" => { + filename = deno_fs::normalize_path( + get_cache_filename(self.deps_http.as_path(), &j).as_ref(), + ) + } + // TODO(kevinkassimo): change this to support other protocols than http + _ => unimplemented!(), + } + + debug!("module_name: {}, filename: {}", module_name, filename); + Ok((module_name, filename)) + } +} + +impl SourceMapGetter for DenoDir { + fn get_source_map(&self, script_name: &str) -> Option<Vec<u8>> { + match self.fetch_module_meta_data(script_name, ".") { + Err(_e) => None, + Ok(out) => match out.maybe_source_map { + None => None, + Some(source_map) => Some(source_map), + }, + } + } +} + +fn get_cache_filename(basedir: &Path, url: &Url) -> PathBuf { + let host = url.host_str().unwrap(); + let host_port = match url.port() { + // Windows doesn't support ":" in filenames, so we represent port using a + // special string. + Some(port) => format!("{}_PORT{}", host, port), + None => host.to_string(), + }; + + let mut out = basedir.to_path_buf(); + out.push(host_port); + for path_seg in url.path_segments().unwrap() { + out.push(path_seg); + } + out +} + +fn source_code_hash( + filename: &str, + source_code: &[u8], + version: &str, +) -> String { + let mut ctx = ring::digest::Context::new(&ring::digest::SHA1); + ctx.update(version.as_bytes()); + ctx.update(filename.as_bytes()); + ctx.update(source_code); + let digest = ctx.finish(); + let mut out = String::new(); + // TODO There must be a better way to do this... + for byte in digest.as_ref() { + write!(&mut out, "{:02x}", byte).unwrap(); + } + out +} + +fn is_remote(module_name: &str) -> bool { + module_name.starts_with("http://") || module_name.starts_with("https://") +} + +fn parse_local_or_remote(p: &str) -> Result<url::Url, url::ParseError> { + if is_remote(p) || p.starts_with("file:") { + Url::parse(p) + } else { + Url::from_file_path(p).map_err(|_err| url::ParseError::IdnaError) + } +} + +fn map_file_extension(path: &Path) -> msg::MediaType { + match path.extension() { + None => msg::MediaType::Unknown, + Some(os_str) => match os_str.to_str() { + Some("ts") => msg::MediaType::TypeScript, + Some("js") => msg::MediaType::JavaScript, + Some("json") => msg::MediaType::Json, + _ => msg::MediaType::Unknown, + }, + } +} + +// convert a ContentType string into a enumerated MediaType +fn map_content_type(path: &Path, content_type: Option<&str>) -> msg::MediaType { + match content_type { + Some(content_type) => { + // sometimes there is additional data after the media type in + // Content-Type so we have to do a bit of manipulation so we are only + // dealing with the actual media type + let ct_vector: Vec<&str> = content_type.split(';').collect(); + let ct: &str = ct_vector.first().unwrap(); + match ct.to_lowercase().as_ref() { + "application/typescript" + | "text/typescript" + | "video/vnd.dlna.mpeg-tts" + | "video/mp2t" + | "application/x-typescript" => msg::MediaType::TypeScript, + "application/javascript" + | "text/javascript" + | "application/ecmascript" + | "text/ecmascript" + | "application/x-javascript" => msg::MediaType::JavaScript, + "application/json" | "text/json" => msg::MediaType::Json, + "text/plain" => map_file_extension(path), + _ => { + debug!("unknown content type: {}", content_type); + msg::MediaType::Unknown + } + } + } + None => map_file_extension(path), + } +} + +fn filter_shebang(bytes: Vec<u8>) -> Vec<u8> { + let string = str::from_utf8(&bytes).unwrap(); + if let Some(i) = string.find('\n') { + let (_, rest) = string.split_at(i); + rest.as_bytes().to_owned() + } else { + Vec::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tokio_util; + use tempfile::TempDir; + + fn test_setup(reload: bool, recompile: bool) -> (TempDir, DenoDir) { + let temp_dir = TempDir::new().expect("tempdir fail"); + let deno_dir = + DenoDir::new(reload, recompile, Some(temp_dir.path().to_path_buf())) + .expect("setup fail"); + (temp_dir, deno_dir) + } + + // The `add_root` macro prepends "C:" to a string if on windows; on posix + // systems it returns the input string untouched. This is necessary because + // `Url::from_file_path()` fails if the input path isn't an absolute path. + macro_rules! add_root { + ($path:expr) => { + if cfg!(target_os = "windows") { + concat!("C:", $path) + } else { + $path + } + }; + } + + macro_rules! file_url { + ($path:expr) => { + if cfg!(target_os = "windows") { + concat!("file:///C:", $path) + } else { + concat!("file://", $path) + } + }; + } + + #[test] + fn test_get_cache_filename() { + let url = Url::parse("http://example.com:1234/path/to/file.ts").unwrap(); + let basedir = Path::new("/cache/dir/"); + let cache_file = get_cache_filename(&basedir, &url); + assert_eq!( + cache_file, + Path::new("/cache/dir/example.com_PORT1234/path/to/file.ts") + ); + } + + #[test] + fn test_cache_path() { + let (temp_dir, deno_dir) = test_setup(false, false); + let filename = "hello.js"; + let source_code = "1+2".as_bytes(); + let hash = source_code_hash(filename, source_code, version::DENO); + assert_eq!( + ( + temp_dir.path().join(format!("gen/{}.js", hash)), + temp_dir.path().join(format!("gen/{}.js.map", hash)) + ), + deno_dir.cache_path(filename, source_code) + ); + } + + #[test] + fn test_code_cache() { + let (_temp_dir, deno_dir) = test_setup(false, false); + + let filename = "hello.js"; + let source_code = "1+2".as_bytes(); + let output_code = "1+2 // output code".as_bytes(); + let source_map = "{}".as_bytes(); + let hash = source_code_hash(filename, source_code, version::DENO); + let (cache_path, source_map_path) = + deno_dir.cache_path(filename, source_code); + assert!(cache_path.ends_with(format!("gen/{}.js", hash))); + assert!(source_map_path.ends_with(format!("gen/{}.js.map", hash))); + + let out = ModuleMetaData { + filename: filename.to_owned(), + source_code: source_code.to_owned(), + module_name: "hello.js".to_owned(), + media_type: msg::MediaType::TypeScript, + maybe_output_code: Some(output_code.to_owned()), + maybe_output_code_filename: None, + maybe_source_map: Some(source_map.to_owned()), + maybe_source_map_filename: None, + }; + + let r = deno_dir.code_cache(&out); + r.expect("code_cache error"); + assert!(cache_path.exists()); + assert_eq!(output_code.to_owned(), fs::read(&cache_path).unwrap()); + } + + #[test] + fn test_source_code_hash() { + assert_eq!( + "7e44de2ed9e0065da09d835b76b8d70be503d276", + source_code_hash("hello.ts", "1+2".as_bytes(), "0.2.11") + ); + // Different source_code should result in different hash. + assert_eq!( + "57033366cf9db1ef93deca258cdbcd9ef5f4bde1", + source_code_hash("hello.ts", "1".as_bytes(), "0.2.11") + ); + // Different filename should result in different hash. + assert_eq!( + "19657f90b5b0540f87679e2fb362e7bd62b644b0", + source_code_hash("hi.ts", "1+2".as_bytes(), "0.2.11") + ); + // Different version should result in different hash. + assert_eq!( + "e2b4b7162975a02bf2770f16836eb21d5bcb8be1", + source_code_hash("hi.ts", "1+2".as_bytes(), "0.2.0") + ); + } + + #[test] + fn test_get_source_code_1() { + let (temp_dir, deno_dir) = test_setup(false, false); + // http_util::fetch_sync_string requires tokio + tokio_util::init(|| { + let module_name = "http://localhost:4545/tests/subdir/mod2.ts"; + let filename = deno_fs::normalize_path( + deno_dir + .deps_http + .join("localhost_PORT4545/tests/subdir/mod2.ts") + .as_ref(), + ); + let mime_file_name = format!("{}.mime", &filename); + + let result = deno_dir.get_source_code(module_name, &filename); + println!("module_name {} filename {}", module_name, filename); + assert!(result.is_ok()); + let r = result.unwrap(); + assert_eq!( + r.source_code, + "export { printHello } from \"./print_hello.ts\";\n".as_bytes() + ); + assert_eq!(&(r.media_type), &msg::MediaType::TypeScript); + // Should not create .mime file due to matching ext + assert!(fs::read_to_string(&mime_file_name).is_err()); + + // Modify .mime + let _ = fs::write(&mime_file_name, "text/javascript"); + let result2 = deno_dir.get_source_code(module_name, &filename); + assert!(result2.is_ok()); + let r2 = result2.unwrap(); + assert_eq!( + r2.source_code, + "export { printHello } from \"./print_hello.ts\";\n".as_bytes() + ); + // If get_source_code does not call remote, this should be JavaScript + // as we modified before! (we do not overwrite .mime due to no http fetch) + assert_eq!(&(r2.media_type), &msg::MediaType::JavaScript); + assert_eq!( + fs::read_to_string(&mime_file_name).unwrap(), + "text/javascript" + ); + + // Force self.reload + let deno_dir = + DenoDir::new(true, false, Some(temp_dir.path().to_path_buf())) + .expect("setup fail"); + let result3 = deno_dir.get_source_code(module_name, &filename); + assert!(result3.is_ok()); + let r3 = result3.unwrap(); + let expected3 = + "export { printHello } from \"./print_hello.ts\";\n".as_bytes(); + assert_eq!(r3.source_code, expected3); + // Now the old .mime file should have gone! Resolved back to TypeScript + assert_eq!(&(r3.media_type), &msg::MediaType::TypeScript); + assert!(fs::read_to_string(&mime_file_name).is_err()); + }); + } + + #[test] + fn test_get_source_code_2() { + let (temp_dir, deno_dir) = test_setup(false, false); + // http_util::fetch_sync_string requires tokio + tokio_util::init(|| { + let module_name = "http://localhost:4545/tests/subdir/mismatch_ext.ts"; + let filename = deno_fs::normalize_path( + deno_dir + .deps_http + .join("localhost_PORT4545/tests/subdir/mismatch_ext.ts") + .as_ref(), + ); + let mime_file_name = format!("{}.mime", &filename); + + let result = deno_dir.get_source_code(module_name, &filename); + println!("module_name {} filename {}", module_name, filename); + assert!(result.is_ok()); + let r = result.unwrap(); + let expected = "export const loaded = true;\n".as_bytes(); + assert_eq!(r.source_code, expected); + // Mismatch ext with content type, create .mime + assert_eq!(&(r.media_type), &msg::MediaType::JavaScript); + assert_eq!( + fs::read_to_string(&mime_file_name).unwrap(), + "text/javascript" + ); + + // Modify .mime + let _ = fs::write(&mime_file_name, "text/typescript"); + let result2 = deno_dir.get_source_code(module_name, &filename); + assert!(result2.is_ok()); + let r2 = result2.unwrap(); + let expected2 = "export const loaded = true;\n".as_bytes(); + assert_eq!(r2.source_code, expected2); + // If get_source_code does not call remote, this should be TypeScript + // as we modified before! (we do not overwrite .mime due to no http fetch) + assert_eq!(&(r2.media_type), &msg::MediaType::TypeScript); + assert_eq!( + fs::read_to_string(&mime_file_name).unwrap(), + "text/typescript" + ); + + // Force self.reload + let deno_dir = + DenoDir::new(true, false, Some(temp_dir.path().to_path_buf())) + .expect("setup fail"); + let result3 = deno_dir.get_source_code(module_name, &filename); + assert!(result3.is_ok()); + let r3 = result3.unwrap(); + let expected3 = "export const loaded = true;\n".as_bytes(); + assert_eq!(r3.source_code, expected3); + // Now the old .mime file should be overwritten back to JavaScript! + // (due to http fetch) + assert_eq!(&(r3.media_type), &msg::MediaType::JavaScript); + assert_eq!( + fs::read_to_string(&mime_file_name).unwrap(), + "text/javascript" + ); + }); + } + + #[test] + fn test_fetch_source_1() { + use crate::tokio_util; + // http_util::fetch_sync_string requires tokio + tokio_util::init(|| { + let (_temp_dir, deno_dir) = test_setup(false, false); + let module_name = + "http://localhost:4545/tests/subdir/mt_video_mp2t.t3.ts"; + let filename = deno_fs::normalize_path( + deno_dir + .deps_http + .join("localhost_PORT4545/tests/subdir/mt_video_mp2t.t3.ts") + .as_ref(), + ); + let mime_file_name = format!("{}.mime", &filename); + + let result = deno_dir.fetch_remote_source(module_name, &filename); + assert!(result.is_ok()); + let r = result.unwrap().unwrap(); + assert_eq!(r.source_code, "export const loaded = true;\n".as_bytes()); + assert_eq!(&(r.media_type), &msg::MediaType::TypeScript); + // matching ext, no .mime file created + assert!(fs::read_to_string(&mime_file_name).is_err()); + + // Modify .mime, make sure read from local + let _ = fs::write(&mime_file_name, "text/javascript"); + let result2 = deno_dir.fetch_local_source(module_name, &filename); + assert!(result2.is_ok()); + let r2 = result2.unwrap().unwrap(); + assert_eq!(r2.source_code, "export const loaded = true;\n".as_bytes()); + // Not MediaType::TypeScript due to .mime modification + assert_eq!(&(r2.media_type), &msg::MediaType::JavaScript); + }); + } + + #[test] + fn test_fetch_source_2() { + use crate::tokio_util; + // http_util::fetch_sync_string requires tokio + tokio_util::init(|| { + let (_temp_dir, deno_dir) = test_setup(false, false); + let module_name = "http://localhost:4545/tests/subdir/no_ext"; + let filename = deno_fs::normalize_path( + deno_dir + .deps_http + .join("localhost_PORT4545/tests/subdir/no_ext") + .as_ref(), + ); + let mime_file_name = format!("{}.mime", &filename); + let result = deno_dir.fetch_remote_source(module_name, &filename); + assert!(result.is_ok()); + let r = result.unwrap().unwrap(); + assert_eq!(r.source_code, "export const loaded = true;\n".as_bytes()); + assert_eq!(&(r.media_type), &msg::MediaType::TypeScript); + // no ext, should create .mime file + assert_eq!( + fs::read_to_string(&mime_file_name).unwrap(), + "text/typescript" + ); + + let module_name_2 = "http://localhost:4545/tests/subdir/mismatch_ext.ts"; + let filename_2 = deno_fs::normalize_path( + deno_dir + .deps_http + .join("localhost_PORT4545/tests/subdir/mismatch_ext.ts") + .as_ref(), + ); + let mime_file_name_2 = format!("{}.mime", &filename_2); + let result_2 = deno_dir.fetch_remote_source(module_name_2, &filename_2); + assert!(result_2.is_ok()); + let r2 = result_2.unwrap().unwrap(); + assert_eq!(r2.source_code, "export const loaded = true;\n".as_bytes()); + assert_eq!(&(r2.media_type), &msg::MediaType::JavaScript); + // mismatch ext, should create .mime file + assert_eq!( + fs::read_to_string(&mime_file_name_2).unwrap(), + "text/javascript" + ); + + // test unknown extension + let module_name_3 = "http://localhost:4545/tests/subdir/unknown_ext.deno"; + let filename_3 = deno_fs::normalize_path( + deno_dir + .deps_http + .join("localhost_PORT4545/tests/subdir/unknown_ext.deno") + .as_ref(), + ); + let mime_file_name_3 = format!("{}.mime", &filename_3); + let result_3 = deno_dir.fetch_remote_source(module_name_3, &filename_3); + assert!(result_3.is_ok()); + let r3 = result_3.unwrap().unwrap(); + assert_eq!(r3.source_code, "export const loaded = true;\n".as_bytes()); + assert_eq!(&(r3.media_type), &msg::MediaType::TypeScript); + // unknown ext, should create .mime file + assert_eq!( + fs::read_to_string(&mime_file_name_3).unwrap(), + "text/typescript" + ); + }); + } + + #[test] + fn test_fetch_source_3() { + // only local, no http_util::fetch_sync_string called + let (_temp_dir, deno_dir) = test_setup(false, false); + let cwd = std::env::current_dir().unwrap(); + let cwd_string = cwd.to_str().unwrap(); + let module_name = "http://example.com/mt_text_typescript.t1.ts"; // not used + let filename = + format!("{}/tests/subdir/mt_text_typescript.t1.ts", &cwd_string); + + let result = deno_dir.fetch_local_source(module_name, &filename); + assert!(result.is_ok()); + let r = result.unwrap().unwrap(); + assert_eq!(r.source_code, "export const loaded = true;\n".as_bytes()); + assert_eq!(&(r.media_type), &msg::MediaType::TypeScript); + } + + #[test] + fn test_fetch_module_meta_data() { + let (_temp_dir, deno_dir) = test_setup(false, false); + + let cwd = std::env::current_dir().unwrap(); + let cwd_string = String::from(cwd.to_str().unwrap()) + "/"; + + // Test failure case. + let specifier = "hello.ts"; + let referrer = add_root!("/baddir/badfile.ts"); + let r = deno_dir.fetch_module_meta_data(specifier, referrer); + assert!(r.is_err()); + + // Assuming cwd is the deno repo root. + let specifier = "./js/main.ts"; + let referrer = cwd_string.as_str(); + let r = deno_dir.fetch_module_meta_data(specifier, referrer); + assert!(r.is_ok()); + //let fetch_module_meta_data_output = r.unwrap(); + //println!("fetch_module_meta_data_output {:?}", fetch_module_meta_data_output); + } + + #[test] + fn test_fetch_module_meta_data_1() { + /*recompile ts file*/ + let (_temp_dir, deno_dir) = test_setup(false, true); + + let cwd = std::env::current_dir().unwrap(); + let cwd_string = String::from(cwd.to_str().unwrap()) + "/"; + + // Test failure case. + let specifier = "hello.ts"; + let referrer = add_root!("/baddir/badfile.ts"); + let r = deno_dir.fetch_module_meta_data(specifier, referrer); + assert!(r.is_err()); + + // Assuming cwd is the deno repo root. + let specifier = "./js/main.ts"; + let referrer = cwd_string.as_str(); + let r = deno_dir.fetch_module_meta_data(specifier, referrer); + assert!(r.is_ok()); + } + + #[test] + fn test_src_file_to_url_1() { + let (_temp_dir, deno_dir) = test_setup(false, false); + assert_eq!("hello", deno_dir.src_file_to_url("hello")); + assert_eq!("/hello", deno_dir.src_file_to_url("/hello")); + let x = deno_dir.deps_http.join("hello/world.txt"); + assert_eq!( + "http://hello/world.txt", + deno_dir.src_file_to_url(x.to_str().unwrap()) + ); + } + + #[test] + fn test_src_file_to_url_2() { + let (_temp_dir, deno_dir) = test_setup(false, false); + assert_eq!("hello", deno_dir.src_file_to_url("hello")); + assert_eq!("/hello", deno_dir.src_file_to_url("/hello")); + let x = deno_dir.deps_https.join("hello/world.txt"); + assert_eq!( + "https://hello/world.txt", + deno_dir.src_file_to_url(x.to_str().unwrap()) + ); + } + + #[test] + fn test_src_file_to_url_3() { + let (_temp_dir, deno_dir) = test_setup(false, false); + let x = deno_dir.deps_http.join("localhost_PORT4545/world.txt"); + assert_eq!( + "http://localhost:4545/world.txt", + deno_dir.src_file_to_url(x.to_str().unwrap()) + ); + } + + #[test] + fn test_src_file_to_url_4() { + let (_temp_dir, deno_dir) = test_setup(false, false); + let x = deno_dir.deps_https.join("localhost_PORT4545/world.txt"); + assert_eq!( + "https://localhost:4545/world.txt", + deno_dir.src_file_to_url(x.to_str().unwrap()) + ); + } + + // https://github.com/denoland/deno/blob/golang/os_test.go#L16-L87 + #[test] + fn test_resolve_module_1() { + let (_temp_dir, deno_dir) = test_setup(false, false); + + let test_cases = [ + ( + "./subdir/print_hello.ts", + add_root!("/Users/rld/go/src/github.com/denoland/deno/testdata/006_url_imports.ts"), + file_url!("/Users/rld/go/src/github.com/denoland/deno/testdata/subdir/print_hello.ts"), + add_root!("/Users/rld/go/src/github.com/denoland/deno/testdata/subdir/print_hello.ts"), + ), + ( + "testdata/001_hello.js", + add_root!("/Users/rld/go/src/github.com/denoland/deno/"), + file_url!("/Users/rld/go/src/github.com/denoland/deno/testdata/001_hello.js"), + add_root!("/Users/rld/go/src/github.com/denoland/deno/testdata/001_hello.js"), + ), + ( + add_root!("/Users/rld/src/deno/hello.js"), + ".", + file_url!("/Users/rld/src/deno/hello.js"), + add_root!("/Users/rld/src/deno/hello.js"), + ), + ( + add_root!("/this/module/got/imported.js"), + add_root!("/that/module/did/it.js"), + file_url!("/this/module/got/imported.js"), + add_root!("/this/module/got/imported.js"), + ), + ]; + for &test in test_cases.iter() { + let specifier = String::from(test.0); + let referrer = String::from(test.1); + let (module_name, filename) = + deno_dir.resolve_module(&specifier, &referrer).unwrap(); + assert_eq!(module_name, test.2); + assert_eq!(filename, test.3); + } + } + + #[test] + fn test_resolve_module_2() { + let (_temp_dir, deno_dir) = test_setup(false, false); + + let specifier = "http://localhost:4545/testdata/subdir/print_hello.ts"; + let referrer = add_root!("/deno/testdata/006_url_imports.ts"); + + let expected_module_name = + "http://localhost:4545/testdata/subdir/print_hello.ts"; + let expected_filename = deno_fs::normalize_path( + deno_dir + .deps_http + .join("localhost_PORT4545/testdata/subdir/print_hello.ts") + .as_ref(), + ); + + let (module_name, filename) = + deno_dir.resolve_module(specifier, referrer).unwrap(); + assert_eq!(module_name, expected_module_name); + assert_eq!(filename, expected_filename); + } + + #[test] + fn test_resolve_module_3() { + let (_temp_dir, deno_dir) = test_setup(false, false); + + let specifier_ = + deno_dir.deps_http.join("unpkg.com/liltest@0.0.5/index.ts"); + let specifier = specifier_.to_str().unwrap(); + let referrer = "."; + + let expected_module_name = "http://unpkg.com/liltest@0.0.5/index.ts"; + let expected_filename = deno_fs::normalize_path( + deno_dir + .deps_http + .join("unpkg.com/liltest@0.0.5/index.ts") + .as_ref(), + ); + + let (module_name, filename) = + deno_dir.resolve_module(specifier, referrer).unwrap(); + assert_eq!(module_name, expected_module_name); + assert_eq!(filename, expected_filename); + } + + #[test] + fn test_resolve_module_4() { + let (_temp_dir, deno_dir) = test_setup(false, false); + + let specifier = "./util"; + let referrer_ = deno_dir.deps_http.join("unpkg.com/liltest@0.0.5/index.ts"); + let referrer = referrer_.to_str().unwrap(); + + // http containing files -> load relative import with http + let expected_module_name = "http://unpkg.com/liltest@0.0.5/util"; + let expected_filename = deno_fs::normalize_path( + deno_dir + .deps_http + .join("unpkg.com/liltest@0.0.5/util") + .as_ref(), + ); + + let (module_name, filename) = + deno_dir.resolve_module(specifier, referrer).unwrap(); + assert_eq!(module_name, expected_module_name); + assert_eq!(filename, expected_filename); + } + + #[test] + fn test_resolve_module_5() { + let (_temp_dir, deno_dir) = test_setup(false, false); + + let specifier = "./util"; + let referrer_ = + deno_dir.deps_https.join("unpkg.com/liltest@0.0.5/index.ts"); + let referrer = referrer_.to_str().unwrap(); + + // https containing files -> load relative import with https + let expected_module_name = "https://unpkg.com/liltest@0.0.5/util"; + let expected_filename = deno_fs::normalize_path( + deno_dir + .deps_https + .join("unpkg.com/liltest@0.0.5/util") + .as_ref(), + ); + + let (module_name, filename) = + deno_dir.resolve_module(specifier, referrer).unwrap(); + assert_eq!(module_name, expected_module_name); + assert_eq!(filename, expected_filename); + } + + #[test] + fn test_resolve_module_6() { + let (_temp_dir, deno_dir) = test_setup(false, false); + + let specifier = "http://localhost:4545/tests/subdir/mod2.ts"; + let referrer = add_root!("/deno/tests/006_url_imports.ts"); + let expected_module_name = "http://localhost:4545/tests/subdir/mod2.ts"; + let expected_filename = deno_fs::normalize_path( + deno_dir + .deps_http + .join("localhost_PORT4545/tests/subdir/mod2.ts") + .as_ref(), + ); + + let (module_name, filename) = + deno_dir.resolve_module(specifier, referrer).unwrap(); + assert_eq!(module_name, expected_module_name); + assert_eq!(filename, expected_filename); + } + + #[test] + fn test_resolve_module_7() { + let (_temp_dir, deno_dir) = test_setup(false, false); + + let specifier = "http_test.ts"; + let referrer = add_root!("/Users/rld/src/deno_net/"); + let expected_module_name = + file_url!("/Users/rld/src/deno_net/http_test.ts"); + let expected_filename = add_root!("/Users/rld/src/deno_net/http_test.ts"); + + let (module_name, filename) = + deno_dir.resolve_module(specifier, referrer).unwrap(); + assert_eq!(module_name, expected_module_name); + assert_eq!(filename, expected_filename); + } + + #[test] + fn test_resolve_module_referrer_dot() { + let (_temp_dir, deno_dir) = test_setup(false, false); + + let specifier = "tests/001_hello.js"; + + let cwd = std::env::current_dir().unwrap(); + let expected_path = cwd.join(specifier); + let expected_module_name = + Url::from_file_path(&expected_path).unwrap().to_string(); + let expected_filename = deno_fs::normalize_path(&expected_path); + + let (module_name, filename) = + deno_dir.resolve_module(specifier, ".").unwrap(); + assert_eq!(module_name, expected_module_name); + assert_eq!(filename, expected_filename); + + let (module_name, filename) = + deno_dir.resolve_module(specifier, "./").unwrap(); + assert_eq!(module_name, expected_module_name); + assert_eq!(filename, expected_filename); + } + + #[test] + fn test_resolve_module_referrer_dotdot() { + let (_temp_dir, deno_dir) = test_setup(false, false); + + let specifier = "tests/001_hello.js"; + + let cwd = std::env::current_dir().unwrap(); + let expected_path = cwd.join("..").join(specifier); + let expected_module_name = + Url::from_file_path(&expected_path).unwrap().to_string(); + let expected_filename = deno_fs::normalize_path(&expected_path); + + let (module_name, filename) = + deno_dir.resolve_module(specifier, "..").unwrap(); + assert_eq!(module_name, expected_module_name); + assert_eq!(filename, expected_filename); + + let (module_name, filename) = + deno_dir.resolve_module(specifier, "../").unwrap(); + assert_eq!(module_name, expected_module_name); + assert_eq!(filename, expected_filename); + } + + #[test] + fn test_map_file_extension() { + assert_eq!( + map_file_extension(Path::new("foo/bar.ts")), + msg::MediaType::TypeScript + ); + assert_eq!( + map_file_extension(Path::new("foo/bar.d.ts")), + msg::MediaType::TypeScript + ); + assert_eq!( + map_file_extension(Path::new("foo/bar.js")), + msg::MediaType::JavaScript + ); + assert_eq!( + map_file_extension(Path::new("foo/bar.json")), + msg::MediaType::Json + ); + assert_eq!( + map_file_extension(Path::new("foo/bar.txt")), + msg::MediaType::Unknown + ); + assert_eq!( + map_file_extension(Path::new("foo/bar")), + msg::MediaType::Unknown + ); + } + + #[test] + fn test_map_content_type() { + // Extension only + assert_eq!( + map_content_type(Path::new("foo/bar.ts"), None), + msg::MediaType::TypeScript + ); + assert_eq!( + map_content_type(Path::new("foo/bar.d.ts"), None), + msg::MediaType::TypeScript + ); + assert_eq!( + map_content_type(Path::new("foo/bar.js"), None), + msg::MediaType::JavaScript + ); + assert_eq!( + map_content_type(Path::new("foo/bar.json"), None), + msg::MediaType::Json + ); + assert_eq!( + map_content_type(Path::new("foo/bar.txt"), None), + msg::MediaType::Unknown + ); + assert_eq!( + map_content_type(Path::new("foo/bar"), None), + msg::MediaType::Unknown + ); + + // Media Type + assert_eq!( + map_content_type(Path::new("foo/bar"), Some("application/typescript")), + msg::MediaType::TypeScript + ); + assert_eq!( + map_content_type(Path::new("foo/bar"), Some("text/typescript")), + msg::MediaType::TypeScript + ); + assert_eq!( + map_content_type(Path::new("foo/bar"), Some("video/vnd.dlna.mpeg-tts")), + msg::MediaType::TypeScript + ); + assert_eq!( + map_content_type(Path::new("foo/bar"), Some("video/mp2t")), + msg::MediaType::TypeScript + ); + assert_eq!( + map_content_type(Path::new("foo/bar"), Some("application/x-typescript")), + msg::MediaType::TypeScript + ); + assert_eq!( + map_content_type(Path::new("foo/bar"), Some("application/javascript")), + msg::MediaType::JavaScript + ); + assert_eq!( + map_content_type(Path::new("foo/bar"), Some("text/javascript")), + msg::MediaType::JavaScript + ); + assert_eq!( + map_content_type(Path::new("foo/bar"), Some("application/ecmascript")), + msg::MediaType::JavaScript + ); + assert_eq!( + map_content_type(Path::new("foo/bar"), Some("text/ecmascript")), + msg::MediaType::JavaScript + ); + assert_eq!( + map_content_type(Path::new("foo/bar"), Some("application/x-javascript")), + msg::MediaType::JavaScript + ); + assert_eq!( + map_content_type(Path::new("foo/bar"), Some("application/json")), + msg::MediaType::Json + ); + assert_eq!( + map_content_type(Path::new("foo/bar"), Some("text/json")), + msg::MediaType::Json + ); + assert_eq!( + map_content_type(Path::new("foo/bar.ts"), Some("text/plain")), + msg::MediaType::TypeScript + ); + assert_eq!( + map_content_type(Path::new("foo/bar.ts"), Some("foo/bar")), + msg::MediaType::Unknown + ); + } + + #[test] + fn test_filter_shebang() { + assert_eq!(filter_shebang("#!".as_bytes().to_owned()), "".as_bytes()); + assert_eq!( + filter_shebang("#!\n\n".as_bytes().to_owned()), + "\n\n".as_bytes() + ); + let code = "#!/usr/bin/env deno\nconsole.log('hello');\n" + .as_bytes() + .to_owned(); + assert_eq!(filter_shebang(code), "\nconsole.log('hello');\n".as_bytes()); + } +} diff --git a/cli/errors.rs b/cli/errors.rs new file mode 100644 index 000000000..65118d070 --- /dev/null +++ b/cli/errors.rs @@ -0,0 +1,207 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use crate::js_errors::JSErrorColor; +pub use crate::msg::ErrorKind; +use crate::resolve_addr::ResolveAddrError; +use deno_core::JSError; +use hyper; +use std; +use std::fmt; +use std::io; +use url; + +pub type DenoResult<T> = std::result::Result<T, DenoError>; + +#[derive(Debug)] +pub struct DenoError { + repr: Repr, +} + +#[derive(Debug)] +enum Repr { + Simple(ErrorKind, String), + IoErr(io::Error), + UrlErr(url::ParseError), + HyperErr(hyper::Error), +} + +pub fn new(kind: ErrorKind, msg: String) -> DenoError { + DenoError { + repr: Repr::Simple(kind, msg), + } +} + +impl DenoError { + pub fn kind(&self) -> ErrorKind { + match self.repr { + Repr::Simple(kind, ref _msg) => kind, + // Repr::Simple(kind) => kind, + Repr::IoErr(ref err) => { + use std::io::ErrorKind::*; + match err.kind() { + NotFound => ErrorKind::NotFound, + PermissionDenied => ErrorKind::PermissionDenied, + ConnectionRefused => ErrorKind::ConnectionRefused, + ConnectionReset => ErrorKind::ConnectionReset, + ConnectionAborted => ErrorKind::ConnectionAborted, + NotConnected => ErrorKind::NotConnected, + AddrInUse => ErrorKind::AddrInUse, + AddrNotAvailable => ErrorKind::AddrNotAvailable, + BrokenPipe => ErrorKind::BrokenPipe, + AlreadyExists => ErrorKind::AlreadyExists, + WouldBlock => ErrorKind::WouldBlock, + InvalidInput => ErrorKind::InvalidInput, + InvalidData => ErrorKind::InvalidData, + TimedOut => ErrorKind::TimedOut, + Interrupted => ErrorKind::Interrupted, + WriteZero => ErrorKind::WriteZero, + Other => ErrorKind::Other, + UnexpectedEof => ErrorKind::UnexpectedEof, + _ => unreachable!(), + } + } + Repr::UrlErr(ref err) => { + use url::ParseError::*; + match err { + EmptyHost => ErrorKind::EmptyHost, + IdnaError => ErrorKind::IdnaError, + InvalidPort => ErrorKind::InvalidPort, + InvalidIpv4Address => ErrorKind::InvalidIpv4Address, + InvalidIpv6Address => ErrorKind::InvalidIpv6Address, + InvalidDomainCharacter => ErrorKind::InvalidDomainCharacter, + RelativeUrlWithoutBase => ErrorKind::RelativeUrlWithoutBase, + RelativeUrlWithCannotBeABaseBase => { + ErrorKind::RelativeUrlWithCannotBeABaseBase + } + SetHostOnCannotBeABaseUrl => ErrorKind::SetHostOnCannotBeABaseUrl, + Overflow => ErrorKind::Overflow, + } + } + Repr::HyperErr(ref err) => { + // For some reason hyper::errors::Kind is private. + if err.is_parse() { + ErrorKind::HttpParse + } else if err.is_user() { + ErrorKind::HttpUser + } else if err.is_canceled() { + ErrorKind::HttpCanceled + } else if err.is_closed() { + ErrorKind::HttpClosed + } else { + ErrorKind::HttpOther + } + } + } + } +} + +impl fmt::Display for DenoError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.repr { + Repr::Simple(_kind, ref err_str) => f.pad(err_str), + Repr::IoErr(ref err) => err.fmt(f), + Repr::UrlErr(ref err) => err.fmt(f), + Repr::HyperErr(ref err) => err.fmt(f), + } + } +} + +impl std::error::Error for DenoError { + fn description(&self) -> &str { + match self.repr { + Repr::Simple(_kind, ref msg) => msg.as_str(), + Repr::IoErr(ref err) => err.description(), + Repr::UrlErr(ref err) => err.description(), + Repr::HyperErr(ref err) => err.description(), + } + } + + fn cause(&self) -> Option<&dyn std::error::Error> { + match self.repr { + Repr::Simple(_kind, ref _msg) => None, + Repr::IoErr(ref err) => Some(err), + Repr::UrlErr(ref err) => Some(err), + Repr::HyperErr(ref err) => Some(err), + } + } +} + +impl From<io::Error> for DenoError { + #[inline] + fn from(err: io::Error) -> Self { + Self { + repr: Repr::IoErr(err), + } + } +} + +impl From<url::ParseError> for DenoError { + #[inline] + fn from(err: url::ParseError) -> Self { + Self { + repr: Repr::UrlErr(err), + } + } +} + +impl From<hyper::Error> for DenoError { + #[inline] + fn from(err: hyper::Error) -> Self { + Self { + repr: Repr::HyperErr(err), + } + } +} + +impl From<ResolveAddrError> for DenoError { + fn from(e: ResolveAddrError) -> Self { + match e { + ResolveAddrError::Syntax => Self { + repr: Repr::Simple( + ErrorKind::InvalidInput, + "invalid address syntax".to_string(), + ), + }, + ResolveAddrError::Resolution(io_err) => Self { + repr: Repr::IoErr(io_err), + }, + } + } +} + +pub fn bad_resource() -> DenoError { + new(ErrorKind::BadResource, String::from("bad resource id")) +} + +pub fn permission_denied() -> DenoError { + new( + ErrorKind::PermissionDenied, + String::from("permission denied"), + ) +} + +#[derive(Debug)] +pub enum RustOrJsError { + Rust(DenoError), + Js(JSError), +} + +impl From<DenoError> for RustOrJsError { + fn from(e: DenoError) -> Self { + RustOrJsError::Rust(e) + } +} + +impl From<JSError> for RustOrJsError { + fn from(e: JSError) -> Self { + RustOrJsError::Js(e) + } +} + +impl fmt::Display for RustOrJsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RustOrJsError::Rust(e) => e.fmt(f), + RustOrJsError::Js(e) => JSErrorColor(e).fmt(f), + } + } +} diff --git a/cli/flags.rs b/cli/flags.rs new file mode 100644 index 000000000..d6a63a9fb --- /dev/null +++ b/cli/flags.rs @@ -0,0 +1,291 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use deno_core::v8_set_flags; +use getopts; +use getopts::Options; + +// Creates vector of strings, Vec<String> +#[cfg(test)] +macro_rules! svec { + ($($x:expr),*) => (vec![$($x.to_string()),*]); +} + +#[cfg_attr(feature = "cargo-clippy", allow(stutter))] +#[derive(Clone, Debug, PartialEq, Default)] +pub struct DenoFlags { + pub help: bool, + pub log_debug: bool, + pub version: bool, + pub reload: bool, + pub recompile: bool, + pub allow_read: bool, + pub allow_write: bool, + pub allow_net: bool, + pub allow_env: bool, + pub allow_run: bool, + pub no_prompts: bool, + pub types: bool, + pub prefetch: bool, + pub info: bool, + pub fmt: bool, +} + +pub fn get_usage(opts: &Options) -> String { + format!( + "Usage: deno script.ts {} +Environment variables: + DENO_DIR Set deno's base directory + NO_COLOR Set to disable color", + opts.usage("") + ) +} + +/// Checks provided arguments for known options and sets appropriate Deno flags +/// for them. Unknown options are returned for further use. +/// Note: +/// +/// 1. This assumes that privileged flags do not accept parameters deno --foo bar. +/// This assumption is currently valid. But if it were to change in the future, +/// this parsing technique would need to be modified. I think we want to keep the +/// privileged flags minimal - so having this restriction is maybe a good thing. +/// +/// 2. Misspelled flags will be forwarded to user code - e.g. --allow-ne would +/// not cause an error. I also think this is ok because missing any of the +/// privileged flags is not destructive. Userland flag parsing would catch these +/// errors. +fn set_recognized_flags( + opts: &Options, + flags: &mut DenoFlags, + args: Vec<String>, +) -> Result<Vec<String>, getopts::Fail> { + let mut rest = Vec::<String>::new(); + // getopts doesn't allow parsing unknown options so we check them + // one-by-one and handle unrecognized ones manually + // better solution welcome! + for arg in args { + let fake_args = vec![arg]; + match opts.parse(&fake_args) { + Err(getopts::Fail::UnrecognizedOption(_)) => { + rest.extend(fake_args); + } + Err(e) => { + return Err(e); + } + Ok(matches) => { + if matches.opt_present("help") { + flags.help = true; + } + if matches.opt_present("log-debug") { + flags.log_debug = true; + } + if matches.opt_present("version") { + flags.version = true; + } + if matches.opt_present("reload") { + flags.reload = true; + } + if matches.opt_present("recompile") { + flags.recompile = true; + } + if matches.opt_present("allow-read") { + flags.allow_read = true; + } + if matches.opt_present("allow-write") { + flags.allow_write = true; + } + if matches.opt_present("allow-net") { + flags.allow_net = true; + } + if matches.opt_present("allow-env") { + flags.allow_env = true; + } + if matches.opt_present("allow-run") { + flags.allow_run = true; + } + if matches.opt_present("allow-all") { + flags.allow_read = true; + flags.allow_env = true; + flags.allow_net = true; + flags.allow_run = true; + flags.allow_read = true; + flags.allow_write = true; + } + if matches.opt_present("no-prompt") { + flags.no_prompts = true; + } + if matches.opt_present("types") { + flags.types = true; + } + if matches.opt_present("prefetch") { + flags.prefetch = true; + } + if matches.opt_present("info") { + flags.info = true; + } + if matches.opt_present("fmt") { + flags.fmt = true; + } + + if !matches.free.is_empty() { + rest.extend(matches.free); + } + } + } + } + Ok(rest) +} + +#[cfg_attr(feature = "cargo-clippy", allow(stutter))] +pub fn set_flags( + args: Vec<String>, +) -> Result<(DenoFlags, Vec<String>, String), String> { + // TODO: all flags passed after "--" are swallowed by v8_set_flags + // eg. deno --allow-net ./test.ts -- --title foobar + // args === ["deno", "--allow-net" "./test.ts"] + let args = v8_set_flags(args); + + let mut opts = Options::new(); + // TODO(kevinkassimo): v8_set_flags intercepts '-help' with single '-' + // Resolve that and then uncomment line below (enabling Go style -long-flag) + // opts.long_only(true); + opts.optflag("", "allow-read", "Allow file system read access"); + opts.optflag("", "allow-write", "Allow file system write access"); + opts.optflag("", "allow-net", "Allow network access"); + opts.optflag("", "allow-env", "Allow environment access"); + opts.optflag("", "allow-run", "Allow running subprocesses"); + opts.optflag("A", "allow-all", "Allow all permissions"); + opts.optflag("", "no-prompt", "Do not use prompts"); + opts.optflag("", "recompile", "Force recompilation of TypeScript code"); + opts.optflag("h", "help", "Print this message"); + opts.optflag("D", "log-debug", "Log debug output"); + opts.optflag("v", "version", "Print the version"); + opts.optflag("r", "reload", "Reload cached remote resources"); + opts.optflag("", "v8-options", "Print V8 command line options"); + opts.optflag("", "types", "Print runtime TypeScript declarations"); + opts.optflag("", "prefetch", "Prefetch the dependencies"); + opts.optflag("", "info", "Show source file related info"); + opts.optflag("", "fmt", "Format code"); + + let mut flags = DenoFlags::default(); + + let rest = + set_recognized_flags(&opts, &mut flags, args).map_err(|e| e.to_string())?; + Ok((flags, rest, get_usage(&opts))) +} + +#[test] +fn test_set_flags_1() { + let (flags, rest, _) = set_flags(svec!["deno", "--version"]).unwrap(); + assert_eq!(rest, svec!["deno"]); + assert_eq!( + flags, + DenoFlags { + version: true, + ..DenoFlags::default() + } + ); +} + +#[test] +fn test_set_flags_2() { + let (flags, rest, _) = + set_flags(svec!["deno", "-r", "-D", "script.ts"]).unwrap(); + assert_eq!(rest, svec!["deno", "script.ts"]); + assert_eq!( + flags, + DenoFlags { + log_debug: true, + reload: true, + ..DenoFlags::default() + } + ); +} + +#[test] +fn test_set_flags_3() { + let (flags, rest, _) = + set_flags(svec!["deno", "-r", "script.ts", "--allow-write"]).unwrap(); + assert_eq!(rest, svec!["deno", "script.ts"]); + assert_eq!( + flags, + DenoFlags { + reload: true, + allow_write: true, + ..DenoFlags::default() + } + ); +} + +#[test] +fn test_set_flags_4() { + let (flags, rest, _) = + set_flags(svec!["deno", "-Dr", "script.ts", "--allow-write"]).unwrap(); + assert_eq!(rest, svec!["deno", "script.ts"]); + assert_eq!( + flags, + DenoFlags { + log_debug: true, + reload: true, + allow_write: true, + ..DenoFlags::default() + } + ); +} + +#[test] +fn test_set_flags_5() { + let (flags, rest, _) = set_flags(svec!["deno", "--types"]).unwrap(); + assert_eq!(rest, svec!["deno"]); + assert_eq!( + flags, + DenoFlags { + types: true, + ..DenoFlags::default() + } + ) +} + +#[test] +fn test_set_flags_6() { + let (flags, rest, _) = + set_flags(svec!["deno", "gist.ts", "--title", "X", "--allow-net"]).unwrap(); + assert_eq!(rest, svec!["deno", "gist.ts", "--title", "X"]); + assert_eq!( + flags, + DenoFlags { + allow_net: true, + ..DenoFlags::default() + } + ) +} + +#[test] +fn test_set_flags_7() { + let (flags, rest, _) = + set_flags(svec!["deno", "gist.ts", "--allow-all"]).unwrap(); + assert_eq!(rest, svec!["deno", "gist.ts"]); + assert_eq!( + flags, + DenoFlags { + allow_net: true, + allow_env: true, + allow_run: true, + allow_read: true, + allow_write: true, + ..DenoFlags::default() + } + ) +} + +#[test] +fn test_set_flags_8() { + let (flags, rest, _) = + set_flags(svec!["deno", "gist.ts", "--allow-read"]).unwrap(); + assert_eq!(rest, svec!["deno", "gist.ts"]); + assert_eq!( + flags, + DenoFlags { + allow_read: true, + ..DenoFlags::default() + } + ) +} diff --git a/cli/fs.rs b/cli/fs.rs new file mode 100644 index 000000000..ff0da95e5 --- /dev/null +++ b/cli/fs.rs @@ -0,0 +1,110 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use std; +use std::fs::{create_dir, DirBuilder, File, OpenOptions}; +use std::io::ErrorKind; +use std::io::Write; +use std::path::{Path, PathBuf}; + +use rand; +use rand::Rng; + +#[cfg(any(unix))] +use std::os::unix::fs::DirBuilderExt; +#[cfg(any(unix))] +use std::os::unix::fs::PermissionsExt; + +pub fn write_file<T: AsRef<[u8]>>( + filename: &Path, + data: T, + perm: u32, +) -> std::io::Result<()> { + write_file_2(filename, data, true, perm, true, false) +} + +pub fn write_file_2<T: AsRef<[u8]>>( + filename: &Path, + data: T, + update_perm: bool, + perm: u32, + is_create: bool, + is_append: bool, +) -> std::io::Result<()> { + let mut file = OpenOptions::new() + .read(false) + .write(true) + .append(is_append) + .truncate(!is_append) + .create(is_create) + .open(filename)?; + + if update_perm { + set_permissions(&mut file, perm)?; + } + + file.write_all(data.as_ref()) +} + +#[cfg(any(unix))] +fn set_permissions(file: &mut File, perm: u32) -> std::io::Result<()> { + debug!("set file perm to {}", perm); + file.set_permissions(PermissionsExt::from_mode(perm & 0o777)) +} +#[cfg(not(any(unix)))] +fn set_permissions(_file: &mut File, _perm: u32) -> std::io::Result<()> { + // NOOP on windows + Ok(()) +} + +pub fn make_temp_dir( + dir: Option<&Path>, + prefix: Option<&str>, + suffix: Option<&str>, +) -> std::io::Result<PathBuf> { + let prefix_ = prefix.unwrap_or(""); + let suffix_ = suffix.unwrap_or(""); + let mut buf: PathBuf = match dir { + Some(ref p) => p.to_path_buf(), + None => std::env::temp_dir(), + }.join("_"); + let mut rng = rand::thread_rng(); + loop { + let unique = rng.gen::<u32>(); + buf.set_file_name(format!("{}{:08x}{}", prefix_, unique, suffix_)); + // TODO: on posix, set mode flags to 0o700. + let r = create_dir(buf.as_path()); + match r { + Err(ref e) if e.kind() == ErrorKind::AlreadyExists => continue, + Ok(_) => return Ok(buf), + Err(e) => return Err(e), + } + } +} + +pub fn mkdir(path: &Path, perm: u32, recursive: bool) -> std::io::Result<()> { + debug!("mkdir -p {}", path.display()); + let mut builder = DirBuilder::new(); + builder.recursive(recursive); + set_dir_permission(&mut builder, perm); + builder.create(path) +} + +#[cfg(any(unix))] +fn set_dir_permission(builder: &mut DirBuilder, perm: u32) { + debug!("set dir perm to {}", perm); + builder.mode(perm & 0o777); +} + +#[cfg(not(any(unix)))] +fn set_dir_permission(_builder: &mut DirBuilder, _perm: u32) { + // NOOP on windows +} + +pub fn normalize_path(path: &Path) -> String { + let s = String::from(path.to_str().unwrap()); + if cfg!(windows) { + // TODO This isn't correct. Probbly should iterate over components. + s.replace("\\", "/") + } else { + s + } +} diff --git a/cli/global_timer.rs b/cli/global_timer.rs new file mode 100644 index 000000000..eef70ddc2 --- /dev/null +++ b/cli/global_timer.rs @@ -0,0 +1,49 @@ +// Copyright 2018-2019 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 and cancel a single timer (or Delay, as Tokio +//! calls it) for an entire Isolate. This is what is implemented here. + +use crate::tokio_util::panic_on_error; +use futures::Future; +use std::time::Instant; +use tokio::sync::oneshot; +use tokio::timer::Delay; + +pub struct GlobalTimer { + tx: Option<oneshot::Sender<()>>, +} + +impl GlobalTimer { + pub fn new() -> Self { + Self { tx: None } + } + + pub fn cancel(&mut self) { + if let Some(tx) = self.tx.take() { + tx.send(()).ok(); + } + } + + pub fn new_timeout( + &mut self, + deadline: Instant, + ) -> impl Future<Item = (), Error = ()> { + if self.tx.is_some() { + self.cancel(); + } + assert!(self.tx.is_none()); + + let (tx, rx) = oneshot::channel(); + self.tx = Some(tx); + + let delay = panic_on_error(Delay::new(deadline)); + let rx = panic_on_error(rx); + + delay.select(rx).then(|_| Ok(())) + } +} diff --git a/cli/http_body.rs b/cli/http_body.rs new file mode 100644 index 000000000..235463ff1 --- /dev/null +++ b/cli/http_body.rs @@ -0,0 +1,112 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +use futures::Async; +use futures::Poll; +use hyper::body::Payload; +use hyper::Body; +use hyper::Chunk; +use std::cmp::min; +use std::io; +use std::io::Read; +use tokio::io::AsyncRead; + +/// Wraps `hyper::Body` so that it can be exposed as an `AsyncRead` and integrated +/// into resources more easily. +pub struct HttpBody { + body: Body, + chunk: Option<Chunk>, + pos: usize, +} + +impl HttpBody { + pub fn from(body: Body) -> Self { + Self { + body, + chunk: None, + pos: 0, + } + } +} + +impl Read for HttpBody { + fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> { + unimplemented!(); + } +} + +impl AsyncRead for HttpBody { + fn poll_read(&mut self, buf: &mut [u8]) -> Poll<usize, io::Error> { + if let Some(chunk) = self.chunk.take() { + debug!( + "HttpBody Fake Read buf {} chunk {} pos {}", + buf.len(), + chunk.len(), + self.pos + ); + let n = min(buf.len(), chunk.len() - self.pos); + { + let rest = &chunk[self.pos..]; + buf[..n].clone_from_slice(&rest[..n]); + } + self.pos += n; + if self.pos == chunk.len() { + self.pos = 0; + } else { + self.chunk = Some(chunk); + } + return Ok(Async::Ready(n)); + } else { + assert_eq!(self.pos, 0); + } + + let p = self.body.poll_data(); + match p { + Err(e) => Err( + // TODO Need to map hyper::Error into std::io::Error. + io::Error::new(io::ErrorKind::Other, e), + ), + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(maybe_chunk)) => match maybe_chunk { + None => Ok(Async::Ready(0)), + Some(chunk) => { + debug!( + "HttpBody Real Read buf {} chunk {} pos {}", + buf.len(), + chunk.len(), + self.pos + ); + let n = min(buf.len(), chunk.len()); + buf[..n].clone_from_slice(&chunk[..n]); + if buf.len() < chunk.len() { + self.pos = n; + self.chunk = Some(chunk); + } + Ok(Async::Ready(n)) + } + }, + } + } +} + +#[test] +fn test_body_async_read() { + use std::str::from_utf8; + let body = Body::from("hello world"); + let mut body = HttpBody::from(body); + + let buf = &mut [0, 0, 0, 0, 0]; + let r = body.poll_read(buf); + assert!(r.is_ok()); + assert_eq!(r.unwrap(), Async::Ready(5)); + assert_eq!(from_utf8(buf).unwrap(), "hello"); + + let r = body.poll_read(buf); + assert!(r.is_ok()); + assert_eq!(r.unwrap(), Async::Ready(5)); + assert_eq!(from_utf8(buf).unwrap(), " worl"); + + let r = body.poll_read(buf); + assert!(r.is_ok()); + assert_eq!(r.unwrap(), Async::Ready(1)); + assert_eq!(from_utf8(&buf[0..1]).unwrap(), "d"); +} diff --git a/cli/http_util.rs b/cli/http_util.rs new file mode 100644 index 000000000..8aadbe136 --- /dev/null +++ b/cli/http_util.rs @@ -0,0 +1,166 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use crate::errors; +use crate::errors::{DenoError, DenoResult}; +use crate::tokio_util; + +use futures::future::{loop_fn, Loop}; +use futures::{future, Future, Stream}; +use hyper; +use hyper::client::{Client, HttpConnector}; +use hyper::header::CONTENT_TYPE; +use hyper::Uri; +use hyper_rustls; + +type Connector = hyper_rustls::HttpsConnector<HttpConnector>; + +lazy_static! { + static ref CONNECTOR: Connector = { + let num_dns_threads = 4; + Connector::new(num_dns_threads) + }; +} + +pub fn get_client() -> Client<Connector, hyper::Body> { + // TODO use Hyper's connection pool. + let c = CONNECTOR.clone(); + Client::builder().build(c) +} + +/// Construct the next uri based on base uri and location header fragment +/// See <https://tools.ietf.org/html/rfc3986#section-4.2> +fn resolve_uri_from_location(base_uri: &Uri, location: &str) -> Uri { + if location.starts_with("http://") || location.starts_with("https://") { + // absolute uri + location + .parse::<Uri>() + .expect("provided redirect url should be a valid url") + } else if location.starts_with("//") { + // "//" authority path-abempty + format!("{}:{}", base_uri.scheme_part().unwrap().as_str(), location) + .parse::<Uri>() + .expect("provided redirect url should be a valid url") + } else if location.starts_with('/') { + // path-absolute + let mut new_uri_parts = base_uri.clone().into_parts(); + new_uri_parts.path_and_query = Some(location.parse().unwrap()); + Uri::from_parts(new_uri_parts).unwrap() + } else { + // assuming path-noscheme | path-empty + let mut new_uri_parts = base_uri.clone().into_parts(); + new_uri_parts.path_and_query = + Some(format!("{}/{}", base_uri.path(), location).parse().unwrap()); + Uri::from_parts(new_uri_parts).unwrap() + } +} + +// The CodeFetch message is used to load HTTP javascript resources and expects a +// synchronous response, this utility method supports that. +pub fn fetch_sync_string(module_name: &str) -> DenoResult<(String, String)> { + let url = module_name.parse::<Uri>().unwrap(); + let client = get_client(); + // TODO(kevinkassimo): consider set a max redirection counter + // to avoid bouncing between 2 or more urls + let fetch_future = loop_fn((client, url), |(client, url)| { + client + .get(url.clone()) + .map_err(DenoError::from) + .and_then(move |response| { + if response.status().is_redirection() { + let location_string = response + .headers() + .get("location") + .expect("url redirection should provide 'location' header") + .to_str() + .unwrap() + .to_string(); + debug!("Redirecting to {}...", &location_string); + let new_url = resolve_uri_from_location(&url, &location_string); + return Ok(Loop::Continue((client, new_url))); + } + if !response.status().is_success() { + return Err(errors::new( + errors::ErrorKind::NotFound, + "module not found".to_string(), + )); + } + Ok(Loop::Break(response)) + }) + }).and_then(|response| { + let content_type = response + .headers() + .get(CONTENT_TYPE) + .map(|content_type| content_type.to_str().unwrap().to_string()); + let body = response + .into_body() + .concat2() + .map(|body| String::from_utf8(body.to_vec()).unwrap()) + .map_err(DenoError::from); + body.join(future::ok(content_type)) + }).and_then(|(body_string, maybe_content_type)| { + future::ok((body_string, maybe_content_type.unwrap())) + }); + + tokio_util::block_on(fetch_future) +} + +#[test] +fn test_fetch_sync_string() { + // Relies on external http server. See tools/http_server.py + tokio_util::init(|| { + let (p, m) = + fetch_sync_string("http://127.0.0.1:4545/package.json").unwrap(); + println!("package.json len {}", p.len()); + assert!(p.len() > 1); + assert!(m == "application/json") + }); +} + +#[test] +fn test_fetch_sync_string_with_redirect() { + // Relies on external http server. See tools/http_server.py + tokio_util::init(|| { + let (p, m) = + fetch_sync_string("http://127.0.0.1:4546/package.json").unwrap(); + println!("package.json len {}", p.len()); + assert!(p.len() > 1); + assert!(m == "application/json") + }); +} + +#[test] +fn test_resolve_uri_from_location_full_1() { + let url = "http://deno.land".parse::<Uri>().unwrap(); + let new_uri = resolve_uri_from_location(&url, "http://golang.org"); + assert_eq!(new_uri.host().unwrap(), "golang.org"); +} + +#[test] +fn test_resolve_uri_from_location_full_2() { + let url = "https://deno.land".parse::<Uri>().unwrap(); + let new_uri = resolve_uri_from_location(&url, "https://golang.org"); + assert_eq!(new_uri.host().unwrap(), "golang.org"); +} + +#[test] +fn test_resolve_uri_from_location_relative_1() { + let url = "http://deno.land/x".parse::<Uri>().unwrap(); + let new_uri = resolve_uri_from_location(&url, "//rust-lang.org/en-US"); + assert_eq!(new_uri.host().unwrap(), "rust-lang.org"); + assert_eq!(new_uri.path(), "/en-US"); +} + +#[test] +fn test_resolve_uri_from_location_relative_2() { + let url = "http://deno.land/x".parse::<Uri>().unwrap(); + let new_uri = resolve_uri_from_location(&url, "/y"); + assert_eq!(new_uri.host().unwrap(), "deno.land"); + assert_eq!(new_uri.path(), "/y"); +} + +#[test] +fn test_resolve_uri_from_location_relative_3() { + let url = "http://deno.land/x".parse::<Uri>().unwrap(); + let new_uri = resolve_uri_from_location(&url, "z"); + assert_eq!(new_uri.host().unwrap(), "deno.land"); + assert_eq!(new_uri.path(), "/x/z"); +} diff --git a/cli/isolate.rs b/cli/isolate.rs new file mode 100644 index 000000000..379203dd3 --- /dev/null +++ b/cli/isolate.rs @@ -0,0 +1,236 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use crate::cli::Cli; +use crate::compiler::compile_sync; +use crate::compiler::ModuleMetaData; +use crate::errors::DenoError; +use crate::errors::RustOrJsError; +use crate::isolate_state::IsolateState; +use crate::js_errors; +use crate::msg; +use deno_core; +use deno_core::deno_mod; +use deno_core::JSError; +use futures::Async; +use futures::Future; +use std::sync::Arc; + +type CoreIsolate = deno_core::Isolate<Cli>; + +/// Wraps deno_core::Isolate to provide source maps, ops for the CLI, and +/// high-level module loading +pub struct Isolate { + inner: CoreIsolate, + state: Arc<IsolateState>, +} + +impl Isolate { + pub fn new(cli: Cli) -> Isolate { + let state = cli.state.clone(); + Self { + inner: CoreIsolate::new(cli), + state, + } + } + + /// Same as execute2() but the filename defaults to "<anonymous>". + pub fn execute(&mut self, js_source: &str) -> Result<(), JSError> { + self.execute2("<anonymous>", js_source) + } + + /// Executes the provided JavaScript source code. The js_filename argument is + /// provided only for debugging purposes. + pub fn execute2( + &mut self, + js_filename: &str, + js_source: &str, + ) -> Result<(), JSError> { + self.inner.execute(js_filename, js_source) + } + + // TODO(ry) make this return a future. + fn mod_load_deps(&self, id: deno_mod) -> Result<(), RustOrJsError> { + // basically iterate over the imports, start loading them. + + let referrer_name = { + let g = self.state.modules.lock().unwrap(); + g.get_name(id).unwrap().clone() + }; + + for specifier in self.inner.mod_get_imports(id) { + let (name, _local_filename) = self + .state + .dir + .resolve_module(&specifier, &referrer_name) + .map_err(DenoError::from) + .map_err(RustOrJsError::from)?; + + debug!("mod_load_deps {}", name); + + if !self.state.modules.lock().unwrap().is_registered(&name) { + let out = fetch_module_meta_data_and_maybe_compile( + &self.state, + &specifier, + &referrer_name, + )?; + let child_id = self.mod_new_and_register( + false, + &out.module_name.clone(), + &out.js_source(), + )?; + + self.mod_load_deps(child_id)?; + } + } + + Ok(()) + } + + /// Executes the provided JavaScript module. + pub fn execute_mod( + &mut self, + js_filename: &str, + is_prefetch: bool, + ) -> Result<(), RustOrJsError> { + // TODO move isolate_state::execute_mod impl here. + self + .execute_mod_inner(js_filename, is_prefetch) + .map_err(|err| match err { + RustOrJsError::Js(err) => RustOrJsError::Js(self.apply_source_map(err)), + x => x, + }) + } + + /// High-level way to execute modules. + /// This will issue HTTP requests and file system calls. + /// Blocks. TODO(ry) Don't block. + fn execute_mod_inner( + &mut self, + url: &str, + is_prefetch: bool, + ) -> Result<(), RustOrJsError> { + let out = fetch_module_meta_data_and_maybe_compile(&self.state, url, ".") + .map_err(RustOrJsError::from)?; + + let id = self + .mod_new_and_register(true, &out.module_name.clone(), &out.js_source()) + .map_err(RustOrJsError::from)?; + + self.mod_load_deps(id)?; + + self + .inner + .mod_instantiate(id) + .map_err(RustOrJsError::from)?; + if !is_prefetch { + self.inner.mod_evaluate(id).map_err(RustOrJsError::from)?; + } + Ok(()) + } + + /// Wraps Isolate::mod_new but registers with modules. + fn mod_new_and_register( + &self, + main: bool, + name: &str, + source: &str, + ) -> Result<deno_mod, JSError> { + let id = self.inner.mod_new(main, name, source)?; + self.state.modules.lock().unwrap().register(id, &name); + Ok(id) + } + + pub fn print_file_info(&self, module: &str) { + let m = self.state.modules.lock().unwrap(); + m.print_file_info(&self.state.dir, module.to_string()); + } + + /// Applies source map to the error. + fn apply_source_map(&self, err: JSError) -> JSError { + js_errors::apply_source_map(&err, &self.state.dir) + } +} + +impl Future for Isolate { + type Item = (); + type Error = JSError; + + fn poll(&mut self) -> Result<Async<()>, Self::Error> { + self.inner.poll().map_err(|err| self.apply_source_map(err)) + } +} + +fn fetch_module_meta_data_and_maybe_compile( + state: &Arc<IsolateState>, + specifier: &str, + referrer: &str, +) -> Result<ModuleMetaData, DenoError> { + let mut out = state.dir.fetch_module_meta_data(specifier, referrer)?; + if (out.media_type == msg::MediaType::TypeScript + && out.maybe_output_code.is_none()) + || state.flags.recompile + { + debug!(">>>>> compile_sync START"); + out = compile_sync(state, specifier, &referrer, &out); + debug!(">>>>> compile_sync END"); + state.dir.code_cache(&out)?; + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::flags; + use crate::permissions::DenoPermissions; + use crate::tokio_util; + use futures::future::lazy; + use std::sync::atomic::Ordering; + + #[test] + fn execute_mod() { + let filename = std::env::current_dir() + .unwrap() + .join("tests/esm_imports_a.js"); + let filename = filename.to_str().unwrap().to_string(); + + let argv = vec![String::from("./deno"), filename.clone()]; + let (flags, rest_argv, _) = flags::set_flags(argv).unwrap(); + + let state = Arc::new(IsolateState::new(flags, rest_argv, None)); + let state_ = state.clone(); + tokio_util::run(lazy(move || { + let cli = Cli::new(None, state.clone(), DenoPermissions::default()); + let mut isolate = Isolate::new(cli); + if let Err(err) = isolate.execute_mod(&filename, false) { + eprintln!("execute_mod err {:?}", err); + } + tokio_util::panic_on_error(isolate) + })); + + let metrics = &state_.metrics; + assert_eq!(metrics.resolve_count.load(Ordering::SeqCst), 1); + } + + #[test] + fn execute_mod_circular() { + let filename = std::env::current_dir().unwrap().join("tests/circular1.js"); + let filename = filename.to_str().unwrap().to_string(); + + let argv = vec![String::from("./deno"), filename.clone()]; + let (flags, rest_argv, _) = flags::set_flags(argv).unwrap(); + + let state = Arc::new(IsolateState::new(flags, rest_argv, None)); + let state_ = state.clone(); + tokio_util::run(lazy(move || { + let cli = Cli::new(None, state.clone(), DenoPermissions::default()); + let mut isolate = Isolate::new(cli); + if let Err(err) = isolate.execute_mod(&filename, false) { + eprintln!("execute_mod err {:?}", err); + } + tokio_util::panic_on_error(isolate) + })); + + let metrics = &state_.metrics; + assert_eq!(metrics.resolve_count.load(Ordering::SeqCst), 2); + } +} diff --git a/cli/isolate_state.rs b/cli/isolate_state.rs new file mode 100644 index 000000000..4cc010389 --- /dev/null +++ b/cli/isolate_state.rs @@ -0,0 +1,110 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use crate::cli::Buf; +use crate::deno_dir; +use crate::flags; +use crate::global_timer::GlobalTimer; +use crate::modules::Modules; +use futures::sync::mpsc as async_mpsc; +use std; +use std::env; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Mutex; + +pub type WorkerSender = async_mpsc::Sender<Buf>; +pub type WorkerReceiver = async_mpsc::Receiver<Buf>; +pub type WorkerChannels = (WorkerSender, WorkerReceiver); + +// AtomicU64 is currently unstable +#[derive(Default)] +pub struct Metrics { + pub ops_dispatched: AtomicUsize, + pub ops_completed: AtomicUsize, + pub bytes_sent_control: AtomicUsize, + pub bytes_sent_data: AtomicUsize, + pub bytes_received: AtomicUsize, + pub resolve_count: AtomicUsize, +} + +// Isolate cannot be passed between threads but IsolateState can. +// IsolateState satisfies Send and Sync. +// So any state that needs to be accessed outside the main V8 thread should be +// inside IsolateState. +#[cfg_attr(feature = "cargo-clippy", allow(stutter))] +pub struct IsolateState { + pub dir: deno_dir::DenoDir, + pub argv: Vec<String>, + pub flags: flags::DenoFlags, + pub metrics: Metrics, + pub modules: Mutex<Modules>, + pub worker_channels: Option<Mutex<WorkerChannels>>, + pub global_timer: Mutex<GlobalTimer>, +} + +impl IsolateState { + pub fn new( + flags: flags::DenoFlags, + argv_rest: Vec<String>, + worker_channels: Option<WorkerChannels>, + ) -> Self { + let custom_root = env::var("DENO_DIR").map(|s| s.into()).ok(); + + Self { + dir: deno_dir::DenoDir::new(flags.reload, flags.recompile, custom_root) + .unwrap(), + argv: argv_rest, + flags, + metrics: Metrics::default(), + modules: Mutex::new(Modules::new()), + worker_channels: worker_channels.map(Mutex::new), + global_timer: Mutex::new(GlobalTimer::new()), + } + } + + pub fn main_module(&self) -> Option<String> { + if self.argv.len() <= 1 { + None + } else { + let specifier = self.argv[1].clone(); + let referrer = "."; + match self.dir.resolve_module_url(&specifier, referrer) { + Ok(url) => Some(url.to_string()), + Err(e) => { + debug!("Potentially swallowed error {}", e); + None + } + } + } + } + + #[cfg(test)] + pub fn mock() -> IsolateState { + let argv = vec![String::from("./deno"), String::from("hello.js")]; + // For debugging: argv.push_back(String::from("-D")); + let (flags, rest_argv, _) = flags::set_flags(argv).unwrap(); + IsolateState::new(flags, rest_argv, None) + } + + pub fn metrics_op_dispatched( + &self, + bytes_sent_control: usize, + bytes_sent_data: usize, + ) { + self.metrics.ops_dispatched.fetch_add(1, Ordering::SeqCst); + self + .metrics + .bytes_sent_control + .fetch_add(bytes_sent_control, Ordering::SeqCst); + self + .metrics + .bytes_sent_data + .fetch_add(bytes_sent_data, Ordering::SeqCst); + } + + pub fn metrics_op_completed(&self, bytes_received: usize) { + self.metrics.ops_completed.fetch_add(1, Ordering::SeqCst); + self + .metrics + .bytes_received + .fetch_add(bytes_received, Ordering::SeqCst); + } +} diff --git a/cli/js_errors.rs b/cli/js_errors.rs new file mode 100644 index 000000000..90c9f2007 --- /dev/null +++ b/cli/js_errors.rs @@ -0,0 +1,424 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +//! This mod adds source maps and ANSI color display to deno_core::JSError. +use crate::ansi; +use deno_core::JSError; +use deno_core::StackFrame; +use source_map_mappings::parse_mappings; +use source_map_mappings::Bias; +use source_map_mappings::Mappings; +use std::collections::HashMap; +use std::fmt; +use std::str; + +/// Wrapper around JSError which provides color to_string. +pub struct JSErrorColor<'a>(pub &'a JSError); + +struct StackFrameColor<'a>(&'a StackFrame); + +pub trait SourceMapGetter { + /// Returns the raw source map file. + fn get_source_map(&self, script_name: &str) -> Option<Vec<u8>>; +} + +/// Cached filename lookups. The key can be None if a previous lookup failed to +/// find a SourceMap. +type CachedMaps = HashMap<String, Option<SourceMap>>; + +struct SourceMap { + mappings: Mappings, + sources: Vec<String>, +} + +impl<'a> fmt::Display for StackFrameColor<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let frame = self.0; + // Note when we print to string, we change from 0-indexed to 1-indexed. + let function_name = ansi::italic_bold(frame.function_name.clone()); + let script_line_column = + format_script_line_column(&frame.script_name, frame.line, frame.column); + + if !frame.function_name.is_empty() { + write!(f, " at {} ({})", function_name, script_line_column) + } else if frame.is_eval { + write!(f, " at eval ({})", script_line_column) + } else { + write!(f, " at {}", script_line_column) + } + } +} + +fn format_script_line_column( + script_name: &str, + line: i64, + column: i64, +) -> String { + // TODO match this style with how typescript displays errors. + let line = ansi::yellow((1 + line).to_string()); + let column = ansi::yellow((1 + column).to_string()); + let script_name = ansi::cyan(script_name.to_string()); + format!("{}:{}:{}", script_name, line, column) +} + +impl<'a> fmt::Display for JSErrorColor<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let e = self.0; + if e.script_resource_name.is_some() { + let script_resource_name = e.script_resource_name.as_ref().unwrap(); + // Avoid showing internal code from gen/bundle/main.js + if script_resource_name != "gen/bundle/main.js" + && script_resource_name != "gen/bundle/compiler.js" + { + if e.line_number.is_some() && e.start_column.is_some() { + assert!(e.line_number.is_some()); + assert!(e.start_column.is_some()); + let script_line_column = format_script_line_column( + script_resource_name, + e.line_number.unwrap() - 1, + e.start_column.unwrap() - 1, + ); + write!(f, "{}", script_line_column)?; + } + if e.source_line.is_some() { + write!(f, "\n{}\n", e.source_line.as_ref().unwrap())?; + let mut s = String::new(); + for i in 0..e.end_column.unwrap() { + if i >= e.start_column.unwrap() { + s.push('^'); + } else { + s.push(' '); + } + } + writeln!(f, "{}", ansi::red_bold(s))?; + } + } + } + + write!(f, "{}", ansi::bold(e.message.clone()))?; + + for frame in &e.frames { + write!(f, "\n{}", StackFrameColor(&frame).to_string())?; + } + Ok(()) + } +} + +impl SourceMap { + fn from_json(json_str: &str) -> Option<Self> { + // Ugly. Maybe use serde_derive. + match serde_json::from_str::<serde_json::Value>(json_str) { + Ok(serde_json::Value::Object(map)) => match map["mappings"].as_str() { + None => None, + Some(mappings_str) => { + match parse_mappings::<()>(mappings_str.as_bytes()) { + Err(_) => None, + Ok(mappings) => { + if !map["sources"].is_array() { + return None; + } + let sources_val = map["sources"].as_array().unwrap(); + let mut sources = Vec::<String>::new(); + + for source_val in sources_val { + match source_val.as_str() { + None => return None, + Some(source) => { + sources.push(source.to_string()); + } + } + } + + Some(SourceMap { sources, mappings }) + } + } + } + }, + _ => None, + } + } +} + +fn frame_apply_source_map( + frame: &StackFrame, + mappings_map: &mut CachedMaps, + getter: &dyn SourceMapGetter, +) -> StackFrame { + let maybe_sm = get_mappings(frame.script_name.as_ref(), mappings_map, getter); + let frame_pos = ( + frame.script_name.to_owned(), + frame.line as i64, + frame.column as i64, + ); + let (script_name, line, column) = match maybe_sm { + None => frame_pos, + Some(sm) => match sm.mappings.original_location_for( + frame.line as u32, + frame.column as u32, + Bias::default(), + ) { + None => frame_pos, + Some(mapping) => match &mapping.original { + None => frame_pos, + Some(original) => { + let orig_source = sm.sources[original.source as usize].clone(); + ( + orig_source, + i64::from(original.original_line), + i64::from(original.original_column), + ) + } + }, + }, + }; + + StackFrame { + script_name, + function_name: frame.function_name.clone(), + line, + column, + is_eval: frame.is_eval, + is_constructor: frame.is_constructor, + is_wasm: frame.is_wasm, + } +} + +pub fn apply_source_map( + js_error: &JSError, + getter: &dyn SourceMapGetter, +) -> JSError { + let mut mappings_map: CachedMaps = HashMap::new(); + let mut frames = Vec::<StackFrame>::new(); + for frame in &js_error.frames { + let f = frame_apply_source_map(&frame, &mut mappings_map, getter); + frames.push(f); + } + JSError { + message: js_error.message.clone(), + frames, + error_level: js_error.error_level, + source_line: js_error.source_line.clone(), + // TODO the following need to be source mapped: + script_resource_name: js_error.script_resource_name.clone(), + line_number: js_error.line_number, + start_position: js_error.start_position, + end_position: js_error.end_position, + start_column: js_error.start_column, + end_column: js_error.end_column, + } +} + +// The bundle does not get built for 'cargo check', so we don't embed the +// bundle source map. +#[cfg(feature = "check-only")] +fn builtin_source_map(script_name: &str) -> Option<Vec<u8>> { + None +} + +#[cfg(not(feature = "check-only"))] +fn builtin_source_map(script_name: &str) -> Option<Vec<u8>> { + match script_name { + "gen/bundle/main.js" => Some( + include_bytes!(concat!(env!("GN_OUT_DIR"), "/gen/bundle/main.js.map")) + .to_vec(), + ), + "gen/bundle/compiler.js" => Some( + include_bytes!(concat!( + env!("GN_OUT_DIR"), + "/gen/bundle/compiler.js.map" + )).to_vec(), + ), + _ => None, + } +} + +fn parse_map_string( + script_name: &str, + getter: &dyn SourceMapGetter, +) -> Option<SourceMap> { + builtin_source_map(script_name) + .or_else(|| getter.get_source_map(script_name)) + .and_then(|raw_source_map| { + SourceMap::from_json(str::from_utf8(&raw_source_map).unwrap()) + }) +} + +fn get_mappings<'a>( + script_name: &str, + mappings_map: &'a mut CachedMaps, + getter: &dyn SourceMapGetter, +) -> &'a Option<SourceMap> { + mappings_map + .entry(script_name.to_string()) + .or_insert_with(|| parse_map_string(script_name, getter)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ansi::strip_ansi_codes; + + fn error1() -> JSError { + JSError { + message: "Error: foo bar".to_string(), + source_line: None, + script_resource_name: None, + line_number: None, + start_position: None, + end_position: None, + error_level: None, + start_column: None, + end_column: None, + frames: vec![ + StackFrame { + line: 4, + column: 16, + script_name: "foo_bar.ts".to_string(), + function_name: "foo".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + StackFrame { + line: 5, + column: 20, + script_name: "bar_baz.ts".to_string(), + function_name: "qat".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + StackFrame { + line: 1, + column: 1, + script_name: "deno_main.js".to_string(), + function_name: "".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + ], + } + } + + struct MockSourceMapGetter {} + + impl SourceMapGetter for MockSourceMapGetter { + fn get_source_map(&self, script_name: &str) -> Option<Vec<u8>> { + let s = match script_name { + "foo_bar.ts" => r#"{"sources": ["foo_bar.ts"], "mappings":";;;IAIA,OAAO,CAAC,GAAG,CAAC,qBAAqB,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC;IAC/C,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACjD,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAE3C,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC"}"#, + "bar_baz.ts" => r#"{"sources": ["bar_baz.ts"], "mappings":";;;IAEA,CAAC,KAAK,IAAI,EAAE;QACV,MAAM,GAAG,GAAG,sDAAa,OAAO,2BAAC,CAAC;QAClC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC,CAAC,EAAE,CAAC;IAEQ,QAAA,GAAG,GAAG,KAAK,CAAC;IAEzB,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC"}"#, + _ => return None, + }; + Some(s.as_bytes().to_owned()) + } + } + + #[test] + fn js_error_to_string() { + let e = error1(); + assert_eq!("Error: foo bar\n at foo (foo_bar.ts:5:17)\n at qat (bar_baz.ts:6:21)\n at deno_main.js:2:2", strip_ansi_codes(&e.to_string())); + } + + #[test] + fn js_error_apply_source_map_1() { + let e = error1(); + let getter = MockSourceMapGetter {}; + let actual = apply_source_map(&e, &getter); + let expected = JSError { + message: "Error: foo bar".to_string(), + source_line: None, + script_resource_name: None, + line_number: None, + start_position: None, + end_position: None, + error_level: None, + start_column: None, + end_column: None, + frames: vec![ + StackFrame { + line: 5, + column: 12, + script_name: "foo_bar.ts".to_string(), + function_name: "foo".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + StackFrame { + line: 4, + column: 14, + script_name: "bar_baz.ts".to_string(), + function_name: "qat".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + StackFrame { + line: 1, + column: 1, + script_name: "deno_main.js".to_string(), + function_name: "".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + ], + }; + assert_eq!(actual, expected); + } + + #[test] + fn js_error_apply_source_map_2() { + let e = JSError { + message: "TypeError: baz".to_string(), + source_line: None, + script_resource_name: None, + line_number: None, + start_position: None, + end_position: None, + error_level: None, + start_column: None, + end_column: None, + frames: vec![StackFrame { + line: 11, + column: 12, + script_name: "gen/bundle/main.js".to_string(), + function_name: "setLogDebug".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }], + }; + let getter = MockSourceMapGetter {}; + let actual = apply_source_map(&e, &getter); + assert_eq!(actual.message, "TypeError: baz"); + // Because this is accessing the live bundle, this test might be more fragile + assert_eq!(actual.frames.len(), 1); + assert!(actual.frames[0].script_name.ends_with("js/util.ts")); + } + + #[test] + fn source_map_from_json() { + let json = r#"{"version":3,"file":"error_001.js","sourceRoot":"","sources":["file:///Users/rld/src/deno/tests/error_001.ts"],"names":[],"mappings":"AAAA,SAAS,GAAG;IACV,MAAM,KAAK,CAAC,KAAK,CAAC,CAAC;AACrB,CAAC;AAED,SAAS,GAAG;IACV,GAAG,EAAE,CAAC;AACR,CAAC;AAED,GAAG,EAAE,CAAC"}"#; + let sm = SourceMap::from_json(json).unwrap(); + assert_eq!(sm.sources.len(), 1); + assert_eq!( + sm.sources[0], + "file:///Users/rld/src/deno/tests/error_001.ts" + ); + let mapping = sm + .mappings + .original_location_for(1, 10, Bias::default()) + .unwrap(); + assert_eq!(mapping.generated_line, 1); + assert_eq!(mapping.generated_column, 10); + assert_eq!( + mapping.original, + Some(source_map_mappings::OriginalLocation { + source: 0, + original_line: 1, + original_column: 8, + name: None + }) + ); + } +} diff --git a/cli/main.rs b/cli/main.rs new file mode 100644 index 000000000..4657a3a4d --- /dev/null +++ b/cli/main.rs @@ -0,0 +1,140 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate log; +#[macro_use] +extern crate futures; +#[macro_use] +extern crate serde_json; + +mod ansi; +pub mod cli; +pub mod compiler; +pub mod deno_dir; +pub mod errors; +pub mod flags; +mod fs; +mod global_timer; +mod http_body; +mod http_util; +pub mod isolate; +pub mod isolate_state; +pub mod js_errors; +pub mod modules; +pub mod msg; +pub mod msg_util; +pub mod ops; +pub mod permissions; +mod repl; +pub mod resolve_addr; +pub mod resources; +mod startup_data; +mod tokio_util; +mod tokio_write; +pub mod version; +pub mod workers; + +use crate::cli::Cli; +use crate::errors::RustOrJsError; +use crate::isolate::Isolate; +use crate::isolate_state::IsolateState; +use futures::lazy; +use futures::Future; +use log::{LevelFilter, Metadata, Record}; +use std::env; +use std::sync::Arc; + +static LOGGER: Logger = Logger; + +struct Logger; + +impl log::Log for Logger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= log::max_level() + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + println!("{} RS - {}", record.level(), record.args()); + } + } + fn flush(&self) {} +} + +fn print_err_and_exit(err: RustOrJsError) { + eprintln!("{}", err.to_string()); + std::process::exit(1); +} + +fn js_check<E>(r: Result<(), E>) +where + E: Into<RustOrJsError>, +{ + if let Err(err) = r { + print_err_and_exit(err.into()); + } +} + +fn main() { + #[cfg(windows)] + ansi_term::enable_ansi_support().ok(); // For Windows 10 + + log::set_logger(&LOGGER).unwrap(); + let args = env::args().collect(); + let (mut flags, mut rest_argv, usage_string) = flags::set_flags(args) + .unwrap_or_else(|err| { + eprintln!("{}", err); + std::process::exit(1) + }); + + if flags.help { + println!("{}", &usage_string); + std::process::exit(0); + } + + log::set_max_level(if flags.log_debug { + LevelFilter::Debug + } else { + LevelFilter::Warn + }); + + if flags.fmt { + rest_argv.insert(1, "https://deno.land/std/prettier/main.ts".to_string()); + flags.allow_read = true; + flags.allow_write = true; + } + + let should_prefetch = flags.prefetch || flags.info; + let should_display_info = flags.info; + + let state = Arc::new(IsolateState::new(flags, rest_argv, None)); + let state_ = state.clone(); + let startup_data = startup_data::deno_isolate_init(); + let permissions = permissions::DenoPermissions::from_flags(&state.flags); + let cli = Cli::new(Some(startup_data), state_, permissions); + let mut isolate = Isolate::new(cli); + + let main_future = lazy(move || { + // Setup runtime. + js_check(isolate.execute("denoMain()")); + + // Execute main module. + if let Some(main_module) = state.main_module() { + debug!("main_module {}", main_module); + js_check(isolate.execute_mod(&main_module, should_prefetch)); + if should_display_info { + // Display file info and exit. Do not run file + isolate.print_file_info(&main_module); + std::process::exit(0); + } + } + + isolate.then(|result| { + js_check(result); + Ok(()) + }) + }); + + tokio_util::run(main_future); +} diff --git a/cli/modules.rs b/cli/modules.rs new file mode 100644 index 000000000..908c31b6d --- /dev/null +++ b/cli/modules.rs @@ -0,0 +1,204 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use crate::ansi; +use crate::deno_dir::DenoDir; +use crate::msg; +use deno_core::deno_mod; +use std::collections::HashMap; +use std::collections::HashSet; +use std::fmt; + +pub struct ModuleInfo { + name: String, + children: Vec<deno_mod>, +} + +/// A collection of JS modules. +#[derive(Default)] +pub struct Modules { + pub info: HashMap<deno_mod, ModuleInfo>, + pub by_name: HashMap<String, deno_mod>, +} + +impl Modules { + pub fn new() -> Modules { + Self { + info: HashMap::new(), + by_name: HashMap::new(), + } + } + + pub fn get_id(&self, name: &str) -> Option<deno_mod> { + self.by_name.get(name).cloned() + } + + pub fn get_children(&self, id: deno_mod) -> Option<&Vec<deno_mod>> { + self.info.get(&id).map(|i| &i.children) + } + + pub fn get_name(&self, id: deno_mod) -> Option<&String> { + self.info.get(&id).map(|i| &i.name) + } + + pub fn is_registered(&self, name: &str) -> bool { + self.by_name.get(name).is_some() + } + + pub fn register(&mut self, id: deno_mod, name: &str) { + let name = String::from(name); + debug!("register {}", name); + self.by_name.insert(name.clone(), id); + self.info.insert( + id, + ModuleInfo { + name, + children: Vec::new(), + }, + ); + } + + pub fn resolve_cb( + &mut self, + deno_dir: &DenoDir, + specifier: &str, + referrer: deno_mod, + ) -> deno_mod { + debug!("resolve_cb {}", specifier); + + let maybe_info = self.info.get_mut(&referrer); + if maybe_info.is_none() { + debug!("cant find referrer {}", referrer); + return 0; + } + let info = maybe_info.unwrap(); + let referrer_name = &info.name; + let r = deno_dir.resolve_module(specifier, referrer_name); + if let Err(err) = r { + debug!("potentially swallowed err: {}", err); + return 0; + } + let (name, _local_filename) = r.unwrap(); + + if let Some(id) = self.by_name.get(&name) { + let child_id = *id; + info.children.push(child_id); + return child_id; + } else { + return 0; + } + } + + pub fn print_file_info(&self, deno_dir: &DenoDir, filename: String) { + let maybe_out = deno_dir.fetch_module_meta_data(&filename, "."); + if maybe_out.is_err() { + println!("{}", maybe_out.unwrap_err()); + return; + } + let out = maybe_out.unwrap(); + + println!("{} {}", ansi::bold("local:".to_string()), &(out.filename)); + println!( + "{} {}", + ansi::bold("type:".to_string()), + msg::enum_name_media_type(out.media_type) + ); + if out.maybe_output_code_filename.is_some() { + println!( + "{} {}", + ansi::bold("compiled:".to_string()), + out.maybe_output_code_filename.as_ref().unwrap(), + ); + } + if out.maybe_source_map_filename.is_some() { + println!( + "{} {}", + ansi::bold("map:".to_string()), + out.maybe_source_map_filename.as_ref().unwrap() + ); + } + + let deps = Deps::new(self, &out.module_name); + println!("{}{}", ansi::bold("deps:\n".to_string()), deps.name); + if let Some(ref depsdeps) = deps.deps { + for d in depsdeps { + println!("{}", d); + } + } + } +} + +pub struct Deps { + pub name: String, + pub deps: Option<Vec<Deps>>, + prefix: String, + is_last: bool, +} + +impl Deps { + pub fn new(modules: &Modules, module_name: &str) -> Deps { + let mut seen = HashSet::new(); + let id = modules.get_id(module_name).unwrap(); + Self::helper(&mut seen, "".to_string(), true, modules, id) + } + + fn helper( + seen: &mut HashSet<deno_mod>, + prefix: String, + is_last: bool, + modules: &Modules, + id: deno_mod, + ) -> Deps { + let name = modules.get_name(id).unwrap().to_string(); + if seen.contains(&id) { + Deps { + name, + prefix, + deps: None, + is_last, + } + } else { + seen.insert(id); + let child_ids = modules.get_children(id).unwrap(); + let child_count = child_ids.iter().count(); + let deps = child_ids + .iter() + .enumerate() + .map(|(index, dep_id)| { + let new_is_last = index == child_count - 1; + let mut new_prefix = prefix.clone(); + new_prefix.push(if is_last { ' ' } else { '│' }); + new_prefix.push(' '); + Self::helper(seen, new_prefix, new_is_last, modules, *dep_id) + }).collect(); + Deps { + name, + prefix, + deps: Some(deps), + is_last, + } + } + } +} + +impl fmt::Display for Deps { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut has_children = false; + if let Some(ref deps) = self.deps { + has_children = !deps.is_empty(); + } + write!( + f, + "{}{}─{} {}", + self.prefix, + if self.is_last { "└" } else { "├" }, + if has_children { "┬" } else { "─" }, + self.name + )?; + + if let Some(ref deps) = self.deps { + for d in deps { + write!(f, "\n{}", d)?; + } + } + Ok(()) + } +} diff --git a/cli/msg.fbs b/cli/msg.fbs new file mode 100644 index 000000000..243034cfb --- /dev/null +++ b/cli/msg.fbs @@ -0,0 +1,524 @@ +union Any { + Accept, + Chdir, + Chmod, + Close, + CopyFile, + Cwd, + CwdRes, + Dial, + Environ, + EnvironRes, + Exit, + Fetch, + FetchModuleMetaData, + FetchModuleMetaDataRes, + FetchRes, + FormatError, + FormatErrorRes, + GlobalTimer, + GlobalTimerRes, + GlobalTimerStop, + IsTTY, + IsTTYRes, + Listen, + ListenRes, + MakeTempDir, + MakeTempDirRes, + Metrics, + MetricsRes, + Mkdir, + NewConn, + Now, + NowRes, + Open, + OpenRes, + PermissionRevoke, + Permissions, + PermissionsRes, + Read, + ReadDir, + ReadDirRes, + ReadFile, + ReadFileRes, + ReadRes, + Readlink, + ReadlinkRes, + Remove, + Rename, + ReplReadline, + ReplReadlineRes, + ReplStart, + ReplStartRes, + Resources, + ResourcesRes, + Run, + RunRes, + RunStatus, + RunStatusRes, + Seek, + SetEnv, + Shutdown, + Start, + StartRes, + Stat, + StatRes, + Symlink, + Truncate, + WorkerGetMessage, + WorkerGetMessageRes, + WorkerPostMessage, + Write, + WriteFile, + WriteRes, +} + +enum ErrorKind: byte { + NoError = 0, + + // io errors + + NotFound, + PermissionDenied, + ConnectionRefused, + ConnectionReset, + ConnectionAborted, + NotConnected, + AddrInUse, + AddrNotAvailable, + BrokenPipe, + AlreadyExists, + WouldBlock, + InvalidInput, + InvalidData, + TimedOut, + Interrupted, + WriteZero, + Other, + UnexpectedEof, + BadResource, + CommandFailed, + + // url errors + + EmptyHost, + IdnaError, + InvalidPort, + InvalidIpv4Address, + InvalidIpv6Address, + InvalidDomainCharacter, + RelativeUrlWithoutBase, + RelativeUrlWithCannotBeABaseBase, + SetHostOnCannotBeABaseUrl, + Overflow, + + // hyper errors + + HttpUser, + HttpClosed, + HttpCanceled, + HttpParse, + HttpOther, + TooLarge, + + // custom errors + InvalidUri, + InvalidSeekMode, +} + +table Cwd {} + +table CwdRes { + cwd: string; +} + +enum MediaType: byte { + JavaScript = 0, + TypeScript, + Json, + Unknown +} + +table Base { + cmd_id: uint32; + sync: bool = false; + error_kind: ErrorKind = NoError; + error: string; + inner: Any; +} + +table Start { + unused: int8; +} + +table StartRes { + cwd: string; + pid: uint32; + argv: [string]; + exec_path: string; + main_module: string; // Absolute URL. + debug_flag: bool; + deps_flag: bool; + types_flag: bool; + version_flag: bool; + deno_version: string; + v8_version: string; + no_color: bool; +} + +table FormatError { + error: string; +} + +table FormatErrorRes { + error: string; +} + +table WorkerGetMessage { + unused: int8; +} + +table WorkerGetMessageRes { + data: [ubyte]; +} + +table WorkerPostMessage { + // data passed thru the zero-copy data parameter. +} + +table FetchModuleMetaData { + specifier: string; + referrer: string; +} + +table FetchModuleMetaDataRes { + // If it's a non-http module, moduleName and filename will be the same. + // For http modules, moduleName is its resolved http URL, and filename + // is the location of the locally downloaded source code. + module_name: string; + filename: string; + media_type: MediaType; + data: [ubyte]; +} + +table Chdir { + directory: string; +} + +table GlobalTimer { + timeout: int; +} + +table GlobalTimerRes { } + +table GlobalTimerStop { } + +table Exit { + code: int; +} + +table Environ {} + +table SetEnv { + key: string; + value: string; +} + +table EnvironRes { + map: [KeyValue]; +} + +table KeyValue { + key: string; + value: string; +} + +table Permissions {} + +table PermissionRevoke { + permission: string; +} + +table PermissionsRes { + run: bool; + read: bool; + write: bool; + net: bool; + env: bool; +} + +// Note this represents The WHOLE header of an http message, not just the key +// value pairs. That means it includes method and url for Requests and status +// for responses. This is why it is singular "Header" instead of "Headers". +table HttpHeader { + is_request: bool; + // Request only: + method: string; + url: string; + // Response only: + status: uint16; + // Both: + fields: [KeyValue]; +} + +table Fetch { + header: HttpHeader; +} + +table FetchRes { + header: HttpHeader; + body_rid: uint32; +} + +table MakeTempDir { + dir: string; + prefix: string; + suffix: string; +} + +table MakeTempDirRes { + path: string; +} + +table Mkdir { + path: string; + recursive: bool; + mode: uint; // Specified by https://godoc.org/os#FileMode +} + +table Chmod { + path: string; + mode: uint; // Specified by https://godoc.org/os#FileMode +} + +table Remove { + path: string; + recursive: bool; +} + +table ReadFile { + filename: string; +} + +table ReadFileRes { + data: [ubyte]; +} + +table ReadDir { + path: string; +} + +table ReadDirRes { + entries: [StatRes]; +} + +table WriteFile { + filename: string; + data: [ubyte]; + update_perm: bool; + perm: uint; + // perm specified by https://godoc.org/os#FileMode + is_create: bool; + is_append: bool; +} + +table CopyFile { + from: string; + to: string; +} + +table Rename { + oldpath: string; + newpath: string; +} + +table Readlink { + name: string; +} + +table ReadlinkRes { + path: string; +} + +table ReplStart { + history_file: string; + // TODO add config +} + +table ReplStartRes { + rid: uint32; +} + +table ReplReadline { + rid: uint32; + prompt: string; +} + +table ReplReadlineRes { + line: string; +} + +table Resources {} + +table Resource { + rid: uint32; + repr: string; +} + +table ResourcesRes { + resources: [Resource]; +} + +table Symlink { + oldname: string; + newname: string; +} + +table Stat { + filename: string; + lstat: bool; +} + +table StatRes { + is_file: bool; + is_symlink: bool; + len: ulong; + modified:ulong; + accessed:ulong; + created:ulong; + mode: uint; + has_mode: bool; // false on windows + name: string; + path: string; +} + +table Truncate { + name: string; + len: uint; +} + +table Open { + filename: string; + perm: uint; + mode: string; +} + +table OpenRes { + rid: uint32; +} + +table Read { + rid: uint32; + // (ptr, len) is passed as second parameter to libdeno.send(). +} + +table ReadRes { + nread: uint; + eof: bool; +} + +table Write { + rid: uint32; +} + +table WriteRes { + nbyte: uint; +} + +table Close { + rid: uint32; +} + +table Shutdown { + rid: uint32; + how: uint; +} + +table Listen { + network: string; + address: string; +} + +table ListenRes { + rid: uint32; +} + +table Accept { + rid: uint32; +} + +table Dial { + network: string; + address: string; +} + +// Response to Accept and Dial. +table NewConn { + rid: uint32; + remote_addr: string; + local_addr: string; +} + +table Metrics {} + +table MetricsRes { + ops_dispatched: uint64; + ops_completed: uint64; + bytes_sent_control: uint64; + bytes_sent_data: uint64; + bytes_received: uint64; +} + +enum ProcessStdio: byte { Inherit, Piped, Null } + +table Run { + args: [string]; + cwd: string; + env: [KeyValue]; + stdin: ProcessStdio; + stdout: ProcessStdio; + stderr: ProcessStdio; +} + +table RunRes { + rid: uint32; + pid: uint32; + // The following stdio rids are only valid if "Piped" was specified for the + // corresponding stdio stream. The caller MUST issue a close op for all valid + // stdio streams. + stdin_rid: uint32; + stdout_rid: uint32; + stderr_rid: uint32; +} + +table RunStatus { + rid: uint32; +} + +table RunStatusRes { + got_signal: bool; + exit_code: int; + exit_signal: int; +} + +table Now {} + +table NowRes { + time: uint64; +} + +table IsTTY {} + +table IsTTYRes { + stdin: bool; + stdout: bool; + stderr: bool; +} + +table Seek { + rid: uint32; + offset: int; + whence: uint; +} + +root_type Base; diff --git a/cli/msg.rs b/cli/msg.rs new file mode 100644 index 000000000..080f39de8 --- /dev/null +++ b/cli/msg.rs @@ -0,0 +1,26 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +#![allow(unused_imports)] +#![allow(dead_code)] +#![cfg_attr( + feature = "cargo-clippy", + allow(clippy::all, clippy::pedantic) +)] +use crate::isolate_state; +use flatbuffers; +use std::sync::atomic::Ordering; + +// GN_OUT_DIR is set either by build.rs (for the Cargo build), or by +// build_extra/rust/run.py (for the GN+Ninja build). +include!(concat!(env!("GN_OUT_DIR"), "/gen/msg_generated.rs")); + +impl<'a> From<&'a isolate_state::Metrics> for MetricsResArgs { + fn from(m: &'a isolate_state::Metrics) -> Self { + MetricsResArgs { + ops_dispatched: m.ops_dispatched.load(Ordering::SeqCst) as u64, + ops_completed: m.ops_completed.load(Ordering::SeqCst) as u64, + bytes_sent_control: m.bytes_sent_control.load(Ordering::SeqCst) as u64, + bytes_sent_data: m.bytes_sent_data.load(Ordering::SeqCst) as u64, + bytes_received: m.bytes_received.load(Ordering::SeqCst) as u64, + } + } +} diff --git a/cli/msg_util.rs b/cli/msg_util.rs new file mode 100644 index 000000000..71bcc19d9 --- /dev/null +++ b/cli/msg_util.rs @@ -0,0 +1,127 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +// Helpers for serialization. +use crate::errors; +use crate::errors::DenoResult; +use crate::msg; + +use flatbuffers; +use http::header::HeaderName; +use http::uri::Uri; +use http::Method; +use hyper::header::HeaderMap; +use hyper::header::HeaderValue; +use hyper::Body; +use hyper::Request; +use hyper::Response; +use std::str::FromStr; + +type Headers = HeaderMap<HeaderValue>; + +pub fn serialize_key_value<'bldr>( + builder: &mut flatbuffers::FlatBufferBuilder<'bldr>, + key: &str, + value: &str, +) -> flatbuffers::WIPOffset<msg::KeyValue<'bldr>> { + let key = builder.create_string(&key); + let value = builder.create_string(&value); + msg::KeyValue::create( + builder, + &msg::KeyValueArgs { + key: Some(key), + value: Some(value), + }, + ) +} + +pub fn serialize_request_header<'bldr>( + builder: &mut flatbuffers::FlatBufferBuilder<'bldr>, + r: &Request<Body>, +) -> flatbuffers::WIPOffset<msg::HttpHeader<'bldr>> { + let method = builder.create_string(r.method().as_str()); + let url = builder.create_string(r.uri().to_string().as_ref()); + + let mut fields = Vec::new(); + for (key, val) in r.headers().iter() { + let kv = serialize_key_value(builder, key.as_ref(), val.to_str().unwrap()); + fields.push(kv); + } + let fields = builder.create_vector(fields.as_ref()); + + msg::HttpHeader::create( + builder, + &msg::HttpHeaderArgs { + is_request: true, + method: Some(method), + url: Some(url), + fields: Some(fields), + ..Default::default() + }, + ) +} + +pub fn serialize_fields<'bldr>( + builder: &mut flatbuffers::FlatBufferBuilder<'bldr>, + headers: &Headers, +) -> flatbuffers::WIPOffset< + flatbuffers::Vector< + 'bldr, + flatbuffers::ForwardsUOffset<msg::KeyValue<'bldr>>, + >, +> { + let mut fields = Vec::new(); + for (key, val) in headers.iter() { + let kv = serialize_key_value(builder, key.as_ref(), val.to_str().unwrap()); + fields.push(kv); + } + builder.create_vector(fields.as_ref()) +} + +// Not to be confused with serialize_response which has nothing to do with HTTP. +pub fn serialize_http_response<'bldr>( + builder: &mut flatbuffers::FlatBufferBuilder<'bldr>, + r: &Response<Body>, +) -> flatbuffers::WIPOffset<msg::HttpHeader<'bldr>> { + let status = r.status().as_u16(); + let fields = serialize_fields(builder, r.headers()); + msg::HttpHeader::create( + builder, + &msg::HttpHeaderArgs { + is_request: false, + status, + fields: Some(fields), + ..Default::default() + }, + ) +} + +pub fn deserialize_request( + header_msg: msg::HttpHeader<'_>, + body: Body, +) -> DenoResult<Request<Body>> { + let mut r = Request::new(body); + + assert!(header_msg.is_request()); + + let u = header_msg.url().unwrap(); + let u = Uri::from_str(u) + .map_err(|e| errors::new(msg::ErrorKind::InvalidUri, e.to_string()))?; + *r.uri_mut() = u; + + if let Some(method) = header_msg.method() { + let method = Method::from_str(method).unwrap(); + *r.method_mut() = method; + } + + if let Some(fields) = header_msg.fields() { + let headers = r.headers_mut(); + for i in 0..fields.len() { + let kv = fields.get(i); + let key = kv.key().unwrap(); + let name = HeaderName::from_bytes(key.as_bytes()).unwrap(); + let value = kv.value().unwrap(); + let v = HeaderValue::from_str(value).unwrap(); + headers.insert(name, v); + } + } + Ok(r) +} diff --git a/cli/ops.rs b/cli/ops.rs new file mode 100644 index 000000000..254a21563 --- /dev/null +++ b/cli/ops.rs @@ -0,0 +1,2020 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use atty; +use crate::ansi; +use crate::cli::Buf; +use crate::cli::Cli; +use crate::errors; +use crate::errors::{permission_denied, DenoError, DenoResult, ErrorKind}; +use crate::fs as deno_fs; +use crate::http_util; +use crate::isolate_state::IsolateState; +use crate::js_errors::apply_source_map; +use crate::js_errors::JSErrorColor; +use crate::msg; +use crate::msg_util; +use crate::repl; +use crate::resolve_addr::resolve_addr; +use crate::resources; +use crate::resources::table_entries; +use crate::resources::Resource; +use crate::tokio_util; +use crate::tokio_write; +use crate::version; +use deno_core::deno_buf; +use deno_core::JSError; +use deno_core::Op; +use flatbuffers::FlatBufferBuilder; +use futures; +use futures::Async; +use futures::Poll; +use futures::Sink; +use futures::Stream; +use hyper; +use hyper::rt::Future; +use remove_dir_all::remove_dir_all; +use std; +use std::convert::From; +use std::fs; +use std::net::Shutdown; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; +use std::sync::Arc; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use tokio; +use tokio::net::TcpListener; +use tokio::net::TcpStream; +use tokio_process::CommandExt; +use tokio_threadpool; + +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +#[cfg(unix)] +use std::os::unix::process::ExitStatusExt; + +type OpResult = DenoResult<Buf>; + +pub type OpWithError = dyn Future<Item = Buf, Error = DenoError> + Send; + +// TODO Ideally we wouldn't have to box the OpWithError being returned. +// The box is just to make it easier to get a prototype refactor working. +type OpCreator = + fn(cli: &Cli, base: &msg::Base<'_>, data: deno_buf) -> Box<OpWithError>; + +#[inline] +fn empty_buf() -> Buf { + Box::new([]) +} + +/// Processes raw messages from JavaScript. +/// This functions invoked every time libdeno.send() is called. +/// control corresponds to the first argument of libdeno.send(). +/// data corresponds to the second argument of libdeno.send(). +pub fn dispatch( + cli: &Cli, + control: &[u8], + zero_copy: deno_buf, +) -> (bool, Box<Op>) { + let bytes_sent_control = control.len(); + let bytes_sent_zero_copy = zero_copy.len(); + let base = msg::get_root_as_base(&control); + let is_sync = base.sync(); + let inner_type = base.inner_type(); + let cmd_id = base.cmd_id(); + + let op: Box<OpWithError> = { + // Handle regular ops. + let op_creator: OpCreator = match inner_type { + msg::Any::Accept => op_accept, + msg::Any::Chdir => op_chdir, + msg::Any::Chmod => op_chmod, + msg::Any::Close => op_close, + msg::Any::CopyFile => op_copy_file, + msg::Any::Cwd => op_cwd, + msg::Any::Dial => op_dial, + msg::Any::Environ => op_env, + msg::Any::Exit => op_exit, + msg::Any::Fetch => op_fetch, + msg::Any::FetchModuleMetaData => op_fetch_module_meta_data, + msg::Any::FormatError => op_format_error, + msg::Any::GlobalTimer => op_global_timer, + msg::Any::GlobalTimerStop => op_global_timer_stop, + msg::Any::IsTTY => op_is_tty, + msg::Any::Listen => op_listen, + msg::Any::MakeTempDir => op_make_temp_dir, + msg::Any::Metrics => op_metrics, + msg::Any::Mkdir => op_mkdir, + msg::Any::Now => op_now, + msg::Any::Open => op_open, + msg::Any::PermissionRevoke => op_revoke_permission, + msg::Any::Permissions => op_permissions, + msg::Any::Read => op_read, + msg::Any::ReadDir => op_read_dir, + msg::Any::ReadFile => op_read_file, + msg::Any::Readlink => op_read_link, + msg::Any::Remove => op_remove, + msg::Any::Rename => op_rename, + msg::Any::ReplReadline => op_repl_readline, + msg::Any::ReplStart => op_repl_start, + msg::Any::Resources => op_resources, + msg::Any::Run => op_run, + msg::Any::RunStatus => op_run_status, + msg::Any::Seek => op_seek, + msg::Any::SetEnv => op_set_env, + msg::Any::Shutdown => op_shutdown, + msg::Any::Start => op_start, + msg::Any::Stat => op_stat, + msg::Any::Symlink => op_symlink, + msg::Any::Truncate => op_truncate, + msg::Any::WorkerGetMessage => op_worker_get_message, + msg::Any::WorkerPostMessage => op_worker_post_message, + msg::Any::Write => op_write, + msg::Any::WriteFile => op_write_file, + _ => panic!(format!( + "Unhandled message {}", + msg::enum_name_any(inner_type) + )), + }; + op_creator(&cli, &base, zero_copy) + }; + + cli + .state + .metrics_op_dispatched(bytes_sent_control, bytes_sent_zero_copy); + let state = cli.state.clone(); + + let boxed_op = Box::new( + op.or_else(move |err: DenoError| -> Result<Buf, ()> { + debug!("op err {}", err); + // No matter whether we got an Err or Ok, we want a serialized message to + // send back. So transform the DenoError into a deno_buf. + let builder = &mut FlatBufferBuilder::new(); + let errmsg_offset = builder.create_string(&format!("{}", err)); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + error: Some(errmsg_offset), + error_kind: err.kind(), + ..Default::default() + }, + )) + }).and_then(move |buf: Buf| -> Result<Buf, ()> { + // Handle empty responses. For sync responses we just want + // to send null. For async we want to send a small message + // with the cmd_id. + let buf = if is_sync || buf.len() > 0 { + buf + } else { + let builder = &mut FlatBufferBuilder::new(); + serialize_response( + cmd_id, + builder, + msg::BaseArgs { + ..Default::default() + }, + ) + }; + state.metrics_op_completed(buf.len()); + Ok(buf) + }).map_err(|err| panic!("unexpected error {:?}", err)), + ); + + debug!( + "msg_from_js {} sync {}", + msg::enum_name_any(inner_type), + base.sync() + ); + (base.sync(), boxed_op) +} + +fn op_now( + _cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let start = SystemTime::now(); + let since_the_epoch = start.duration_since(UNIX_EPOCH).unwrap(); + let time = since_the_epoch.as_secs() * 1000 + + u64::from(since_the_epoch.subsec_millis()); + + let builder = &mut FlatBufferBuilder::new(); + let inner = msg::NowRes::create(builder, &msg::NowResArgs { time }); + ok_future(serialize_response( + base.cmd_id(), + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::NowRes, + ..Default::default() + }, + )) +} + +fn op_is_tty( + _cli: &Cli, + base: &msg::Base<'_>, + _data: deno_buf, +) -> Box<OpWithError> { + let builder = &mut FlatBufferBuilder::new(); + let inner = msg::IsTTYRes::create( + builder, + &msg::IsTTYResArgs { + stdin: atty::is(atty::Stream::Stdin), + stdout: atty::is(atty::Stream::Stdout), + stderr: atty::is(atty::Stream::Stderr), + }, + ); + ok_future(serialize_response( + base.cmd_id(), + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::IsTTYRes, + ..Default::default() + }, + )) +} + +fn op_exit( + _cli: &Cli, + base: &msg::Base<'_>, + _data: deno_buf, +) -> Box<OpWithError> { + let inner = base.inner_as_exit().unwrap(); + std::process::exit(inner.code()) +} + +fn op_start( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let mut builder = FlatBufferBuilder::new(); + + let argv = cli + .state + .argv + .iter() + .map(|s| s.as_str()) + .collect::<Vec<_>>(); + let argv_off = builder.create_vector_of_strings(argv.as_slice()); + + let cwd_path = std::env::current_dir().unwrap(); + let cwd_off = + builder.create_string(deno_fs::normalize_path(cwd_path.as_ref()).as_ref()); + + let exec_path = + builder.create_string(std::env::current_exe().unwrap().to_str().unwrap()); + + let v8_version = version::v8(); + let v8_version_off = builder.create_string(v8_version); + + let deno_version = version::DENO; + let deno_version_off = builder.create_string(deno_version); + + let main_module = cli.state.main_module().map(|m| builder.create_string(&m)); + + let inner = msg::StartRes::create( + &mut builder, + &msg::StartResArgs { + cwd: Some(cwd_off), + pid: std::process::id(), + argv: Some(argv_off), + main_module, + debug_flag: cli.state.flags.log_debug, + types_flag: cli.state.flags.types, + version_flag: cli.state.flags.version, + v8_version: Some(v8_version_off), + deno_version: Some(deno_version_off), + no_color: !ansi::use_color(), + exec_path: Some(exec_path), + ..Default::default() + }, + ); + + ok_future(serialize_response( + base.cmd_id(), + &mut builder, + msg::BaseArgs { + inner_type: msg::Any::StartRes, + inner: Some(inner.as_union_value()), + ..Default::default() + }, + )) +} + +fn op_format_error( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_format_error().unwrap(); + let orig_error = String::from(inner.error().unwrap()); + + let js_error = JSError::from_v8_exception(&orig_error).unwrap(); + let js_error_mapped = apply_source_map(&js_error, &cli.state.dir); + let js_error_string = JSErrorColor(&js_error_mapped).to_string(); + + let mut builder = FlatBufferBuilder::new(); + let new_error = builder.create_string(&js_error_string); + + let inner = msg::FormatErrorRes::create( + &mut builder, + &msg::FormatErrorResArgs { + error: Some(new_error), + ..Default::default() + }, + ); + + ok_future(serialize_response( + base.cmd_id(), + &mut builder, + msg::BaseArgs { + inner_type: msg::Any::FormatErrorRes, + inner: Some(inner.as_union_value()), + ..Default::default() + }, + )) +} + +fn serialize_response( + cmd_id: u32, + builder: &mut FlatBufferBuilder<'_>, + mut args: msg::BaseArgs<'_>, +) -> Buf { + args.cmd_id = cmd_id; + let base = msg::Base::create(builder, &args); + msg::finish_base_buffer(builder, base); + let data = builder.finished_data(); + // println!("serialize_response {:x?}", data); + data.into() +} + +#[inline] +pub fn ok_future(buf: Buf) -> Box<OpWithError> { + Box::new(futures::future::ok(buf)) +} + +// Shout out to Earl Sweatshirt. +#[inline] +pub fn odd_future(err: DenoError) -> Box<OpWithError> { + Box::new(futures::future::err(err)) +} + +// https://github.com/denoland/deno/blob/golang/os.go#L100-L154 +fn op_fetch_module_meta_data( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_fetch_module_meta_data().unwrap(); + let cmd_id = base.cmd_id(); + let specifier = inner.specifier().unwrap(); + let referrer = inner.referrer().unwrap(); + + // Check for allow read since this operation could be used to read from the file system. + if !cli.permissions.allows_read() { + debug!("No read permission for fetch_module_meta_data"); + return odd_future(permission_denied()); + } + + // Check for allow write since this operation could be used to write to the file system. + if !cli.permissions.allows_write() { + debug!("No network permission for fetch_module_meta_data"); + return odd_future(permission_denied()); + } + + // Check for allow net since this operation could be used to make https/http requests. + if !cli.permissions.allows_net() { + debug!("No network permission for fetch_module_meta_data"); + return odd_future(permission_denied()); + } + + assert_eq!( + cli.state.dir.root.join("gen"), + cli.state.dir.gen, + "Sanity check" + ); + + Box::new(futures::future::result(|| -> OpResult { + let builder = &mut FlatBufferBuilder::new(); + let out = cli.state.dir.fetch_module_meta_data(specifier, referrer)?; + let data_off = builder.create_vector(out.source_code.as_slice()); + let msg_args = msg::FetchModuleMetaDataResArgs { + module_name: Some(builder.create_string(&out.module_name)), + filename: Some(builder.create_string(&out.filename)), + media_type: out.media_type, + data: Some(data_off), + }; + let inner = msg::FetchModuleMetaDataRes::create(builder, &msg_args); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::FetchModuleMetaDataRes, + ..Default::default() + }, + )) + }())) +} + +fn op_chdir( + _cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_chdir().unwrap(); + let directory = inner.directory().unwrap(); + Box::new(futures::future::result(|| -> OpResult { + std::env::set_current_dir(&directory)?; + Ok(empty_buf()) + }())) +} + +fn op_global_timer_stop( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert!(base.sync()); + assert_eq!(data.len(), 0); + let mut t = cli.state.global_timer.lock().unwrap(); + t.cancel(); + ok_future(empty_buf()) +} + +fn op_global_timer( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert!(!base.sync()); + assert_eq!(data.len(), 0); + let cmd_id = base.cmd_id(); + let inner = base.inner_as_global_timer().unwrap(); + let val = inner.timeout(); + assert!(val >= 0); + + let mut t = cli.state.global_timer.lock().unwrap(); + let deadline = Instant::now() + Duration::from_millis(val as u64); + let f = t.new_timeout(deadline); + + Box::new(f.then(move |_| { + let builder = &mut FlatBufferBuilder::new(); + let inner = + msg::GlobalTimerRes::create(builder, &msg::GlobalTimerResArgs {}); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::GlobalTimerRes, + ..Default::default() + }, + )) + })) +} + +fn op_set_env( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_set_env().unwrap(); + let key = inner.key().unwrap(); + let value = inner.value().unwrap(); + if let Err(e) = cli.check_env() { + return odd_future(e); + } + std::env::set_var(key, value); + ok_future(empty_buf()) +} + +fn op_env(cli: &Cli, base: &msg::Base<'_>, data: deno_buf) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let cmd_id = base.cmd_id(); + + if let Err(e) = cli.check_env() { + return odd_future(e); + } + + let builder = &mut FlatBufferBuilder::new(); + let vars: Vec<_> = std::env::vars() + .map(|(key, value)| msg_util::serialize_key_value(builder, &key, &value)) + .collect(); + let tables = builder.create_vector(&vars); + let inner = msg::EnvironRes::create( + builder, + &msg::EnvironResArgs { map: Some(tables) }, + ); + ok_future(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::EnvironRes, + ..Default::default() + }, + )) +} + +fn op_permissions( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let cmd_id = base.cmd_id(); + let builder = &mut FlatBufferBuilder::new(); + let inner = msg::PermissionsRes::create( + builder, + &msg::PermissionsResArgs { + run: cli.permissions.allows_run(), + read: cli.permissions.allows_read(), + write: cli.permissions.allows_write(), + net: cli.permissions.allows_net(), + env: cli.permissions.allows_env(), + }, + ); + ok_future(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::PermissionsRes, + ..Default::default() + }, + )) +} + +fn op_revoke_permission( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_permission_revoke().unwrap(); + let permission = inner.permission().unwrap(); + let result = match permission { + "run" => cli.permissions.revoke_run(), + "read" => cli.permissions.revoke_read(), + "write" => cli.permissions.revoke_write(), + "net" => cli.permissions.revoke_net(), + "env" => cli.permissions.revoke_env(), + _ => Ok(()), + }; + if let Err(e) = result { + return odd_future(e); + } + ok_future(empty_buf()) +} + +fn op_fetch( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + let inner = base.inner_as_fetch().unwrap(); + let cmd_id = base.cmd_id(); + + let header = inner.header().unwrap(); + assert!(header.is_request()); + let url = header.url().unwrap(); + + let body = if data.is_empty() { + hyper::Body::empty() + } else { + hyper::Body::from(Vec::from(&*data)) + }; + + let maybe_req = msg_util::deserialize_request(header, body); + if let Err(e) = maybe_req { + return odd_future(e); + } + let req = maybe_req.unwrap(); + + if let Err(e) = cli.check_net(url) { + return odd_future(e); + } + + let client = http_util::get_client(); + + debug!("Before fetch {}", url); + let future = + client + .request(req) + .map_err(DenoError::from) + .and_then(move |res| { + let builder = &mut FlatBufferBuilder::new(); + let header_off = msg_util::serialize_http_response(builder, &res); + let body = res.into_body(); + let body_resource = resources::add_hyper_body(body); + let inner = msg::FetchRes::create( + builder, + &msg::FetchResArgs { + header: Some(header_off), + body_rid: body_resource.rid, + }, + ); + + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::FetchRes, + ..Default::default() + }, + )) + }); + Box::new(future) +} + +// This is just type conversion. Implement From trait? +// See https://github.com/tokio-rs/tokio/blob/ffd73a64e7ec497622b7f939e38017afe7124dc4/tokio-fs/src/lib.rs#L76-L85 +fn convert_blocking<F>(f: F) -> Poll<Buf, DenoError> +where + F: FnOnce() -> DenoResult<Buf>, +{ + use futures::Async::*; + match tokio_threadpool::blocking(f) { + Ok(Ready(Ok(v))) => Ok(v.into()), + Ok(Ready(Err(err))) => Err(err), + Ok(NotReady) => Ok(NotReady), + Err(err) => panic!("blocking error {}", err), + } +} + +fn blocking<F>(is_sync: bool, f: F) -> Box<OpWithError> +where + F: 'static + Send + FnOnce() -> DenoResult<Buf>, +{ + if is_sync { + Box::new(futures::future::result(f())) + } else { + Box::new(tokio_util::poll_fn(move || convert_blocking(f))) + } +} + +fn op_make_temp_dir( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let base = Box::new(*base); + let inner = base.inner_as_make_temp_dir().unwrap(); + let cmd_id = base.cmd_id(); + + // FIXME + if let Err(e) = cli.check_write("make_temp") { + return odd_future(e); + } + + let dir = inner.dir().map(PathBuf::from); + let prefix = inner.prefix().map(String::from); + let suffix = inner.suffix().map(String::from); + + blocking(base.sync(), move || -> OpResult { + // 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 = deno_fs::make_temp_dir( + // Converting Option<String> to Option<&str> + dir.as_ref().map(|x| &**x), + prefix.as_ref().map(|x| &**x), + suffix.as_ref().map(|x| &**x), + )?; + let builder = &mut FlatBufferBuilder::new(); + let path_off = builder.create_string(path.to_str().unwrap()); + let inner = msg::MakeTempDirRes::create( + builder, + &msg::MakeTempDirResArgs { + path: Some(path_off), + }, + ); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::MakeTempDirRes, + ..Default::default() + }, + )) + }) +} + +fn op_mkdir( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_mkdir().unwrap(); + let path = String::from(inner.path().unwrap()); + let recursive = inner.recursive(); + let mode = inner.mode(); + + if let Err(e) = cli.check_write(&path) { + return odd_future(e); + } + + blocking(base.sync(), move || { + debug!("op_mkdir {}", path); + deno_fs::mkdir(Path::new(&path), mode, recursive)?; + Ok(empty_buf()) + }) +} + +fn op_chmod( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_chmod().unwrap(); + let _mode = inner.mode(); + let path = String::from(inner.path().unwrap()); + + if let Err(e) = cli.check_write(&path) { + return odd_future(e); + } + + blocking(base.sync(), move || { + debug!("op_chmod {}", &path); + let path = PathBuf::from(&path); + // Still check file/dir exists on windows + let _metadata = fs::metadata(&path)?; + // Only work in unix + #[cfg(any(unix))] + { + // We need to use underscore to compile in Windows. + #[cfg_attr( + feature = "cargo-clippy", + allow(clippy::used_underscore_binding) + )] + let mut permissions = _metadata.permissions(); + #[cfg_attr( + feature = "cargo-clippy", + allow(clippy::used_underscore_binding) + )] + permissions.set_mode(_mode); + fs::set_permissions(&path, permissions)?; + } + Ok(empty_buf()) + }) +} + +fn op_open( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let cmd_id = base.cmd_id(); + let inner = base.inner_as_open().unwrap(); + let filename_str = inner.filename().unwrap(); + let filename = PathBuf::from(&filename_str); + let mode = inner.mode().unwrap(); + + let mut open_options = tokio::fs::OpenOptions::new(); + + match mode { + "r" => { + open_options.read(true); + } + "r+" => { + open_options.read(true).write(true); + } + "w" => { + open_options.create(true).write(true).truncate(true); + } + "w+" => { + open_options + .read(true) + .create(true) + .write(true) + .truncate(true); + } + "a" => { + open_options.create(true).append(true); + } + "a+" => { + open_options.read(true).create(true).append(true); + } + "x" => { + open_options.create_new(true).write(true); + } + "x+" => { + open_options.create_new(true).read(true).write(true); + } + &_ => { + panic!("Unknown file open mode."); + } + } + + match mode { + "r" => { + if let Err(e) = cli.check_read(&filename_str) { + return odd_future(e); + } + } + "w" | "a" | "x" => { + if let Err(e) = cli.check_write(&filename_str) { + return odd_future(e); + } + } + &_ => { + if let Err(e) = cli.check_read(&filename_str) { + return odd_future(e); + } + if let Err(e) = cli.check_write(&filename_str) { + return odd_future(e); + } + } + } + + let op = open_options + .open(filename) + .map_err(DenoError::from) + .and_then(move |fs_file| -> OpResult { + let resource = resources::add_fs_file(fs_file); + let builder = &mut FlatBufferBuilder::new(); + let inner = + msg::OpenRes::create(builder, &msg::OpenResArgs { rid: resource.rid }); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::OpenRes, + ..Default::default() + }, + )) + }); + Box::new(op) +} + +fn op_close( + _cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_close().unwrap(); + let rid = inner.rid(); + match resources::lookup(rid) { + None => odd_future(errors::bad_resource()), + Some(resource) => { + resource.close(); + ok_future(empty_buf()) + } + } +} + +fn op_shutdown( + _cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_shutdown().unwrap(); + let rid = inner.rid(); + let how = inner.how(); + match resources::lookup(rid) { + None => odd_future(errors::bad_resource()), + Some(mut resource) => { + let shutdown_mode = match how { + 0 => Shutdown::Read, + 1 => Shutdown::Write, + _ => unimplemented!(), + }; + blocking(base.sync(), move || { + // Use UFCS for disambiguation + Resource::shutdown(&mut resource, shutdown_mode)?; + Ok(empty_buf()) + }) + } + } +} + +fn op_read( + _cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + let cmd_id = base.cmd_id(); + let inner = base.inner_as_read().unwrap(); + let rid = inner.rid(); + + match resources::lookup(rid) { + None => odd_future(errors::bad_resource()), + Some(resource) => { + let op = tokio::io::read(resource, data) + .map_err(DenoError::from) + .and_then(move |(_resource, _buf, nread)| { + let builder = &mut FlatBufferBuilder::new(); + let inner = msg::ReadRes::create( + builder, + &msg::ReadResArgs { + nread: nread as u32, + eof: nread == 0, + }, + ); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::ReadRes, + ..Default::default() + }, + )) + }); + Box::new(op) + } + } +} + +fn op_write( + _cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + let cmd_id = base.cmd_id(); + let inner = base.inner_as_write().unwrap(); + let rid = inner.rid(); + + match resources::lookup(rid) { + None => odd_future(errors::bad_resource()), + Some(resource) => { + let op = tokio_write::write(resource, data) + .map_err(DenoError::from) + .and_then(move |(_resource, _buf, nwritten)| { + let builder = &mut FlatBufferBuilder::new(); + let inner = msg::WriteRes::create( + builder, + &msg::WriteResArgs { + nbyte: nwritten as u32, + }, + ); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::WriteRes, + ..Default::default() + }, + )) + }); + Box::new(op) + } + } +} + +fn op_seek( + _cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let _cmd_id = base.cmd_id(); + let inner = base.inner_as_seek().unwrap(); + let rid = inner.rid(); + let offset = inner.offset(); + let whence = inner.whence(); + + match resources::lookup(rid) { + None => odd_future(errors::bad_resource()), + Some(resource) => { + let op = resources::seek(resource, offset, whence) + .and_then(move |_| Ok(empty_buf())); + Box::new(op) + } + } +} + +fn op_remove( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_remove().unwrap(); + let path_ = inner.path().unwrap(); + let path = PathBuf::from(path_); + let recursive = inner.recursive(); + + if let Err(e) = cli.check_write(path.to_str().unwrap()) { + return odd_future(e); + } + + blocking(base.sync(), move || { + debug!("op_remove {}", path.display()); + let metadata = fs::metadata(&path)?; + if metadata.is_file() { + fs::remove_file(&path)?; + } else if recursive { + remove_dir_all(&path)?; + } else { + fs::remove_dir(&path)?; + } + Ok(empty_buf()) + }) +} + +// Prototype https://github.com/denoland/deno/blob/golang/os.go#L171-L184 +fn op_read_file( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_read_file().unwrap(); + let cmd_id = base.cmd_id(); + let filename_ = inner.filename().unwrap(); + let filename = PathBuf::from(filename_); + debug!("op_read_file {}", filename.display()); + if let Err(e) = cli.check_read(&filename_) { + return odd_future(e); + } + blocking(base.sync(), move || { + let vec = fs::read(&filename)?; + // Build the response message. memcpy data into inner. + // TODO(ry) zero-copy. + let builder = &mut FlatBufferBuilder::new(); + let data_off = builder.create_vector(vec.as_slice()); + let inner = msg::ReadFileRes::create( + builder, + &msg::ReadFileResArgs { + data: Some(data_off), + }, + ); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::ReadFileRes, + ..Default::default() + }, + )) + }) +} + +fn op_copy_file( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_copy_file().unwrap(); + let from_ = inner.from().unwrap(); + let from = PathBuf::from(from_); + let to_ = inner.to().unwrap(); + let to = PathBuf::from(to_); + + if let Err(e) = cli.check_read(&from_) { + return odd_future(e); + } + if let Err(e) = cli.check_write(&to_) { + return odd_future(e); + } + + debug!("op_copy_file {} {}", from.display(), to.display()); + blocking(base.sync(), move || { + // On *nix, Rust deem non-existent path as invalid input + // See https://github.com/rust-lang/rust/issues/54800 + // Once the issue is reolved, we should remove this workaround. + if cfg!(unix) && !from.is_file() { + return Err(errors::new( + ErrorKind::NotFound, + "File not found".to_string(), + )); + } + + fs::copy(&from, &to)?; + Ok(empty_buf()) + }) +} + +macro_rules! to_seconds { + ($time:expr) => {{ + // Unwrap is safe here as if the file is before the unix epoch + // something is very wrong. + $time + .and_then(|t| Ok(t.duration_since(UNIX_EPOCH).unwrap().as_secs())) + .unwrap_or(0) + }}; +} + +#[cfg(any(unix))] +fn get_mode(perm: &fs::Permissions) -> u32 { + perm.mode() +} + +#[cfg(not(any(unix)))] +fn get_mode(_perm: &fs::Permissions) -> u32 { + 0 +} + +fn op_cwd( + _cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let cmd_id = base.cmd_id(); + Box::new(futures::future::result(|| -> OpResult { + let path = std::env::current_dir()?; + let builder = &mut FlatBufferBuilder::new(); + let cwd = + builder.create_string(&path.into_os_string().into_string().unwrap()); + let inner = + msg::CwdRes::create(builder, &msg::CwdResArgs { cwd: Some(cwd) }); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::CwdRes, + ..Default::default() + }, + )) + }())) +} + +fn op_stat( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_stat().unwrap(); + let cmd_id = base.cmd_id(); + let filename_ = inner.filename().unwrap(); + let filename = PathBuf::from(filename_); + let lstat = inner.lstat(); + + if let Err(e) = cli.check_read(&filename_) { + return odd_future(e); + } + + blocking(base.sync(), move || { + let builder = &mut FlatBufferBuilder::new(); + debug!("op_stat {} {}", filename.display(), lstat); + let metadata = if lstat { + fs::symlink_metadata(&filename)? + } else { + fs::metadata(&filename)? + }; + + let inner = msg::StatRes::create( + builder, + &msg::StatResArgs { + is_file: metadata.is_file(), + is_symlink: metadata.file_type().is_symlink(), + len: metadata.len(), + modified: to_seconds!(metadata.modified()), + accessed: to_seconds!(metadata.accessed()), + created: to_seconds!(metadata.created()), + mode: get_mode(&metadata.permissions()), + has_mode: cfg!(target_family = "unix"), + ..Default::default() + }, + ); + + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::StatRes, + ..Default::default() + }, + )) + }) +} + +fn op_read_dir( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_read_dir().unwrap(); + let cmd_id = base.cmd_id(); + let path = String::from(inner.path().unwrap()); + + if let Err(e) = cli.check_read(&path) { + return odd_future(e); + } + + blocking(base.sync(), move || -> OpResult { + debug!("op_read_dir {}", path); + let builder = &mut FlatBufferBuilder::new(); + let entries: Vec<_> = fs::read_dir(Path::new(&path))? + .map(|entry| { + let entry = entry.unwrap(); + let metadata = entry.metadata().unwrap(); + let file_type = metadata.file_type(); + let name = builder.create_string(entry.file_name().to_str().unwrap()); + let path = builder.create_string(entry.path().to_str().unwrap()); + + msg::StatRes::create( + builder, + &msg::StatResArgs { + is_file: file_type.is_file(), + is_symlink: file_type.is_symlink(), + len: metadata.len(), + modified: to_seconds!(metadata.modified()), + accessed: to_seconds!(metadata.accessed()), + created: to_seconds!(metadata.created()), + name: Some(name), + path: Some(path), + mode: get_mode(&metadata.permissions()), + has_mode: cfg!(target_family = "unix"), + }, + ) + }).collect(); + + let entries = builder.create_vector(&entries); + let inner = msg::ReadDirRes::create( + builder, + &msg::ReadDirResArgs { + entries: Some(entries), + }, + ); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::ReadDirRes, + ..Default::default() + }, + )) + }) +} + +fn op_write_file( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + let inner = base.inner_as_write_file().unwrap(); + let filename = String::from(inner.filename().unwrap()); + let update_perm = inner.update_perm(); + let perm = inner.perm(); + let is_create = inner.is_create(); + let is_append = inner.is_append(); + + if let Err(e) = cli.check_write(&filename) { + return odd_future(e); + } + + blocking(base.sync(), move || -> OpResult { + debug!("op_write_file {} {}", filename, data.len()); + deno_fs::write_file_2( + Path::new(&filename), + data, + update_perm, + perm, + is_create, + is_append, + )?; + Ok(empty_buf()) + }) +} + +fn op_rename( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_rename().unwrap(); + let oldpath = PathBuf::from(inner.oldpath().unwrap()); + let newpath_ = inner.newpath().unwrap(); + let newpath = PathBuf::from(newpath_); + if let Err(e) = cli.check_write(&newpath_) { + return odd_future(e); + } + blocking(base.sync(), move || -> OpResult { + debug!("op_rename {} {}", oldpath.display(), newpath.display()); + fs::rename(&oldpath, &newpath)?; + Ok(empty_buf()) + }) +} + +fn op_symlink( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_symlink().unwrap(); + let oldname = PathBuf::from(inner.oldname().unwrap()); + let newname_ = inner.newname().unwrap(); + let newname = PathBuf::from(newname_); + + if let Err(e) = cli.check_write(&newname_) { + return odd_future(e); + } + // TODO Use type for Windows. + if cfg!(windows) { + return odd_future(errors::new( + ErrorKind::Other, + "Not implemented".to_string(), + )); + } + blocking(base.sync(), move || -> OpResult { + debug!("op_symlink {} {}", oldname.display(), newname.display()); + #[cfg(any(unix))] + std::os::unix::fs::symlink(&oldname, &newname)?; + Ok(empty_buf()) + }) +} + +fn op_read_link( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_readlink().unwrap(); + let cmd_id = base.cmd_id(); + let name_ = inner.name().unwrap(); + let name = PathBuf::from(name_); + + if let Err(e) = cli.check_read(&name_) { + return odd_future(e); + } + + blocking(base.sync(), move || -> OpResult { + debug!("op_read_link {}", name.display()); + let path = fs::read_link(&name)?; + let builder = &mut FlatBufferBuilder::new(); + let path_off = builder.create_string(path.to_str().unwrap()); + let inner = msg::ReadlinkRes::create( + builder, + &msg::ReadlinkResArgs { + path: Some(path_off), + }, + ); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::ReadlinkRes, + ..Default::default() + }, + )) + }) +} + +fn op_repl_start( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_repl_start().unwrap(); + let cmd_id = base.cmd_id(); + let history_file = String::from(inner.history_file().unwrap()); + + debug!("op_repl_start {}", history_file); + let history_path = repl::history_path(&cli.state.dir, &history_file); + let repl = repl::Repl::new(history_path); + let resource = resources::add_repl(repl); + + let builder = &mut FlatBufferBuilder::new(); + let inner = msg::ReplStartRes::create( + builder, + &msg::ReplStartResArgs { rid: resource.rid }, + ); + ok_future(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::ReplStartRes, + ..Default::default() + }, + )) +} + +fn op_repl_readline( + _cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let inner = base.inner_as_repl_readline().unwrap(); + let cmd_id = base.cmd_id(); + let rid = inner.rid(); + let prompt = inner.prompt().unwrap().to_owned(); + debug!("op_repl_readline {} {}", rid, prompt); + + blocking(base.sync(), move || -> OpResult { + let repl = resources::get_repl(rid)?; + let line = repl.lock().unwrap().readline(&prompt)?; + + let builder = &mut FlatBufferBuilder::new(); + let line_off = builder.create_string(&line); + let inner = msg::ReplReadlineRes::create( + builder, + &msg::ReplReadlineResArgs { + line: Some(line_off), + }, + ); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::ReplReadlineRes, + ..Default::default() + }, + )) + }) +} + +fn op_truncate( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + + let inner = base.inner_as_truncate().unwrap(); + let filename = String::from(inner.name().unwrap()); + let len = inner.len(); + + if let Err(e) = cli.check_write(&filename) { + return odd_future(e); + } + + blocking(base.sync(), move || { + debug!("op_truncate {} {}", filename, len); + let f = fs::OpenOptions::new().write(true).open(&filename)?; + f.set_len(u64::from(len))?; + Ok(empty_buf()) + }) +} + +fn op_listen( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + if let Err(e) = cli.check_net("listen") { + return odd_future(e); + } + + let cmd_id = base.cmd_id(); + let inner = base.inner_as_listen().unwrap(); + let network = inner.network().unwrap(); + assert_eq!(network, "tcp"); + let address = inner.address().unwrap(); + + Box::new(futures::future::result((move || { + let addr = resolve_addr(address).wait()?; + + let listener = TcpListener::bind(&addr)?; + let resource = resources::add_tcp_listener(listener); + + let builder = &mut FlatBufferBuilder::new(); + let inner = msg::ListenRes::create( + builder, + &msg::ListenResArgs { rid: resource.rid }, + ); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::ListenRes, + ..Default::default() + }, + )) + })())) +} + +fn new_conn(cmd_id: u32, tcp_stream: TcpStream) -> OpResult { + let tcp_stream_resource = resources::add_tcp_stream(tcp_stream); + // TODO forward socket_addr to client. + + let builder = &mut FlatBufferBuilder::new(); + let inner = msg::NewConn::create( + builder, + &msg::NewConnArgs { + rid: tcp_stream_resource.rid, + ..Default::default() + }, + ); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::NewConn, + ..Default::default() + }, + )) +} + +fn op_accept( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + if let Err(e) = cli.check_net("accept") { + return odd_future(e); + } + let cmd_id = base.cmd_id(); + let inner = base.inner_as_accept().unwrap(); + let server_rid = inner.rid(); + + match resources::lookup(server_rid) { + None => odd_future(errors::bad_resource()), + Some(server_resource) => { + let op = tokio_util::accept(server_resource) + .map_err(DenoError::from) + .and_then(move |(tcp_stream, _socket_addr)| { + new_conn(cmd_id, tcp_stream) + }); + Box::new(op) + } + } +} + +fn op_dial( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + if let Err(e) = cli.check_net("dial") { + return odd_future(e); + } + let cmd_id = base.cmd_id(); + let inner = base.inner_as_dial().unwrap(); + let network = inner.network().unwrap(); + assert_eq!(network, "tcp"); // TODO Support others. + let address = inner.address().unwrap(); + + let op = + resolve_addr(address) + .map_err(DenoError::from) + .and_then(move |addr| { + TcpStream::connect(&addr) + .map_err(DenoError::from) + .and_then(move |tcp_stream| new_conn(cmd_id, tcp_stream)) + }); + Box::new(op) +} + +fn op_metrics( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let cmd_id = base.cmd_id(); + + let builder = &mut FlatBufferBuilder::new(); + let inner = msg::MetricsRes::create( + builder, + &msg::MetricsResArgs::from(&cli.state.metrics), + ); + ok_future(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::MetricsRes, + ..Default::default() + }, + )) +} + +fn op_resources( + _cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let cmd_id = base.cmd_id(); + + let builder = &mut FlatBufferBuilder::new(); + let serialized_resources = table_entries(); + + let res: Vec<_> = serialized_resources + .iter() + .map(|(key, value)| { + let repr = builder.create_string(value); + + msg::Resource::create( + builder, + &msg::ResourceArgs { + rid: *key, + repr: Some(repr), + }, + ) + }).collect(); + + let resources = builder.create_vector(&res); + let inner = msg::ResourcesRes::create( + builder, + &msg::ResourcesResArgs { + resources: Some(resources), + }, + ); + + ok_future(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::ResourcesRes, + ..Default::default() + }, + )) +} + +fn subprocess_stdio_map(v: msg::ProcessStdio) -> std::process::Stdio { + match v { + msg::ProcessStdio::Inherit => std::process::Stdio::inherit(), + msg::ProcessStdio::Piped => std::process::Stdio::piped(), + msg::ProcessStdio::Null => std::process::Stdio::null(), + } +} + +fn op_run(cli: &Cli, base: &msg::Base<'_>, data: deno_buf) -> Box<OpWithError> { + assert!(base.sync()); + let cmd_id = base.cmd_id(); + + if let Err(e) = cli.check_run() { + return odd_future(e); + } + + assert_eq!(data.len(), 0); + let inner = base.inner_as_run().unwrap(); + let args = inner.args().unwrap(); + let env = inner.env().unwrap(); + let cwd = inner.cwd(); + + let mut c = Command::new(args.get(0)); + (1..args.len()).for_each(|i| { + let arg = args.get(i); + c.arg(arg); + }); + cwd.map(|d| c.current_dir(d)); + (0..env.len()).for_each(|i| { + let entry = env.get(i); + c.env(entry.key().unwrap(), entry.value().unwrap()); + }); + + c.stdin(subprocess_stdio_map(inner.stdin())); + c.stdout(subprocess_stdio_map(inner.stdout())); + c.stderr(subprocess_stdio_map(inner.stderr())); + + // Spawn the command. + let child = match c.spawn_async() { + Ok(v) => v, + Err(err) => { + return odd_future(err.into()); + } + }; + + let pid = child.id(); + let resources = resources::add_child(child); + + let mut res_args = msg::RunResArgs { + rid: resources.child_rid, + pid, + ..Default::default() + }; + + if let Some(stdin_rid) = resources.stdin_rid { + res_args.stdin_rid = stdin_rid; + } + if let Some(stdout_rid) = resources.stdout_rid { + res_args.stdout_rid = stdout_rid; + } + if let Some(stderr_rid) = resources.stderr_rid { + res_args.stderr_rid = stderr_rid; + } + + let builder = &mut FlatBufferBuilder::new(); + let inner = msg::RunRes::create(builder, &res_args); + ok_future(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::RunRes, + ..Default::default() + }, + )) +} + +fn op_run_status( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let cmd_id = base.cmd_id(); + let inner = base.inner_as_run_status().unwrap(); + let rid = inner.rid(); + + if let Err(e) = cli.check_run() { + return odd_future(e); + } + + let future = match resources::child_status(rid) { + Err(e) => { + return odd_future(e); + } + Ok(f) => f, + }; + + let future = future.and_then(move |run_status| { + 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(); + + let builder = &mut FlatBufferBuilder::new(); + let inner = msg::RunStatusRes::create( + builder, + &msg::RunStatusResArgs { + got_signal, + exit_code: code.unwrap_or(-1), + exit_signal: signal.unwrap_or(-1), + }, + ); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::RunStatusRes, + ..Default::default() + }, + )) + }); + Box::new(future) +} + +struct GetMessageFuture { + pub state: Arc<IsolateState>, +} + +impl Future for GetMessageFuture { + type Item = Option<Buf>; + type Error = (); + + fn poll(&mut self) -> Result<Async<Self::Item>, Self::Error> { + assert!(self.state.worker_channels.is_some()); + match self.state.worker_channels { + None => panic!("expected worker_channels"), + Some(ref wc) => { + let mut wc = wc.lock().unwrap(); + wc.1.poll() + } + } + } +} + +fn op_worker_get_message( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + assert_eq!(data.len(), 0); + let cmd_id = base.cmd_id(); + + let op = GetMessageFuture { + state: cli.state.clone(), + }; + let op = op.map_err(move |_| -> DenoError { unimplemented!() }); + let op = op.and_then(move |maybe_buf| -> DenoResult<Buf> { + debug!("op_worker_get_message"); + let builder = &mut FlatBufferBuilder::new(); + + let data = maybe_buf.as_ref().map(|buf| builder.create_vector(buf)); + let inner = msg::WorkerGetMessageRes::create( + builder, + &msg::WorkerGetMessageResArgs { data }, + ); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::WorkerGetMessageRes, + ..Default::default() + }, + )) + }); + Box::new(op) +} + +fn op_worker_post_message( + cli: &Cli, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box<OpWithError> { + let cmd_id = base.cmd_id(); + + let d = Vec::from(data.as_ref()).into_boxed_slice(); + + assert!(cli.state.worker_channels.is_some()); + let tx = match cli.state.worker_channels { + None => panic!("expected worker_channels"), + Some(ref wc) => { + let wc = wc.lock().unwrap(); + wc.0.clone() + } + }; + let op = tx.send(d); + let op = op.map_err(|e| errors::new(ErrorKind::Other, e.to_string())); + let op = op.and_then(move |_| -> DenoResult<Buf> { + let builder = &mut FlatBufferBuilder::new(); + + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + ..Default::default() + }, + )) + }); + Box::new(op) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cli::Cli; + use crate::isolate_state::IsolateState; + use crate::permissions::{DenoPermissions, PermissionAccessor}; + + #[test] + fn fetch_module_meta_fails_without_read() { + let state = Arc::new(IsolateState::mock()); + let permissions = DenoPermissions { + allow_write: PermissionAccessor::from(true), + allow_env: PermissionAccessor::from(true), + allow_net: PermissionAccessor::from(true), + allow_run: PermissionAccessor::from(true), + ..Default::default() + }; + let cli = Cli::new(None, state, permissions); + let builder = &mut FlatBufferBuilder::new(); + let fetch_msg_args = msg::FetchModuleMetaDataArgs { + specifier: Some(builder.create_string("./somefile")), + referrer: Some(builder.create_string(".")), + }; + let inner = msg::FetchModuleMetaData::create(builder, &fetch_msg_args); + let base_args = msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::FetchModuleMetaData, + ..Default::default() + }; + let base = msg::Base::create(builder, &base_args); + msg::finish_base_buffer(builder, base); + let data = builder.finished_data(); + let final_msg = msg::get_root_as_base(&data); + let fetch_result = + op_fetch_module_meta_data(&cli, &final_msg, deno_buf::empty()).wait(); + match fetch_result { + Ok(_) => assert!(true), + Err(e) => assert_eq!(e.to_string(), permission_denied().to_string()), + } + } + + #[test] + fn fetch_module_meta_fails_without_write() { + let state = Arc::new(IsolateState::mock()); + let permissions = DenoPermissions { + allow_read: PermissionAccessor::from(true), + allow_env: PermissionAccessor::from(true), + allow_net: PermissionAccessor::from(true), + allow_run: PermissionAccessor::from(true), + ..Default::default() + }; + let cli = Cli::new(None, state, permissions); + let builder = &mut FlatBufferBuilder::new(); + let fetch_msg_args = msg::FetchModuleMetaDataArgs { + specifier: Some(builder.create_string("./somefile")), + referrer: Some(builder.create_string(".")), + }; + let inner = msg::FetchModuleMetaData::create(builder, &fetch_msg_args); + let base_args = msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::FetchModuleMetaData, + ..Default::default() + }; + let base = msg::Base::create(builder, &base_args); + msg::finish_base_buffer(builder, base); + let data = builder.finished_data(); + let final_msg = msg::get_root_as_base(&data); + let fetch_result = + op_fetch_module_meta_data(&cli, &final_msg, deno_buf::empty()).wait(); + match fetch_result { + Ok(_) => assert!(true), + Err(e) => assert_eq!(e.to_string(), permission_denied().to_string()), + } + } + + #[test] + fn fetch_module_meta_fails_without_net() { + let state = Arc::new(IsolateState::mock()); + let permissions = DenoPermissions { + allow_read: PermissionAccessor::from(true), + allow_write: PermissionAccessor::from(true), + allow_env: PermissionAccessor::from(true), + allow_run: PermissionAccessor::from(true), + ..Default::default() + }; + let cli = Cli::new(None, state, permissions); + let builder = &mut FlatBufferBuilder::new(); + let fetch_msg_args = msg::FetchModuleMetaDataArgs { + specifier: Some(builder.create_string("./somefile")), + referrer: Some(builder.create_string(".")), + }; + let inner = msg::FetchModuleMetaData::create(builder, &fetch_msg_args); + let base_args = msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::FetchModuleMetaData, + ..Default::default() + }; + let base = msg::Base::create(builder, &base_args); + msg::finish_base_buffer(builder, base); + let data = builder.finished_data(); + let final_msg = msg::get_root_as_base(&data); + let fetch_result = + op_fetch_module_meta_data(&cli, &final_msg, deno_buf::empty()).wait(); + match fetch_result { + Ok(_) => assert!(true), + Err(e) => assert_eq!(e.to_string(), permission_denied().to_string()), + } + } + + #[test] + fn fetch_module_meta_not_permission_denied_with_permissions() { + let state = Arc::new(IsolateState::mock()); + let permissions = DenoPermissions { + allow_read: PermissionAccessor::from(true), + allow_write: PermissionAccessor::from(true), + allow_net: PermissionAccessor::from(true), + ..Default::default() + }; + let cli = Cli::new(None, state, permissions); + let builder = &mut FlatBufferBuilder::new(); + let fetch_msg_args = msg::FetchModuleMetaDataArgs { + specifier: Some(builder.create_string("./somefile")), + referrer: Some(builder.create_string(".")), + }; + let inner = msg::FetchModuleMetaData::create(builder, &fetch_msg_args); + let base_args = msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::FetchModuleMetaData, + ..Default::default() + }; + let base = msg::Base::create(builder, &base_args); + msg::finish_base_buffer(builder, base); + let data = builder.finished_data(); + let final_msg = msg::get_root_as_base(&data); + let fetch_result = + op_fetch_module_meta_data(&cli, &final_msg, deno_buf::empty()).wait(); + match fetch_result { + Ok(_) => assert!(true), + Err(e) => assert!(e.to_string() != permission_denied().to_string()), + } + } +} diff --git a/cli/permissions.rs b/cli/permissions.rs new file mode 100644 index 000000000..9093c14f0 --- /dev/null +++ b/cli/permissions.rs @@ -0,0 +1,343 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use atty; + +use crate::flags::DenoFlags; + +use ansi_term::Style; +use crate::errors::permission_denied; +use crate::errors::DenoResult; +use std::fmt; +use std::io; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::Arc; + +/// Tri-state value for storing permission state +pub enum PermissionAccessorState { + Allow = 0, + Ask = 1, + Deny = 2, +} + +impl From<usize> for PermissionAccessorState { + fn from(val: usize) -> Self { + match val { + 0 => PermissionAccessorState::Allow, + 1 => PermissionAccessorState::Ask, + 2 => PermissionAccessorState::Deny, + _ => unreachable!(), + } + } +} + +impl From<bool> for PermissionAccessorState { + fn from(val: bool) -> Self { + match val { + true => PermissionAccessorState::Allow, + false => PermissionAccessorState::Ask, + } + } +} + +impl fmt::Display for PermissionAccessorState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PermissionAccessorState::Allow => f.pad("Allow"), + PermissionAccessorState::Ask => f.pad("Ask"), + PermissionAccessorState::Deny => f.pad("Deny"), + } + } +} + +#[derive(Debug)] +pub struct PermissionAccessor { + state: Arc<AtomicUsize>, +} + +impl PermissionAccessor { + pub fn new(state: PermissionAccessorState) -> Self { + Self { + state: Arc::new(AtomicUsize::new(state as usize)), + } + } + + pub fn is_allow(&self) -> bool { + match self.get_state() { + PermissionAccessorState::Allow => true, + _ => false, + } + } + + /// If the state is "Allow" walk it back to the default "Ask" + /// Don't do anything if state is "Deny" + pub fn revoke(&self) { + if self.is_allow() { + self.ask(); + } + } + + pub fn allow(&self) { + self.set_state(PermissionAccessorState::Allow) + } + + pub fn ask(&self) { + self.set_state(PermissionAccessorState::Ask) + } + + pub fn deny(&self) { + self.set_state(PermissionAccessorState::Deny) + } + + /// Update this accessors state based on a PromptResult value + /// This will only update the state if the PromptResult value + /// is one of the "Always" values + pub fn update_with_prompt_result(&self, prompt_result: &PromptResult) { + match prompt_result { + PromptResult::AllowAlways => self.allow(), + PromptResult::DenyAlways => self.deny(), + _ => {} + } + } + + #[inline] + pub fn get_state(&self) -> PermissionAccessorState { + self.state.load(Ordering::SeqCst).into() + } + fn set_state(&self, state: PermissionAccessorState) { + self.state.store(state as usize, Ordering::SeqCst) + } +} + +impl From<bool> for PermissionAccessor { + fn from(val: bool) -> Self { + Self::new(PermissionAccessorState::from(val)) + } +} + +impl Default for PermissionAccessor { + fn default() -> Self { + Self { + state: Arc::new(AtomicUsize::new(PermissionAccessorState::Ask as usize)), + } + } +} + +#[cfg_attr(feature = "cargo-clippy", allow(stutter))] +#[derive(Debug, Default)] +pub struct DenoPermissions { + // Keep in sync with src/permissions.ts + pub allow_read: PermissionAccessor, + pub allow_write: PermissionAccessor, + pub allow_net: PermissionAccessor, + pub allow_env: PermissionAccessor, + pub allow_run: PermissionAccessor, + pub no_prompts: AtomicBool, +} + +impl DenoPermissions { + pub fn from_flags(flags: &DenoFlags) -> Self { + Self { + allow_read: PermissionAccessor::from(flags.allow_read), + allow_write: PermissionAccessor::from(flags.allow_write), + allow_env: PermissionAccessor::from(flags.allow_env), + allow_net: PermissionAccessor::from(flags.allow_net), + allow_run: PermissionAccessor::from(flags.allow_run), + no_prompts: AtomicBool::new(flags.no_prompts), + } + } + + pub fn check_run(&self) -> DenoResult<()> { + match self.allow_run.get_state() { + PermissionAccessorState::Allow => Ok(()), + PermissionAccessorState::Ask => { + match self.try_permissions_prompt("access to run a subprocess") { + Err(e) => Err(e), + Ok(v) => { + self.allow_run.update_with_prompt_result(&v); + v.check()?; + Ok(()) + } + } + } + PermissionAccessorState::Deny => Err(permission_denied()), + } + } + + pub fn check_read(&self, filename: &str) -> DenoResult<()> { + match self.allow_read.get_state() { + PermissionAccessorState::Allow => Ok(()), + PermissionAccessorState::Ask => match self + .try_permissions_prompt(&format!("read access to \"{}\"", filename)) + { + Err(e) => Err(e), + Ok(v) => { + self.allow_read.update_with_prompt_result(&v); + v.check()?; + Ok(()) + } + }, + PermissionAccessorState::Deny => Err(permission_denied()), + } + } + + pub fn check_write(&self, filename: &str) -> DenoResult<()> { + match self.allow_write.get_state() { + PermissionAccessorState::Allow => Ok(()), + PermissionAccessorState::Ask => match self + .try_permissions_prompt(&format!("write access to \"{}\"", filename)) + { + Err(e) => Err(e), + Ok(v) => { + self.allow_write.update_with_prompt_result(&v); + v.check()?; + Ok(()) + } + }, + PermissionAccessorState::Deny => Err(permission_denied()), + } + } + + pub fn check_net(&self, domain_name: &str) -> DenoResult<()> { + match self.allow_net.get_state() { + PermissionAccessorState::Allow => Ok(()), + PermissionAccessorState::Ask => match self.try_permissions_prompt( + &format!("network access to \"{}\"", domain_name), + ) { + Err(e) => Err(e), + Ok(v) => { + self.allow_net.update_with_prompt_result(&v); + v.check()?; + Ok(()) + } + }, + PermissionAccessorState::Deny => Err(permission_denied()), + } + } + + pub fn check_env(&self) -> DenoResult<()> { + match self.allow_env.get_state() { + PermissionAccessorState::Allow => Ok(()), + PermissionAccessorState::Ask => { + match self.try_permissions_prompt("access to environment variables") { + Err(e) => Err(e), + Ok(v) => { + self.allow_env.update_with_prompt_result(&v); + v.check()?; + Ok(()) + } + } + } + PermissionAccessorState::Deny => Err(permission_denied()), + } + } + + /// Try to present the user with a permission prompt + /// will error with permission_denied if no_prompts is enabled + fn try_permissions_prompt(&self, message: &str) -> DenoResult<PromptResult> { + if self.no_prompts.load(Ordering::SeqCst) { + return Err(permission_denied()); + } + if !atty::is(atty::Stream::Stdin) || !atty::is(atty::Stream::Stderr) { + return Err(permission_denied()); + }; + permission_prompt(message) + } + + pub fn allows_run(&self) -> bool { + return self.allow_run.is_allow(); + } + + pub fn allows_read(&self) -> bool { + return self.allow_read.is_allow(); + } + + pub fn allows_write(&self) -> bool { + return self.allow_write.is_allow(); + } + + pub fn allows_net(&self) -> bool { + return self.allow_net.is_allow(); + } + + pub fn allows_env(&self) -> bool { + return self.allow_env.is_allow(); + } + + pub fn revoke_run(&self) -> DenoResult<()> { + self.allow_run.revoke(); + return Ok(()); + } + + pub fn revoke_read(&self) -> DenoResult<()> { + self.allow_read.revoke(); + return Ok(()); + } + + pub fn revoke_write(&self) -> DenoResult<()> { + self.allow_write.revoke(); + return Ok(()); + } + + pub fn revoke_net(&self) -> DenoResult<()> { + self.allow_net.revoke(); + return Ok(()); + } + + pub fn revoke_env(&self) -> DenoResult<()> { + self.allow_env.revoke(); + return Ok(()); + } +} + +/// Quad-state value for representing user input on permission prompt +#[derive(Debug, Clone)] +pub enum PromptResult { + AllowAlways = 0, + AllowOnce = 1, + DenyOnce = 2, + DenyAlways = 3, +} + +impl PromptResult { + /// If value is any form of deny this will error with permission_denied + pub fn check(&self) -> DenoResult<()> { + match self { + PromptResult::DenyOnce => Err(permission_denied()), + PromptResult::DenyAlways => Err(permission_denied()), + _ => Ok(()), + } + } +} + +impl fmt::Display for PromptResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PromptResult::AllowAlways => f.pad("AllowAlways"), + PromptResult::AllowOnce => f.pad("AllowOnce"), + PromptResult::DenyOnce => f.pad("DenyOnce"), + PromptResult::DenyAlways => f.pad("DenyAlways"), + } + } +} + +fn permission_prompt(message: &str) -> DenoResult<PromptResult> { + let msg = format!("⚠️ Deno requests {}. Grant? [a/y/n/d (a = allow always, y = allow once, n = deny once, d = deny always)] ", message); + // print to stderr so that if deno is > to a file this is still displayed. + eprint!("{}", Style::new().bold().paint(msg)); + loop { + let mut input = String::new(); + let stdin = io::stdin(); + let _nread = stdin.read_line(&mut input)?; + let ch = input.chars().next().unwrap(); + match ch.to_ascii_lowercase() { + 'a' => return Ok(PromptResult::AllowAlways), + 'y' => return Ok(PromptResult::AllowOnce), + 'n' => return Ok(PromptResult::DenyOnce), + 'd' => return Ok(PromptResult::DenyAlways), + _ => { + // If we don't get a recognized option try again. + let msg_again = format!("Unrecognized option '{}' [a/y/n/d (a = allow always, y = allow once, n = deny once, d = deny always)] ", ch); + eprint!("{}", Style::new().bold().paint(msg_again)); + } + }; + } +} diff --git a/cli/repl.rs b/cli/repl.rs new file mode 100644 index 000000000..55bf4a114 --- /dev/null +++ b/cli/repl.rs @@ -0,0 +1,114 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use rustyline; + +use crate::msg::ErrorKind; +use std::error::Error; + +use crate::deno_dir::DenoDir; +use crate::errors::new as deno_error; +use crate::errors::DenoResult; +use std::path::PathBuf; + +#[cfg(not(windows))] +use rustyline::Editor; + +// Work around the issue that on Windows, `struct Editor` does not implement the +// `Send` trait, because it embeds a windows HANDLE which is a type alias for +// *mut c_void. This value isn't actually a pointer and there's nothing that +// can be mutated through it, so hack around it. TODO: a prettier solution. +#[cfg(windows)] +use std::ops::{Deref, DerefMut}; + +#[cfg(windows)] +struct Editor<T: rustyline::Helper> { + inner: rustyline::Editor<T>, +} + +#[cfg(windows)] +unsafe impl<T: rustyline::Helper> Send for Editor<T> {} + +#[cfg(windows)] +impl<T: rustyline::Helper> Editor<T> { + pub fn new() -> Editor<T> { + Editor { + inner: rustyline::Editor::<T>::new(), + } + } +} + +#[cfg(windows)] +impl<T: rustyline::Helper> Deref for Editor<T> { + type Target = rustyline::Editor<T>; + + fn deref(&self) -> &rustyline::Editor<T> { + &self.inner + } +} + +#[cfg(windows)] +impl<T: rustyline::Helper> DerefMut for Editor<T> { + fn deref_mut(&mut self) -> &mut rustyline::Editor<T> { + &mut self.inner + } +} + +pub struct Repl { + editor: Editor<()>, + history_file: PathBuf, +} + +impl Repl { + pub fn new(history_file: PathBuf) -> Self { + let mut repl = Self { + editor: Editor::<()>::new(), + history_file, + }; + + repl.load_history(); + repl + } + + fn load_history(&mut self) { + debug!("Loading REPL history: {:?}", self.history_file); + self + .editor + .load_history(&self.history_file.to_str().unwrap()) + .map_err(|e| debug!("Unable to load history file: {:?} {}", self.history_file, e)) + // ignore this error (e.g. it occurs on first load) + .unwrap_or(()) + } + + fn save_history(&mut self) -> DenoResult<()> { + self + .editor + .save_history(&self.history_file.to_str().unwrap()) + .map(|_| debug!("Saved REPL history to: {:?}", self.history_file)) + .map_err(|e| { + eprintln!("Unable to save REPL history: {:?} {}", self.history_file, e); + deno_error(ErrorKind::Other, e.description().to_string()) + }) + } + + pub fn readline(&mut self, prompt: &str) -> DenoResult<String> { + self + .editor + .readline(&prompt) + .map(|line| { + self.editor.add_history_entry(line.as_ref()); + line + }).map_err(|e| deno_error(ErrorKind::Other, e.description().to_string())) + // Forward error to TS side for processing + } +} + +impl Drop for Repl { + fn drop(&mut self) { + self.save_history().unwrap(); + } +} + +pub fn history_path(dir: &DenoDir, history_file: &str) -> PathBuf { + let mut p: PathBuf = dir.root.clone(); + p.push(history_file); + p +} diff --git a/cli/resolve_addr.rs b/cli/resolve_addr.rs new file mode 100644 index 000000000..f26655be1 --- /dev/null +++ b/cli/resolve_addr.rs @@ -0,0 +1,156 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +use futures::Async; +use futures::Future; +use futures::Poll; +use std::error::Error; +use std::fmt; +use std::net::SocketAddr; +use std::net::ToSocketAddrs; + +/// Go-style network address parsing. Returns a future. +/// Examples: +/// "192.0.2.1:25" +/// ":80" +/// "[2001:db8::1]:80" +/// "198.51.100.1:80" +/// "deno.land:443" +pub fn resolve_addr(address: &str) -> ResolveAddrFuture { + ResolveAddrFuture { + address: address.to_string(), + } +} + +#[derive(Debug)] +pub enum ResolveAddrError { + Syntax, + Resolution(std::io::Error), +} + +impl fmt::Display for ResolveAddrError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.write_str(self.description()) + } +} + +impl Error for ResolveAddrError { + fn description(&self) -> &str { + match self { + ResolveAddrError::Syntax => "invalid address syntax", + ResolveAddrError::Resolution(e) => e.description(), + } + } +} + +pub struct ResolveAddrFuture { + address: String, +} + +impl Future for ResolveAddrFuture { + type Item = SocketAddr; + type Error = ResolveAddrError; + + fn poll(&mut self) -> Poll<Self::Item, Self::Error> { + // The implementation of this is not actually async at the moment, + // however we intend to use async DNS resolution in the future and + // so we expose this as a future instead of Result. + match split(&self.address) { + None => Err(ResolveAddrError::Syntax), + Some(addr_port_pair) => { + // I absolutely despise the .to_socket_addrs() API. + let r = addr_port_pair + .to_socket_addrs() + .map_err(ResolveAddrError::Resolution); + + r.and_then(|mut iter| match iter.next() { + Some(a) => Ok(Async::Ready(a)), + None => panic!("There should be at least one result"), + }) + } + } + } +} + +fn split(address: &str) -> Option<(&str, u16)> { + address.rfind(':').and_then(|i| { + let (a, p) = address.split_at(i); + // Default to localhost if given just the port. Example: ":80" + let addr = if !a.is_empty() { a } 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 p = p.trim_start_matches(':'); + match p.parse::<u16>() { + Err(_) => None, + Ok(port) => Some((addr, port)), + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::Ipv4Addr; + use std::net::Ipv6Addr; + use std::net::SocketAddrV4; + use std::net::SocketAddrV6; + + #[test] + fn split1() { + assert_eq!(split("127.0.0.1:80"), Some(("127.0.0.1", 80))); + } + + #[test] + fn split2() { + assert_eq!(split(":80"), Some(("0.0.0.0", 80))); + } + + #[test] + fn split3() { + assert_eq!(split("no colon"), None); + } + + #[test] + fn split4() { + assert_eq!(split("deno.land:443"), Some(("deno.land", 443))); + } + + #[test] + fn split5() { + assert_eq!(split("[2001:db8::1]:8080"), Some(("2001:db8::1", 8080))); + } + + #[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").wait().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").wait().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").wait().unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/cli/resources.rs b/cli/resources.rs new file mode 100644 index 000000000..1540f4ff7 --- /dev/null +++ b/cli/resources.rs @@ -0,0 +1,494 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +// Think of Resources as File Descriptors. They are integers that are allocated +// by the privileged side of Deno to refer to various resources. The simplest +// example are standard file system files and stdio - but there will be other +// resources added in the future that might not correspond to operating system +// level File Descriptors. To avoid confusion we call them "resources" not "file +// descriptors". This module implements a global resource table. Ops (AKA +// handlers) look up resources by their integer id here. + +use crate::cli::Buf; +use crate::errors; +use crate::errors::bad_resource; +use crate::errors::DenoError; +use crate::errors::DenoResult; +use crate::http_body::HttpBody; +use crate::isolate_state::WorkerChannels; +use crate::repl::Repl; + +use futures; +use futures::Future; +use futures::Poll; +use futures::Sink; +use futures::Stream; +use hyper; +use std; +use std::collections::HashMap; +use std::io::{Error, Read, Seek, SeekFrom, Write}; +use std::net::{Shutdown, SocketAddr}; +use std::process::ExitStatus; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; +use std::sync::{Arc, Mutex}; +use tokio; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::net::TcpStream; +use tokio_process; + +pub type ResourceId = u32; // Sometimes referred to RID. + +// These store Deno's file descriptors. These are not necessarily the operating +// system ones. +type ResourceTable = HashMap<ResourceId, Repr>; + +#[cfg(not(windows))] +use std::os::unix::io::FromRawFd; + +#[cfg(windows)] +use std::os::windows::io::FromRawHandle; + +#[cfg(windows)] +extern crate winapi; + +lazy_static! { + // Starts at 3 because stdio is [0-2]. + static ref NEXT_RID: AtomicUsize = AtomicUsize::new(3); + static ref RESOURCE_TABLE: Mutex<ResourceTable> = Mutex::new({ + let mut m = HashMap::new(); + // TODO Load these lazily during lookup? + m.insert(0, Repr::Stdin(tokio::io::stdin())); + + m.insert(1, Repr::Stdout({ + #[cfg(not(windows))] + let stdout = unsafe { std::fs::File::from_raw_fd(1) }; + #[cfg(windows)] + let stdout = unsafe { + std::fs::File::from_raw_handle(winapi::um::processenv::GetStdHandle( + winapi::um::winbase::STD_OUTPUT_HANDLE)) + }; + tokio::fs::File::from_std(stdout) + })); + + m.insert(2, Repr::Stderr(tokio::io::stderr())); + m + }); +} + +// Internal representation of Resource. +enum Repr { + Stdin(tokio::io::Stdin), + Stdout(tokio::fs::File), + Stderr(tokio::io::Stderr), + FsFile(tokio::fs::File), + // Since TcpListener might be closed while there is a pending accept task, + // we need to track the task so that when the listener is closed, + // this pending task could be notified and die. + // Currently TcpListener itself does not take care of this issue. + // See: https://github.com/tokio-rs/tokio/issues/846 + TcpListener(tokio::net::TcpListener, Option<futures::task::Task>), + TcpStream(tokio::net::TcpStream), + HttpBody(HttpBody), + Repl(Arc<Mutex<Repl>>), + // Enum size is bounded by the largest variant. + // Use `Box` around large `Child` struct. + // https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant + Child(Box<tokio_process::Child>), + ChildStdin(tokio_process::ChildStdin), + ChildStdout(tokio_process::ChildStdout), + ChildStderr(tokio_process::ChildStderr), + Worker(WorkerChannels), +} + +/// If the given rid is open, this returns the type of resource, E.G. "worker". +/// If the rid is closed or was never open, it returns None. +pub fn get_type(rid: ResourceId) -> Option<String> { + let table = RESOURCE_TABLE.lock().unwrap(); + table.get(&rid).map(inspect_repr) +} + +pub fn table_entries() -> Vec<(u32, String)> { + let table = RESOURCE_TABLE.lock().unwrap(); + + table + .iter() + .map(|(key, value)| (*key, inspect_repr(&value))) + .collect() +} + +#[test] +fn test_table_entries() { + let mut entries = table_entries(); + entries.sort(); + assert_eq!(entries[0], (0, String::from("stdin"))); + assert_eq!(entries[1], (1, String::from("stdout"))); + assert_eq!(entries[2], (2, String::from("stderr"))); +} + +fn inspect_repr(repr: &Repr) -> String { + let h_repr = match repr { + Repr::Stdin(_) => "stdin", + Repr::Stdout(_) => "stdout", + Repr::Stderr(_) => "stderr", + Repr::FsFile(_) => "fsFile", + Repr::TcpListener(_, _) => "tcpListener", + Repr::TcpStream(_) => "tcpStream", + Repr::HttpBody(_) => "httpBody", + Repr::Repl(_) => "repl", + Repr::Child(_) => "child", + Repr::ChildStdin(_) => "childStdin", + Repr::ChildStdout(_) => "childStdout", + Repr::ChildStderr(_) => "childStderr", + Repr::Worker(_) => "worker", + }; + + String::from(h_repr) +} + +// Abstract async file interface. +// Ideally in unix, if Resource represents an OS rid, it will be the same. +#[derive(Clone, Debug)] +pub struct Resource { + pub rid: ResourceId, +} + +impl Resource { + // TODO Should it return a Resource instead of net::TcpStream? + pub fn poll_accept(&mut self) -> Poll<(TcpStream, SocketAddr), Error> { + let mut table = RESOURCE_TABLE.lock().unwrap(); + let maybe_repr = table.get_mut(&self.rid); + match maybe_repr { + None => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Listener has been closed", + )), + Some(repr) => match repr { + Repr::TcpListener(ref mut s, _) => s.poll_accept(), + _ => panic!("Cannot accept"), + }, + } + } + + // close(2) is done by dropping the value. Therefore we just need to remove + // the resource from the RESOURCE_TABLE. + pub fn close(&self) { + let mut table = RESOURCE_TABLE.lock().unwrap(); + let r = table.remove(&self.rid); + assert!(r.is_some()); + } + + pub fn shutdown(&mut self, how: Shutdown) -> Result<(), DenoError> { + let mut table = RESOURCE_TABLE.lock().unwrap(); + let maybe_repr = table.get_mut(&self.rid); + match maybe_repr { + None => panic!("bad rid"), + Some(repr) => match repr { + Repr::TcpStream(ref mut f) => { + TcpStream::shutdown(f, how).map_err(DenoError::from) + } + _ => panic!("Cannot shutdown"), + }, + } + } +} + +impl Read for Resource { + fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> { + unimplemented!(); + } +} + +impl AsyncRead for Resource { + fn poll_read(&mut self, buf: &mut [u8]) -> Poll<usize, Error> { + let mut table = RESOURCE_TABLE.lock().unwrap(); + let maybe_repr = table.get_mut(&self.rid); + match maybe_repr { + None => panic!("bad rid"), + Some(repr) => match repr { + Repr::FsFile(ref mut f) => f.poll_read(buf), + Repr::Stdin(ref mut f) => f.poll_read(buf), + Repr::TcpStream(ref mut f) => f.poll_read(buf), + Repr::HttpBody(ref mut f) => f.poll_read(buf), + Repr::ChildStdout(ref mut f) => f.poll_read(buf), + Repr::ChildStderr(ref mut f) => f.poll_read(buf), + _ => panic!("Cannot read"), + }, + } + } +} + +impl Write for Resource { + fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> { + unimplemented!() + } + + fn flush(&mut self) -> std::io::Result<()> { + unimplemented!() + } +} + +impl AsyncWrite for Resource { + fn poll_write(&mut self, buf: &[u8]) -> Poll<usize, Error> { + let mut table = RESOURCE_TABLE.lock().unwrap(); + let maybe_repr = table.get_mut(&self.rid); + match maybe_repr { + None => panic!("bad rid"), + Some(repr) => match repr { + Repr::FsFile(ref mut f) => f.poll_write(buf), + Repr::Stdout(ref mut f) => f.poll_write(buf), + Repr::Stderr(ref mut f) => f.poll_write(buf), + Repr::TcpStream(ref mut f) => f.poll_write(buf), + Repr::ChildStdin(ref mut f) => f.poll_write(buf), + _ => panic!("Cannot write"), + }, + } + } + + fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { + unimplemented!() + } +} + +fn new_rid() -> ResourceId { + let next_rid = NEXT_RID.fetch_add(1, Ordering::SeqCst); + next_rid as ResourceId +} + +pub fn add_fs_file(fs_file: tokio::fs::File) -> Resource { + let rid = new_rid(); + let mut tg = RESOURCE_TABLE.lock().unwrap(); + match tg.insert(rid, Repr::FsFile(fs_file)) { + Some(_) => panic!("There is already a file with that rid"), + None => Resource { rid }, + } +} + +pub fn add_tcp_listener(listener: tokio::net::TcpListener) -> Resource { + let rid = new_rid(); + let mut tg = RESOURCE_TABLE.lock().unwrap(); + let r = tg.insert(rid, Repr::TcpListener(listener, None)); + assert!(r.is_none()); + Resource { rid } +} + +pub fn add_tcp_stream(stream: tokio::net::TcpStream) -> Resource { + let rid = new_rid(); + let mut tg = RESOURCE_TABLE.lock().unwrap(); + let r = tg.insert(rid, Repr::TcpStream(stream)); + assert!(r.is_none()); + Resource { rid } +} + +pub fn add_hyper_body(body: hyper::Body) -> Resource { + let rid = new_rid(); + let mut tg = RESOURCE_TABLE.lock().unwrap(); + let body = HttpBody::from(body); + let r = tg.insert(rid, Repr::HttpBody(body)); + assert!(r.is_none()); + Resource { rid } +} + +pub fn add_repl(repl: Repl) -> Resource { + let rid = new_rid(); + let mut tg = RESOURCE_TABLE.lock().unwrap(); + let r = tg.insert(rid, Repr::Repl(Arc::new(Mutex::new(repl)))); + assert!(r.is_none()); + Resource { rid } +} + +pub fn add_worker(wc: WorkerChannels) -> Resource { + let rid = new_rid(); + let mut tg = RESOURCE_TABLE.lock().unwrap(); + let r = tg.insert(rid, Repr::Worker(wc)); + assert!(r.is_none()); + Resource { rid } +} + +pub fn worker_post_message( + rid: ResourceId, + buf: Buf, +) -> futures::sink::Send<futures::sync::mpsc::Sender<Buf>> { + let mut table = RESOURCE_TABLE.lock().unwrap(); + let maybe_repr = table.get_mut(&rid); + match maybe_repr { + Some(Repr::Worker(ref mut wc)) => { + // unwrap here is incorrect, but doing it anyway + wc.0.clone().send(buf) + } + _ => panic!("bad resource"), // futures::future::err(bad_resource()).into(), + } +} + +pub struct WorkerReceiver { + rid: ResourceId, +} + +// Invert the dumbness that tokio_process causes by making Child itself a future. +impl Future for WorkerReceiver { + type Item = Option<Buf>; + type Error = DenoError; + + fn poll(&mut self) -> Poll<Option<Buf>, DenoError> { + let mut table = RESOURCE_TABLE.lock().unwrap(); + let maybe_repr = table.get_mut(&self.rid); + match maybe_repr { + Some(Repr::Worker(ref mut wc)) => wc.1.poll().map_err(|()| { + errors::new(errors::ErrorKind::Other, "recv msg error".to_string()) + }), + _ => Err(bad_resource()), + } + } +} + +pub fn worker_recv_message(rid: ResourceId) -> WorkerReceiver { + WorkerReceiver { rid } +} + +#[cfg_attr(feature = "cargo-clippy", allow(stutter))] +pub struct ChildResources { + pub child_rid: ResourceId, + pub stdin_rid: Option<ResourceId>, + pub stdout_rid: Option<ResourceId>, + pub stderr_rid: Option<ResourceId>, +} + +pub fn add_child(mut c: tokio_process::Child) -> ChildResources { + let child_rid = new_rid(); + let mut tg = RESOURCE_TABLE.lock().unwrap(); + + let mut resources = ChildResources { + child_rid, + stdin_rid: None, + stdout_rid: None, + stderr_rid: None, + }; + + if c.stdin().is_some() { + let stdin = c.stdin().take().unwrap(); + let rid = new_rid(); + let r = tg.insert(rid, Repr::ChildStdin(stdin)); + assert!(r.is_none()); + resources.stdin_rid = Some(rid); + } + if c.stdout().is_some() { + let stdout = c.stdout().take().unwrap(); + let rid = new_rid(); + let r = tg.insert(rid, Repr::ChildStdout(stdout)); + assert!(r.is_none()); + resources.stdout_rid = Some(rid); + } + if c.stderr().is_some() { + let stderr = c.stderr().take().unwrap(); + let rid = new_rid(); + let r = tg.insert(rid, Repr::ChildStderr(stderr)); + assert!(r.is_none()); + resources.stderr_rid = Some(rid); + } + + let r = tg.insert(child_rid, Repr::Child(Box::new(c))); + assert!(r.is_none()); + + resources +} + +pub struct ChildStatus { + rid: ResourceId, +} + +// Invert the dumbness that tokio_process causes by making Child itself a future. +impl Future for ChildStatus { + type Item = ExitStatus; + type Error = DenoError; + + fn poll(&mut self) -> Poll<ExitStatus, DenoError> { + let mut table = RESOURCE_TABLE.lock().unwrap(); + let maybe_repr = table.get_mut(&self.rid); + match maybe_repr { + Some(Repr::Child(ref mut child)) => child.poll().map_err(DenoError::from), + _ => Err(bad_resource()), + } + } +} + +pub fn child_status(rid: ResourceId) -> DenoResult<ChildStatus> { + let mut table = RESOURCE_TABLE.lock().unwrap(); + let maybe_repr = table.get_mut(&rid); + match maybe_repr { + Some(Repr::Child(ref mut _child)) => Ok(ChildStatus { rid }), + _ => Err(bad_resource()), + } +} + +pub fn get_repl(rid: ResourceId) -> DenoResult<Arc<Mutex<Repl>>> { + let mut table = RESOURCE_TABLE.lock().unwrap(); + let maybe_repr = table.get_mut(&rid); + match maybe_repr { + Some(Repr::Repl(ref mut r)) => Ok(r.clone()), + _ => Err(bad_resource()), + } +} + +pub fn lookup(rid: ResourceId) -> Option<Resource> { + debug!("resource lookup {}", rid); + let table = RESOURCE_TABLE.lock().unwrap(); + table.get(&rid).map(|_| Resource { rid }) +} + +// TODO(kevinkassimo): revamp this after the following lands: +// https://github.com/tokio-rs/tokio/pull/785 +pub fn seek( + resource: Resource, + offset: i32, + whence: u32, +) -> Box<dyn Future<Item = (), Error = DenoError> + Send> { + let mut table = RESOURCE_TABLE.lock().unwrap(); + // We take ownership of File here. + // It is put back below while still holding the lock. + let maybe_repr = table.remove(&resource.rid); + match maybe_repr { + None => panic!("bad rid"), + Some(Repr::FsFile(f)) => { + let seek_from = match whence { + 0 => SeekFrom::Start(offset as u64), + 1 => SeekFrom::Current(offset as i64), + 2 => SeekFrom::End(offset as i64), + _ => { + return Box::new(futures::future::err(errors::new( + errors::ErrorKind::InvalidSeekMode, + format!("Invalid seek mode: {}", whence), + ))); + } + }; + // Trait Clone not implemented on tokio::fs::File, + // so convert to std File first. + let std_file = f.into_std(); + // Create a copy and immediately put back. + // We don't want to block other resource ops. + // try_clone() would yield a copy containing the same + // underlying fd, so operations on the copy would also + // affect the one in resource table, and we don't need + // to write back. + let maybe_std_file_copy = std_file.try_clone(); + // Insert the entry back with the same rid. + table.insert( + resource.rid, + Repr::FsFile(tokio_fs::File::from_std(std_file)), + ); + if maybe_std_file_copy.is_err() { + return Box::new(futures::future::err(DenoError::from( + maybe_std_file_copy.unwrap_err(), + ))); + } + let mut std_file_copy = maybe_std_file_copy.unwrap(); + return Box::new(futures::future::lazy(move || { + let result = std_file_copy + .seek(seek_from) + .map(|_| { + return (); + }).map_err(DenoError::from); + futures::future::result(result) + })); + } + _ => panic!("cannot seek"), + } +} diff --git a/cli/startup_data.rs b/cli/startup_data.rs new file mode 100644 index 000000000..29ae4db7d --- /dev/null +++ b/cli/startup_data.rs @@ -0,0 +1,57 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use deno_core::deno_buf; +use deno_core::{StartupData, StartupScript}; + +pub fn deno_isolate_init() -> StartupData { + if cfg!(feature = "no-snapshot-init") { + debug!("Deno isolate init without snapshots."); + #[cfg(not(feature = "check-only"))] + let source_bytes = + include_bytes!(concat!(env!("GN_OUT_DIR"), "/gen/bundle/main.js")); + #[cfg(feature = "check-only")] + let source_bytes = vec![]; + + StartupData::Script(StartupScript { + filename: "gen/bundle/main.js".to_string(), + source: std::str::from_utf8(source_bytes).unwrap().to_string(), + }) + } else { + debug!("Deno isolate init with snapshots."); + #[cfg(not(any(feature = "check-only", feature = "no-snapshot-init")))] + let data = + include_bytes!(concat!(env!("GN_OUT_DIR"), "/gen/snapshot_deno.bin")); + #[cfg(any(feature = "check-only", feature = "no-snapshot-init"))] + let data = vec![]; + + unsafe { + StartupData::Snapshot(deno_buf::from_raw_parts(data.as_ptr(), data.len())) + } + } +} + +pub fn compiler_isolate_init() -> StartupData { + if cfg!(feature = "no-snapshot-init") { + debug!("Deno isolate init without snapshots."); + #[cfg(not(feature = "check-only"))] + let source_bytes = + include_bytes!(concat!(env!("GN_OUT_DIR"), "/gen/bundle/compiler.js")); + #[cfg(feature = "check-only")] + let source_bytes = vec![]; + + StartupData::Script(StartupScript { + filename: "gen/bundle/compiler.js".to_string(), + source: std::str::from_utf8(source_bytes).unwrap().to_string(), + }) + } else { + debug!("Deno isolate init with snapshots."); + #[cfg(not(any(feature = "check-only", feature = "no-snapshot-init")))] + let data = + include_bytes!(concat!(env!("GN_OUT_DIR"), "/gen/snapshot_compiler.bin")); + #[cfg(any(feature = "check-only", feature = "no-snapshot-init"))] + let data = vec![]; + + unsafe { + StartupData::Snapshot(deno_buf::from_raw_parts(data.as_ptr(), data.len())) + } + } +} diff --git a/cli/tokio_util.rs b/cli/tokio_util.rs new file mode 100644 index 000000000..810b826b4 --- /dev/null +++ b/cli/tokio_util.rs @@ -0,0 +1,118 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use crate::resources::Resource; +use futures; +use futures::Future; +use futures::Poll; +use std::io; +use std::mem; +use std::net::SocketAddr; +use tokio; +use tokio::net::TcpStream; + +pub fn run<F>(future: F) +where + F: Future<Item = (), Error = ()> + Send + 'static, +{ + // tokio::runtime::current_thread::run(future) + tokio::run(future) +} + +pub fn block_on<F, R, E>(future: F) -> Result<R, E> +where + F: Send + 'static + Future<Item = R, Error = E>, + R: Send + 'static, + E: Send + 'static, +{ + let (tx, rx) = futures::sync::oneshot::channel(); + tokio::spawn(future.then(move |r| tx.send(r).map_err(|_| unreachable!()))); + rx.wait().unwrap() +} + +// Set the default executor so we can use tokio::spawn(). It's difficult to +// pass around mut references to the runtime, so using with_default is +// preferable. Ideally Tokio would provide this function. +#[cfg(test)] +pub fn init<F>(f: F) +where + F: FnOnce(), +{ + use tokio_executor; + let rt = tokio::runtime::Runtime::new().unwrap(); + let mut executor = rt.executor(); + let mut enter = tokio_executor::enter().expect("Multiple executors at once"); + tokio_executor::with_default(&mut executor, &mut enter, move |_enter| f()); +} + +#[derive(Debug)] +enum AcceptState { + Pending(Resource), + Empty, +} + +/// Simply accepts a connection. +pub fn accept(r: Resource) -> Accept { + Accept { + state: AcceptState::Pending(r), + } +} + +/// A future which can be used to easily read available number of bytes to fill +/// a buffer. +/// +/// Created by the [`read`] function. +#[derive(Debug)] +pub struct Accept { + state: AcceptState, +} + +impl Future for Accept { + type Item = (TcpStream, SocketAddr); + type Error = io::Error; + + fn poll(&mut self) -> Poll<Self::Item, Self::Error> { + let (stream, addr) = match self.state { + AcceptState::Pending(ref mut r) => try_ready!(r.poll_accept()), + AcceptState::Empty => panic!("poll Accept after it's done"), + }; + + match mem::replace(&mut self.state, AcceptState::Empty) { + AcceptState::Pending(_) => Ok((stream, addr).into()), + AcceptState::Empty => panic!("invalid internal state"), + } + } +} + +/// `futures::future::poll_fn` only support `F: FnMut()->Poll<T, E>` +/// However, we require that `F: FnOnce()->Poll<T, E>`. +/// Therefore, we created our version of `poll_fn`. +pub fn poll_fn<T, E, F>(f: F) -> PollFn<F> +where + F: FnOnce() -> Poll<T, E>, +{ + PollFn { inner: Some(f) } +} + +pub struct PollFn<F> { + inner: Option<F>, +} + +impl<T, E, F> Future for PollFn<F> +where + F: FnOnce() -> Poll<T, E>, +{ + type Item = T; + type Error = E; + + fn poll(&mut self) -> Poll<T, E> { + let f = self.inner.take().expect("Inner fn has been taken."); + f() + } +} + +pub fn panic_on_error<I, E, F>(f: F) -> impl Future<Item = I, Error = ()> +where + F: Future<Item = I, Error = E>, + E: std::fmt::Debug, +{ + f.map_err(|err| panic!("Future got unexpected error: {:?}", err)) +} diff --git a/cli/tokio_write.rs b/cli/tokio_write.rs new file mode 100644 index 000000000..945de375d --- /dev/null +++ b/cli/tokio_write.rs @@ -0,0 +1,62 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +// TODO Submit this file upstream into tokio-io/src/io/write.rs +use std::io; +use std::mem; + +use futures::{Future, Poll}; +use tokio::io::AsyncWrite; + +/// A future used to write some data to a stream. +/// +/// This is created by the [`write`] top-level method. +/// +/// [`write`]: fn.write.html +#[derive(Debug)] +pub struct Write<A, T> { + state: State<A, T>, +} + +#[derive(Debug)] +enum State<A, T> { + Pending { a: A, buf: T }, + Empty, +} + +/// Creates a future that will write some of the buffer `buf` to +/// the stream `a` provided. +/// +/// Any error which happens during writing will cause both the stream and the +/// buffer to get destroyed. +pub fn write<A, T>(a: A, buf: T) -> Write<A, T> +where + A: AsyncWrite, + T: AsRef<[u8]>, +{ + Write { + state: State::Pending { a, buf }, + } +} + +impl<A, T> Future for Write<A, T> +where + A: AsyncWrite, + T: AsRef<[u8]>, +{ + type Item = (A, T, usize); + type Error = io::Error; + + fn poll(&mut self) -> Poll<(A, T, usize), io::Error> { + let nwritten = match self.state { + State::Pending { + ref mut a, + ref mut buf, + } => try_ready!(a.poll_write(buf.as_ref())), + State::Empty => panic!("poll a Read after it's done"), + }; + + match mem::replace(&mut self.state, State::Empty) { + State::Pending { a, buf } => Ok((a, buf, nwritten).into()), + State::Empty => panic!("invalid internal state"), + } + } +} diff --git a/cli/version.rs b/cli/version.rs new file mode 100644 index 000000000..e6ec9008b --- /dev/null +++ b/cli/version.rs @@ -0,0 +1,6 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +pub const DENO: &str = env!("CARGO_PKG_VERSION"); + +pub fn v8() -> &'static str { + deno_core::v8_version() +} diff --git a/cli/workers.rs b/cli/workers.rs new file mode 100644 index 000000000..edded7756 --- /dev/null +++ b/cli/workers.rs @@ -0,0 +1,181 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use crate::cli::Buf; +use crate::cli::Cli; +use crate::flags::DenoFlags; +use crate::isolate::Isolate; +use crate::isolate_state::IsolateState; +use crate::isolate_state::WorkerChannels; +use crate::js_errors::JSErrorColor; +use crate::permissions::DenoPermissions; +use crate::resources; +use crate::tokio_util; +use deno_core::JSError; +use deno_core::StartupData; +use futures::future::lazy; +use futures::sync::mpsc; +use futures::sync::oneshot; +use futures::Future; +use futures::Poll; +use std::sync::Arc; +use std::thread; + +/// Rust interface for WebWorkers. +pub struct Worker { + isolate: Isolate, +} + +impl Worker { + pub fn new( + startup_data: Option<StartupData>, + flags: DenoFlags, + argv: Vec<String>, + permissions: DenoPermissions, + ) -> (Self, WorkerChannels) { + let (worker_in_tx, worker_in_rx) = mpsc::channel::<Buf>(1); + let (worker_out_tx, worker_out_rx) = mpsc::channel::<Buf>(1); + + let internal_channels = (worker_out_tx, worker_in_rx); + let external_channels = (worker_in_tx, worker_out_rx); + + let state = + Arc::new(IsolateState::new(flags, argv, Some(internal_channels))); + + let cli = Cli::new(startup_data, state, permissions); + let isolate = Isolate::new(cli); + + let worker = Worker { isolate }; + (worker, external_channels) + } + + pub fn execute(&mut self, js_source: &str) -> Result<(), JSError> { + self.isolate.execute(js_source) + } +} + +impl Future for Worker { + type Item = (); + type Error = JSError; + + fn poll(&mut self) -> Poll<(), JSError> { + self.isolate.poll() + } +} + +pub fn spawn( + startup_data: Option<StartupData>, + state: &IsolateState, + js_source: String, + permissions: DenoPermissions, +) -> resources::Resource { + // TODO This function should return a Future, so that the caller can retrieve + // the JSError if one is thrown. Currently it just prints to stderr and calls + // exit(1). + // let (js_error_tx, js_error_rx) = oneshot::channel::<JSError>(); + let (p, c) = oneshot::channel::<resources::Resource>(); + let builder = thread::Builder::new().name("worker".to_string()); + + let flags = state.flags.clone(); + let argv = state.argv.clone(); + + let _tid = builder + .spawn(move || { + tokio_util::run(lazy(move || { + let (mut worker, external_channels) = + Worker::new(startup_data, flags, argv, permissions); + let resource = resources::add_worker(external_channels); + p.send(resource.clone()).unwrap(); + + worker + .execute("denoMain()") + .expect("worker denoMain failed"); + worker + .execute("workerMain()") + .expect("worker workerMain failed"); + worker.execute(&js_source).expect("worker js_source failed"); + + worker.then(move |r| -> Result<(), ()> { + resource.close(); + debug!("workers.rs after resource close"); + if let Err(err) = r { + eprintln!("{}", JSErrorColor(&err).to_string()); + std::process::exit(1); + } + Ok(()) + }) + })); + + debug!("workers.rs after spawn"); + }).unwrap(); + + c.wait().unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::startup_data; + + #[test] + fn test_spawn() { + let startup_data = startup_data::compiler_isolate_init(); + let resource = spawn( + Some(startup_data), + &IsolateState::mock(), + r#" + onmessage = function(e) { + let s = new TextDecoder().decode(e.data);; + console.log("msg from main script", s); + if (s == "exit") { + close(); + return; + } else { + console.assert(s === "hi"); + } + postMessage(new Uint8Array([1, 2, 3])); + console.log("after postMessage"); + } + "#.into(), + DenoPermissions::default(), + ); + let msg = String::from("hi").into_boxed_str().into_boxed_bytes(); + + let r = resources::worker_post_message(resource.rid, msg).wait(); + assert!(r.is_ok()); + + let maybe_msg = + resources::worker_recv_message(resource.rid).wait().unwrap(); + assert!(maybe_msg.is_some()); + assert_eq!(*maybe_msg.unwrap(), [1, 2, 3]); + + let msg = String::from("exit").into_boxed_str().into_boxed_bytes(); + let r = resources::worker_post_message(resource.rid, msg).wait(); + assert!(r.is_ok()); + } + + #[test] + fn removed_from_resource_table_on_close() { + let startup_data = startup_data::compiler_isolate_init(); + let resource = spawn( + Some(startup_data), + &IsolateState::mock(), + "onmessage = () => close();".into(), + DenoPermissions::default(), + ); + + assert_eq!( + resources::get_type(resource.rid), + Some("worker".to_string()) + ); + + let msg = String::from("hi").into_boxed_str().into_boxed_bytes(); + let r = resources::worker_post_message(resource.rid, msg).wait(); + assert!(r.is_ok()); + println!("rid {:?}", resource.rid); + + // TODO Need a way to get a future for when a resource closes. + // For now, just sleep for a bit. + // resource.close(); + thread::sleep(std::time::Duration::from_millis(1000)); + assert_eq!(resources::get_type(resource.rid), None); + } +} |