diff options
author | Andy Hayden <andyhayden1@gmail.com> | 2018-11-05 09:55:59 -0800 |
---|---|---|
committer | Ryan Dahl <ry@tinyclouds.org> | 2018-11-05 09:55:59 -0800 |
commit | 27ecfc1617c79d23255e025fcbd0257b3523906a (patch) | |
tree | e6b68b4a291372e0a479d7dff156d546ff965d30 /src | |
parent | 5e48a681c4d6d6cb3debb4024b5108780dbbad90 (diff) |
Add repl (#998)
- Running repl from js side.
- Add tests for repl behavior.
- Handle ctrl-C and ctrl-D.
Diffstat (limited to 'src')
-rw-r--r-- | src/main.rs | 2 | ||||
-rw-r--r-- | src/msg.fbs | 22 | ||||
-rw-r--r-- | src/ops.rs | 72 | ||||
-rw-r--r-- | src/repl.rs | 122 | ||||
-rw-r--r-- | src/resources.rs | 33 |
5 files changed, 245 insertions, 6 deletions
diff --git a/src/main.rs b/src/main.rs index af84b19be..ca15d468b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ extern crate libc; extern crate rand; extern crate remove_dir_all; extern crate ring; +extern crate rustyline; extern crate tempfile; extern crate tokio; extern crate tokio_executor; @@ -35,6 +36,7 @@ pub mod msg; pub mod msg_util; pub mod ops; pub mod permissions; +mod repl; pub mod resources; pub mod snapshot; mod tokio_util; diff --git a/src/msg.fbs b/src/msg.fbs index 5f86ad56a..9686c76cb 100644 --- a/src/msg.fbs +++ b/src/msg.fbs @@ -24,6 +24,10 @@ union Any { Rename, Readlink, ReadlinkRes, + ReplStart, + ReplStartRes, + ReplReadline, + ReplReadlineRes, Resources, ResourcesRes, Symlink, @@ -273,6 +277,24 @@ table ReadlinkRes { path: string; } +table ReplStart { + history_file: string; + // TODO add config +} + +table ReplStartRes { + rid: int; +} + +table ReplReadline { + rid: int; + prompt: string; +} + +table ReplReadlineRes { + line: string; +} + table Resources {} table Resource { diff --git a/src/ops.rs b/src/ops.rs index 9266646ef..f645f1d01 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -20,6 +20,7 @@ use futures::Poll; use hyper; use hyper::rt::{Future, Stream}; use remove_dir_all::remove_dir_all; +use repl; use resources::table_entries; use std; use std::fs; @@ -96,6 +97,8 @@ pub fn dispatch( msg::Any::Read => op_read, 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::SetEnv => op_set_env, msg::Any::Shutdown => op_shutdown, @@ -1086,6 +1089,75 @@ fn op_read_link( }) } +fn op_repl_start( + state: &Arc<IsolateState>, + base: &msg::Base, + data: &'static mut [u8], +) -> Box<Op> { + 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(&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( + _state: &Arc<IsolateState>, + base: &msg::Base, + data: &'static mut [u8], +) -> Box<Op> { + 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); + + // Ignore this clippy warning until this issue is addressed: + // https://github.com/rust-lang-nursery/rust-clippy/issues/1684 + #[cfg_attr(feature = "cargo-clippy", allow(redundant_closure_call))] + Box::new(futures::future::result((move || { + let line = resources::readline(rid, &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( state: &Arc<IsolateState>, base: &msg::Base, diff --git a/src/repl.rs b/src/repl.rs new file mode 100644 index 000000000..af1679194 --- /dev/null +++ b/src/repl.rs @@ -0,0 +1,122 @@ +// Copyright 2018 the Deno authors. All rights reserved. MIT license. +extern crate rustyline; + +use rustyline::error::ReadlineError::Interrupted; + +use msg::ErrorKind; +use std::error::Error; + +use deno_dir::DenoDir; +use errors::new as deno_error; +use errors::DenoResult; +use std::path::PathBuf; +use std::process::exit; + +#[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) -> Repl { + let mut repl = Repl { + 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| match e { + Interrupted => { + self.save_history().unwrap(); + exit(1) + } + e => deno_error(ErrorKind::Other, e.description().to_string()), + }) + } +} + +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/src/resources.rs b/src/resources.rs index 1e3b0a9b7..5d472adc7 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -10,7 +10,10 @@ #[cfg(unix)] use eager_unix as eager; +use errors::bad_resource; use errors::DenoError; +use errors::DenoResult; +use repl::Repl; use tokio_util; use tokio_write; @@ -56,6 +59,7 @@ enum Repr { FsFile(tokio::fs::File), TcpListener(tokio::net::TcpListener), TcpStream(tokio::net::TcpStream), + Repl(Repl), } pub fn table_entries() -> Vec<(i32, String)> { @@ -85,6 +89,7 @@ fn inspect_repr(repr: &Repr) -> String { Repr::FsFile(_) => "fsFile", Repr::TcpListener(_) => "tcpListener", Repr::TcpStream(_) => "tcpStream", + Repr::Repl(_) => "repl", }; String::from(h_repr) @@ -150,10 +155,7 @@ impl AsyncRead for Resource { 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::Stdout(_) | Repr::Stderr(_) => { - panic!("Cannot read from stdout/stderr") - } - Repr::TcpListener(_) => panic!("Cannot read"), + _ => panic!("Cannot read"), }, } } @@ -180,8 +182,7 @@ impl AsyncWrite for Resource { 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::Stdin(_) => panic!("Cannot write to stdin"), - Repr::TcpListener(_) => panic!("Cannot write"), + _ => panic!("Cannot write"), }, } } @@ -221,6 +222,26 @@ pub fn add_tcp_stream(stream: tokio::net::TcpStream) -> Resource { 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(repl)); + assert!(r.is_none()); + Resource { rid } +} + +pub fn readline(rid: ResourceId, prompt: &str) -> DenoResult<String> { + let mut table = RESOURCE_TABLE.lock().unwrap(); + let maybe_repr = table.get_mut(&rid); + match maybe_repr { + Some(Repr::Repl(ref mut r)) => { + let line = r.readline(&prompt)?; + Ok(line) + } + _ => Err(bad_resource()), + } +} + pub fn lookup(rid: ResourceId) -> Option<Resource> { let table = RESOURCE_TABLE.lock().unwrap(); table.get(&rid).map(|_| Resource { rid }) |