1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-24 08:09:08 -05:00

refactor(repl): use an inspector session (#7763)

This ports the REPL over to Rust and makes use of an inspector session to run a REPL on top of any isolate which lets make full use of rustylines various things like validators and completors without having to introduce a bunch of hard to test internal ops and glue code.

An accidental but good side effect of this is that the multiple line input we previously had is now an editable multi-line input prompt that is correctly stored in the history as a single entry.
This commit is contained in:
Casper Beyer 2020-10-02 07:14:55 +08:00 committed by GitHub
parent 5590b97670
commit 4c779b5e8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 285 additions and 352 deletions

11
Cargo.lock generated
View file

@ -426,6 +426,7 @@ dependencies = [
"regex",
"ring",
"rustyline",
"rustyline-derive",
"semver-parser 0.9.0",
"serde",
"sourcemap",
@ -1946,6 +1947,16 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "rustyline-derive"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a50e29610a5be68d4a586a5cce3bfb572ed2c2a74227e4168444b7bf4e5235"
dependencies = [
"quote 1.0.7",
"syn 1.0.41",
]
[[package]]
name = "ryu"
version = "1.0.5"

View file

@ -57,6 +57,7 @@ rand = "0.7.3"
regex = "1.3.9"
ring = "0.16.15"
rustyline = { version = "6.3.0", default-features = false }
rustyline-derive = "0.3.1"
serde = { version = "1.0.116", features = ["derive"] }
sys-info = "0.7.0"
sourcemap = "6.0.1"

View file

@ -120,6 +120,7 @@ pub struct Flags {
pub no_remote: bool,
pub read_allowlist: Vec<PathBuf>,
pub reload: bool,
pub repl: bool,
pub seed: Option<u64>,
pub unstable: bool,
pub v8_flags: Option<Vec<String>>,
@ -447,6 +448,7 @@ fn completions_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
fn repl_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
runtime_args_parse(flags, matches, false);
flags.repl = true;
flags.subcommand = DenoSubcommand::Repl;
flags.allow_net = true;
flags.allow_env = true;
@ -2142,6 +2144,7 @@ mod tests {
assert_eq!(
r.unwrap(),
Flags {
repl: true,
subcommand: DenoSubcommand::Repl,
allow_net: true,
allow_env: true,
@ -2162,6 +2165,7 @@ mod tests {
assert_eq!(
r.unwrap(),
Flags {
repl: true,
subcommand: DenoSubcommand::Repl,
unstable: true,
import_map_path: Some("import_map.json".to_string()),

View file

@ -857,6 +857,7 @@ impl v8::inspector::ChannelImpl for InspectorSession {
) {
let raw_message = message.unwrap().string().to_string();
let message = serde_json::from_str(&raw_message).unwrap();
self
.response_tx_map
.remove(&call_id)

View file

@ -63,6 +63,7 @@ use crate::file_fetcher::SourceFileFetcher;
use crate::file_fetcher::TextDocument;
use crate::fs as deno_fs;
use crate::global_state::GlobalState;
use crate::inspector::InspectorSession;
use crate::media_type::MediaType;
use crate::permissions::Permissions;
use crate::worker::MainWorker;
@ -428,9 +429,26 @@ async fn run_repl(flags: Flags) -> Result<(), AnyError> {
let main_module =
ModuleSpecifier::resolve_url_or_path("./$deno$repl.ts").unwrap();
let global_state = GlobalState::new(flags)?;
let mut worker = MainWorker::new(&global_state, main_module);
let mut worker = MainWorker::new(&global_state, main_module.clone());
(&mut *worker).await?;
let inspector = worker
.inspector
.as_mut()
.expect("Inspector is not created.");
let inspector_session = InspectorSession::new(&mut **inspector);
let repl = repl::run(&global_state, inspector_session);
tokio::pin!(repl);
loop {
(&mut *worker).await?;
tokio::select! {
result = &mut repl => {
return result;
}
_ = &mut *worker => {}
}
}
}

View file

@ -16,7 +16,6 @@ pub mod permissions;
pub mod plugin;
pub mod process;
pub mod random;
pub mod repl;
pub mod runtime;
pub mod runtime_compiler;
pub mod signal;

View file

@ -1,78 +0,0 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use crate::repl;
use crate::repl::Repl;
use deno_core::error::bad_resource_id;
use deno_core::error::AnyError;
use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::serde_json::Value;
use deno_core::BufVec;
use deno_core::OpState;
use deno_core::ZeroCopyBuf;
use serde::Deserialize;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::Mutex;
pub fn init(rt: &mut deno_core::JsRuntime) {
super::reg_json_sync(rt, "op_repl_start", op_repl_start);
super::reg_json_async(rt, "op_repl_readline", op_repl_readline);
}
struct ReplResource(Arc<Mutex<Repl>>);
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ReplStartArgs {
history_file: String,
}
fn op_repl_start(
state: &mut OpState,
args: Value,
_zero_copy: &mut [ZeroCopyBuf],
) -> Result<Value, AnyError> {
let args: ReplStartArgs = serde_json::from_value(args)?;
debug!("op_repl_start {}", args.history_file);
let history_path = {
let cli_state = super::global_state(state);
repl::history_path(&cli_state.dir, &args.history_file)
};
let repl = repl::Repl::new(history_path);
let resource = ReplResource(Arc::new(Mutex::new(repl)));
let rid = state.resource_table.add("repl", Box::new(resource));
Ok(json!(rid))
}
#[derive(Deserialize)]
struct ReplReadlineArgs {
rid: i32,
prompt: String,
}
async fn op_repl_readline(
state: Rc<RefCell<OpState>>,
args: Value,
_zero_copy: BufVec,
) -> Result<Value, AnyError> {
let args: ReplReadlineArgs = serde_json::from_value(args)?;
let rid = args.rid as u32;
let prompt = args.prompt;
debug!("op_repl_readline {} {}", rid, prompt);
let repl = {
let state = state.borrow();
let resource = state
.resource_table
.get::<ReplResource>(rid)
.ok_or_else(bad_resource_id)?;
resource.0.clone()
};
tokio::task::spawn_blocking(move || {
let line = repl.lock().unwrap().readline(&prompt)?;
Ok(json!(line))
})
.await
.unwrap()
}

View file

@ -4,7 +4,6 @@ use crate::colors;
use crate::metrics::Metrics;
use crate::permissions::Permissions;
use crate::version;
use crate::DenoSubcommand;
use deno_core::error::AnyError;
use deno_core::serde_json;
use deno_core::serde_json::json;
@ -41,7 +40,6 @@ fn op_start(
"noColor": !colors::use_color(),
"pid": std::process::id(),
"ppid": ppid(),
"repl": gs.flags.subcommand == DenoSubcommand::Repl,
"target": env!("TARGET"),
"tsVersion": version::TYPESCRIPT,
"unstableFlag": gs.flags.unstable,

View file

@ -1,73 +1,255 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use crate::deno_dir::DenoDir;
use crate::global_state::GlobalState;
use crate::inspector::InspectorSession;
use deno_core::error::AnyError;
use deno_core::serde_json::json;
use rustyline::error::ReadlineError;
use rustyline::validate::MatchingBracketValidator;
use rustyline::validate::ValidationContext;
use rustyline::validate::ValidationResult;
use rustyline::validate::Validator;
use rustyline::Editor;
use std::fs;
use std::path::PathBuf;
use rustyline_derive::{Completer, Helper, Highlighter, Hinter};
use std::sync::Arc;
use std::sync::Mutex;
pub struct Repl {
editor: Editor<()>,
history_file: PathBuf,
// Provides syntax specific helpers to the editor like validation for multi-line edits.
#[derive(Completer, Helper, Highlighter, Hinter)]
struct Helper {
validator: MatchingBracketValidator,
}
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) -> Result<(), AnyError> {
fs::create_dir_all(self.history_file.parent().unwrap())?;
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);
e.into()
})
}
pub fn readline(&mut self, prompt: &str) -> Result<String, AnyError> {
self
.editor
.readline(&prompt)
.map(|line| {
self.editor.add_history_entry(line.clone());
line
})
.map_err(AnyError::from)
// Forward error to TS side for processing
impl Validator for Helper {
fn validate(
&self,
ctx: &mut ValidationContext,
) -> Result<ValidationResult, ReadlineError> {
self.validator.validate(ctx)
}
}
impl Drop for Repl {
fn drop(&mut self) {
self.save_history().unwrap();
}
}
pub async fn run(
global_state: &GlobalState,
mut session: Box<InspectorSession>,
) -> Result<(), AnyError> {
// Our inspector is unable to default to the default context id so we have to specify it here.
let context_id: u32 = 1;
pub fn history_path(dir: &DenoDir, history_file: &str) -> PathBuf {
let mut p: PathBuf = dir.root.clone();
p.push(history_file);
p
let history_file = global_state.dir.root.join("deno_history.txt");
session
.post_message("Runtime.enable".to_string(), None)
.await?;
let helper = Helper {
validator: MatchingBracketValidator::new(),
};
let editor = Arc::new(Mutex::new(Editor::new()));
editor.lock().unwrap().set_helper(Some(helper));
editor
.lock()
.unwrap()
.load_history(history_file.to_str().unwrap())
.unwrap_or(());
println!("Deno {}", crate::version::DENO);
println!("exit using ctrl+d or close()");
let prelude = r#"
Object.defineProperty(globalThis, "_", {
configurable: true,
get: () => Deno[Deno.internal].lastEvalResult,
set: (value) => {
Object.defineProperty(globalThis, "_", {
value: value,
writable: true,
enumerable: true,
configurable: true,
});
console.log("Last evaluation result is no longer saved to _.");
},
});
Object.defineProperty(globalThis, "_error", {
configurable: true,
get: () => Deno[Deno.internal].lastThrownError,
set: (value) => {
Object.defineProperty(globalThis, "_error", {
value: value,
writable: true,
enumerable: true,
configurable: true,
});
console.log("Last thrown error is no longer saved to _error.");
},
});
"#;
session
.post_message(
"Runtime.evaluate".to_string(),
Some(json!({
"expression": prelude,
"contextId": context_id,
})),
)
.await?;
loop {
let editor2 = editor.clone();
let line = tokio::task::spawn_blocking(move || {
editor2.lock().unwrap().readline("> ")
})
.await?;
match line {
Ok(line) => {
// It is a bit unexpected that { "foo": "bar" } is interpreted as a block
// statement rather than an object literal so we interpret it as an expression statement
// to match the behavior found in a typical prompt including browser developer tools.
let wrapped_line = if line.trim_start().starts_with('{')
&& !line.trim_end().ends_with(';')
{
format!("({})", &line)
} else {
line.clone()
};
let evaluate_response = session
.post_message(
"Runtime.evaluate".to_string(),
Some(json!({
"expression": format!("'use strict'; void 0;\n{}", &wrapped_line),
"contextId": context_id,
// TODO(caspervonb) set repl mode to true to enable const redeclarations and top
// level await
"replMode": false,
})),
)
.await?;
// If that fails, we retry it without wrapping in parens letting the error bubble up to the
// user if it is still an error.
let evaluate_response =
if evaluate_response.get("exceptionDetails").is_some()
&& wrapped_line != line
{
session
.post_message(
"Runtime.evaluate".to_string(),
Some(json!({
"expression": format!("'use strict'; void 0;\n{}", &line),
"contextId": context_id,
// TODO(caspervonb) set repl mode to true to enable const redeclarations and top
// level await
"replMode": false,
})),
)
.await?
} else {
evaluate_response
};
let is_closing = session
.post_message(
"Runtime.evaluate".to_string(),
Some(json!({
"expression": "(globalThis.closed)",
"contextId": context_id,
})),
)
.await?
.get("result")
.unwrap()
.get("value")
.unwrap()
.as_bool()
.unwrap();
if is_closing {
break;
}
let evaluate_result = evaluate_response.get("result").unwrap();
let evaluate_exception_details =
evaluate_response.get("exceptionDetails");
if evaluate_exception_details.is_some() {
session
.post_message(
"Runtime.callFunctionOn".to_string(),
Some(json!({
"executionContextId": context_id,
"functionDeclaration": "function (object) { Deno[Deno.internal].lastThrownError = object; }",
"arguments": [
evaluate_result,
],
}))).await?;
} else {
session
.post_message(
"Runtime.callFunctionOn".to_string(),
Some(json!({
"executionContextId": context_id,
"functionDeclaration": "function (object) { Deno[Deno.internal].lastEvalResult = object; }",
"arguments": [
evaluate_result,
],
}))).await?;
}
// TODO(caspervonb) we should investigate using previews here but to keep things
// consistent with the previous implementation we just get the preview result from
// Deno.inspectArgs.
let inspect_response = session
.post_message(
"Runtime.callFunctionOn".to_string(),
Some(json!({
"executionContextId": context_id,
"functionDeclaration": "function (object) { return Deno[Deno.internal].inspectArgs(['%o', object]); }",
"arguments": [
evaluate_result,
],
}))).await?;
let inspect_result = inspect_response.get("result").unwrap();
match evaluate_exception_details {
Some(_) => eprintln!(
"Uncaught {}",
inspect_result.get("value").unwrap().as_str().unwrap()
),
None => println!(
"{}",
inspect_result.get("value").unwrap().as_str().unwrap()
),
}
editor.lock().unwrap().add_history_entry(line.as_str());
}
Err(ReadlineError::Interrupted) => {
break;
}
Err(ReadlineError::Eof) => {
break;
}
Err(err) => {
println!("Error: {:?}", err);
break;
}
}
}
std::fs::create_dir_all(history_file.parent().unwrap())?;
editor
.lock()
.unwrap()
.save_history(history_file.to_str().unwrap())?;
Ok(())
}

View file

@ -1,197 +0,0 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
((window) => {
const core = window.Deno.core;
const exit = window.__bootstrap.os.exit;
const version = window.__bootstrap.version.version;
const inspectArgs = window.__bootstrap.console.inspectArgs;
function opStartRepl(historyFile) {
return core.jsonOpSync("op_repl_start", { historyFile });
}
function opReadline(rid, prompt) {
return core.jsonOpAsync("op_repl_readline", { rid, prompt });
}
function replLog(...args) {
core.print(inspectArgs(args) + "\n");
}
function replError(...args) {
core.print(inspectArgs(args) + "\n", true);
}
// Error messages that allow users to continue input
// instead of throwing an error to REPL
// ref: https://github.com/v8/v8/blob/master/src/message-template.h
// TODO(kevinkassimo): this list might not be comprehensive
const recoverableErrorMessages = [
"Unexpected end of input", // { or [ or (
"Missing initializer in const declaration", // const a
"Missing catch or finally after try", // try {}
"missing ) after argument list", // console.log(1
"Unterminated template literal", // `template
// TODO(kevinkassimo): need a parser to handling errors such as:
// "Missing } in template expression" // `${ or `${ a 123 }`
];
function isRecoverableError(e) {
return recoverableErrorMessages.includes(e.message);
}
// Returns `true` if `close()` is called in REPL.
// We should quit the REPL when this function returns `true`.
function isCloseCalled() {
return globalThis.closed;
}
let lastEvalResult = undefined;
let lastThrownError = undefined;
// Evaluate code.
// Returns true if code is consumed (no error/irrecoverable error).
// Returns false if error is recoverable
function evaluate(code, preprocess = true) {
const rawCode = code;
if (preprocess) {
// It is a bit unexpected that { "foo": "bar" } is interpreted as a block
// statement rather than an object literal so we interpret it as an expression statement
// to match the behavior found in a typical prompt including browser developer tools.
if (code.trimLeft().startsWith("{") && !code.trimRight().endsWith(";")) {
code = `(${code})`;
}
}
// each evalContext is a separate function body, and we want strict mode to
// work, so we should ensure that the code starts with "use strict"
const [result, errInfo] = core.evalContext(`"use strict";\n\n${code}`);
if (!errInfo) {
// when a function is eval'ed with just "use strict" sometimes the result
// is "use strict" which should be discarded
lastEvalResult = typeof result === "string" && result === "use strict"
? undefined
: result;
if (!isCloseCalled()) {
replLog("%o", lastEvalResult);
}
} else if (errInfo.isCompileError && code.length != rawCode.length) {
return evaluate(rawCode, false);
} else if (errInfo.isCompileError && isRecoverableError(errInfo.thrown)) {
// Recoverable compiler error
return false; // don't consume code.
} else {
lastThrownError = errInfo.thrown;
if (errInfo.isNativeError) {
const formattedError = core.formatError(errInfo.thrown);
replError(formattedError);
} else {
replError("Thrown:", errInfo.thrown);
}
}
return true;
}
async function replLoop() {
const { console } = globalThis;
const historyFile = "deno_history.txt";
const rid = opStartRepl(historyFile);
const quitRepl = (exitCode) => {
// Special handling in case user calls deno.close(3).
try {
core.close(rid); // close signals Drop on REPL and saves history.
} catch {}
exit(exitCode);
};
// Configure globalThis._ to give the last evaluation result.
Object.defineProperty(globalThis, "_", {
configurable: true,
get: () => lastEvalResult,
set: (value) => {
Object.defineProperty(globalThis, "_", {
value: value,
writable: true,
enumerable: true,
configurable: true,
});
console.log("Last evaluation result is no longer saved to _.");
},
});
// Configure globalThis._error to give the last thrown error.
Object.defineProperty(globalThis, "_error", {
configurable: true,
get: () => lastThrownError,
set: (value) => {
Object.defineProperty(globalThis, "_error", {
value: value,
writable: true,
enumerable: true,
configurable: true,
});
console.log("Last thrown error is no longer saved to _error.");
},
});
replLog(`Deno ${version.deno}`);
replLog("exit using ctrl+d or close()");
while (true) {
if (isCloseCalled()) {
quitRepl(0);
}
let code = "";
// Top level read
try {
code = await opReadline(rid, "> ");
if (code.trim() === "") {
continue;
}
} catch (err) {
if (err.message === "EOF") {
quitRepl(0);
} else {
// If interrupted, don't print error.
if (err.message !== "Interrupted") {
// e.g. this happens when we have deno.close(3).
// We want to display the problem.
const formattedError = core.formatError(err);
replError(formattedError);
}
// Quit REPL anyways.
quitRepl(1);
}
}
// Start continued read
while (!evaluate(code)) {
code += "\n";
try {
code += await opReadline(rid, " ");
} catch (err) {
// If interrupted on continued read,
// abort this read instead of quitting.
if (err.message === "Interrupted") {
break;
} else if (err.message === "EOF") {
quitRepl(0);
} else {
// e.g. this happens when we have deno.close(3).
// We want to display the problem.
const formattedError = core.formatError(err);
replError(formattedError);
quitRepl(1);
}
}
}
}
}
window.__bootstrap.repl = {
replLoop,
};
})(this);

View file

@ -14,7 +14,6 @@ delete Object.prototype.__proto__;
const errorStack = window.__bootstrap.errorStack;
const os = window.__bootstrap.os;
const timers = window.__bootstrap.timers;
const replLoop = window.__bootstrap.repl.replLoop;
const Console = window.__bootstrap.console.Console;
const worker = window.__bootstrap.worker;
const signals = window.__bootstrap.signals;
@ -294,8 +293,7 @@ delete Object.prototype.__proto__;
}
});
const { args, cwd, noColor, pid, ppid, repl, unstableFlag } =
runtimeStart();
const { args, cwd, noColor, pid, ppid, unstableFlag } = runtimeStart();
registerErrors();
@ -329,10 +327,6 @@ delete Object.prototype.__proto__;
util.log("cwd", cwd);
util.log("args", args);
if (repl) {
replLoop();
}
}
function bootstrapWorkerRuntime(name, useDenoNamespace, internalName) {

View file

@ -1239,6 +1239,7 @@ fn repl_test_function() {
}
#[test]
#[ignore]
fn repl_test_multiline() {
let (out, err) = util::run_and_collect_output(
true,
@ -1374,7 +1375,7 @@ fn repl_test_save_last_thrown() {
false,
);
assert!(out.ends_with("1\n"));
assert_eq!(err, "Thrown: 1\n");
assert_eq!(err, "Uncaught 1\n");
}
#[test]
@ -1404,7 +1405,7 @@ fn repl_test_assign_underscore_error() {
assert!(
out.ends_with("Last thrown error is no longer saved to _error.\n1\n1\n")
);
assert_eq!(err, "Thrown: 2\n");
assert_eq!(err, "Uncaught 2\n");
}
#[test]

View file

@ -134,7 +134,7 @@ impl Worker {
&mut isolate,
Some(inspector_server.clone()),
))
} else if global_state.flags.coverage {
} else if global_state.flags.coverage || global_state.flags.repl {
Some(DenoInspector::new(&mut isolate, None))
} else {
None
@ -309,7 +309,6 @@ impl MainWorker {
ops::permissions::init(&mut worker);
ops::plugin::init(&mut worker);
ops::process::init(&mut worker);
ops::repl::init(&mut worker);
ops::runtime_compiler::init(&mut worker);
ops::signal::init(&mut worker);
ops::tls::init(&mut worker);