Support scoped variables, unblock REPL async op, and REPL error colors (#1721)

This commit is contained in:
Kevin (Kun) "Kassimo" Qian 2019-02-09 13:55:40 -08:00 committed by Ryan Dahl
parent 1502051453
commit 1d36eb47eb
18 changed files with 329 additions and 54 deletions

View file

@ -68,6 +68,7 @@ ts_sources = [
"js/event.ts",
"js/event_target.ts",
"js/fetch.ts",
"js/format_error.ts",
"js/dom_file.ts",
"js/file_info.ts",
"js/files.ts",

View file

@ -3,6 +3,8 @@ import { isTypedArray } from "./util";
import { TextEncoder } from "./text_encoding";
import { File, stdout } from "./files";
import { cliTable } from "./console_table";
import { formatError } from "./format_error";
import { libdeno } from "./libdeno";
// tslint:disable-next-line:no-any
type ConsoleContext = Set<any>;
@ -263,7 +265,8 @@ function createObjectString(
...args: [ConsoleContext, number, number]
): string {
if (value instanceof Error) {
return value.stack! || "";
const errorJSON = libdeno.errorToJSON(value);
return formatError(errorJSON);
} else if (Array.isArray(value)) {
return createArrayString(value, ...args);
} else if (value instanceof Number) {

View file

@ -1,6 +1,6 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { Console, libdeno, stringifyArgs, inspect, write, stdout } from "deno";
import { test, assertEqual } from "./test_util.ts";
import { test, assertEqual, assert } from "./test_util.ts";
const console = new Console(libdeno.print);
@ -245,7 +245,8 @@ test(function consoleTestError() {
try {
throw new MyError("This is an error");
} catch (e) {
assertEqual(stringify(e).split("\n")[0], "MyError: This is an error");
assert(stringify(e).split("\n")[3]
.includes("MyError: This is an error"));
}
});

21
js/format_error.ts Normal file
View file

@ -0,0 +1,21 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import * as msg from "gen/msg_generated";
import * as flatbuffers from "./flatbuffers";
import { sendSync } from "./dispatch";
import { assert } from "./util";
export function formatError(errString: string): string {
const builder = flatbuffers.createBuilder();
const errString_ = builder.createString(errString);
msg.FormatError.startFormatError(builder);
msg.FormatError.addError(builder, errString_);
const offset = msg.FormatError.endFormatError(builder);
const baseRes = sendSync(builder, msg.Any.FormatError, offset);
assert(baseRes != null);
assert(msg.Any.FormatErrorRes === baseRes!.innerType());
const formatErrorResMsg = new msg.FormatErrorRes();
assert(baseRes!.inner(formatErrorResMsg) != null);
const formattedError = formatErrorResMsg.error();
assert(formatError != null);
return formattedError!;
}

View file

@ -3,11 +3,17 @@ import { globalEval } from "./global_eval";
// The libdeno functions are moved so that users can't access them.
type MessageCallback = (msg: Uint8Array) => void;
export type PromiseRejectEvent =
| "RejectWithNoHandler"
| "HandlerAddedAfterReject"
| "ResolveAfterResolved"
| "RejectAfterResolved";
interface EvalErrorInfo {
// Is the object thrown a native Error?
isNativeError: boolean;
// Was the error happened during compilation?
isCompileError: boolean;
// The actual thrown entity
// (might be an Error or anything else thrown by the user)
// If isNativeError is true, this is an Error
thrown: any; // tslint:disable-line:no-any
}
interface Libdeno {
recv(cb: MessageCallback): void;
@ -20,26 +26,17 @@ interface Libdeno {
builtinModules: { [s: string]: object };
setGlobalErrorHandler: (
handler: (
message: string,
source: string,
line: number,
col: number,
error: Error
) => void
) => void;
/** Evaluate provided code in the current context.
* It differs from eval(...) in that it does not create a new context.
* Returns an array: [output, errInfo].
* If an error occurs, `output` becomes null and `errInfo` is non-null.
*/
evalContext(
code: string
): [any, EvalErrorInfo | null] /* tslint:disable-line:no-any */;
setPromiseRejectHandler: (
handler: (
error: Error | string,
event: PromiseRejectEvent,
/* tslint:disable-next-line:no-any */
promise: Promise<any>
) => void
) => void;
setPromiseErrorExaminer: (handler: () => boolean) => void;
// tslint:disable-next-line:no-any
errorToJSON: (e: Error) => string;
}
const window = globalEval("this");

View file

@ -7,6 +7,8 @@ import { close } from "./files";
import * as dispatch from "./dispatch";
import { exit } from "./os";
import { globalEval } from "./global_eval";
import { libdeno } from "./libdeno";
import { formatError } from "./format_error";
const window = globalEval("this");
@ -96,14 +98,19 @@ export async function replLoop(): Promise<void> {
}
function evaluate(code: string): void {
try {
const result = eval.call(window, code); // FIXME use a new scope.
if (code.trim() === "") {
return;
}
const [result, errInfo] = libdeno.evalContext(code);
if (!errInfo) {
console.log(result);
} catch (err) {
if (err instanceof Error) {
console.error(`${err.constructor.name}: ${err.message}`);
} else {
console.error("Thrown:", err);
if (errInfo.isNativeError) {
const formattedError = formatError(
libdeno.errorToJSON(errInfo.thrown as Error));
console.error(formattedError);
} else {
console.error("Thrown:", errInfo.thrown);
}
}
}

View file

@ -9,6 +9,7 @@
#include "third_party/v8/src/base/logging.h"
#include "deno.h"
#include "exceptions.h"
#include "internal.h"
extern "C" {

View file

@ -120,6 +120,16 @@ void Print(const v8::FunctionCallbackInfo<v8::Value>& args) {
fflush(file);
}
void ErrorToJSON(const v8::FunctionCallbackInfo<v8::Value>& args) {
CHECK_EQ(args.Length(), 1);
auto* isolate = args.GetIsolate();
DenoIsolate* d = DenoIsolate::FromIsolate(isolate);
auto context = d->context_.Get(d->isolate_);
v8::HandleScope handle_scope(isolate);
auto json_string = EncodeExceptionAsJSON(context, args[0]);
args.GetReturnValue().Set(v8_str(json_string.c_str()));
}
v8::Local<v8::Uint8Array> ImportBuf(DenoIsolate* d, deno_buf buf) {
if (buf.alloc_ptr == nullptr) {
// If alloc_ptr isn't set, we memcpy.
@ -368,6 +378,80 @@ bool Execute(v8::Local<v8::Context> context, const char* js_filename,
return true;
}
static inline v8::Local<v8::Boolean> v8_bool(bool v) {
return v8::Boolean::New(v8::Isolate::GetCurrent(), v);
}
void EvalContext(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
DenoIsolate* d = DenoIsolate::FromIsolate(isolate);
v8::EscapableHandleScope handleScope(isolate);
auto context = d->context_.Get(isolate);
v8::Context::Scope context_scope(context);
CHECK(args[0]->IsString());
auto source = args[0].As<v8::String>();
auto output = v8::Array::New(isolate, 2);
/**
* output[0] = result
* output[1] = ErrorInfo | null
* ErrorInfo = {
* thrown: Error | any,
* isNativeError: boolean,
* isCompileError: boolean,
* }
*/
v8::TryCatch try_catch(isolate);
auto name = v8_str("<unknown>");
v8::ScriptOrigin origin(name);
auto script = v8::Script::Compile(context, source, &origin);
if (script.IsEmpty()) {
DCHECK(try_catch.HasCaught());
auto exception = try_catch.Exception();
output->Set(0, v8::Null(isolate));
auto errinfo_obj = v8::Object::New(isolate);
errinfo_obj->Set(v8_str("isCompileError"), v8_bool(true));
errinfo_obj->Set(v8_str("isNativeError"),
v8_bool(exception->IsNativeError()));
errinfo_obj->Set(v8_str("thrown"), exception);
output->Set(1, errinfo_obj);
args.GetReturnValue().Set(output);
return;
}
auto result = script.ToLocalChecked()->Run(context);
if (result.IsEmpty()) {
DCHECK(try_catch.HasCaught());
auto exception = try_catch.Exception();
output->Set(0, v8::Null(isolate));
auto errinfo_obj = v8::Object::New(isolate);
errinfo_obj->Set(v8_str("isCompileError"), v8_bool(false));
errinfo_obj->Set(v8_str("isNativeError"),
v8_bool(exception->IsNativeError()));
errinfo_obj->Set(v8_str("thrown"), exception);
output->Set(1, errinfo_obj);
args.GetReturnValue().Set(output);
return;
}
output->Set(0, result.ToLocalChecked());
output->Set(1, v8::Null(isolate));
args.GetReturnValue().Set(output);
}
void InitializeContext(v8::Isolate* isolate, v8::Local<v8::Context> context) {
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(context);
@ -389,6 +473,18 @@ void InitializeContext(v8::Isolate* isolate, v8::Local<v8::Context> context) {
auto send_val = send_tmpl->GetFunction(context).ToLocalChecked();
CHECK(deno_val->Set(context, deno::v8_str("send"), send_val).FromJust());
auto eval_context_tmpl = v8::FunctionTemplate::New(isolate, EvalContext);
auto eval_context_val =
eval_context_tmpl->GetFunction(context).ToLocalChecked();
CHECK(deno_val->Set(context, deno::v8_str("evalContext"), eval_context_val)
.FromJust());
auto error_to_json_tmpl = v8::FunctionTemplate::New(isolate, ErrorToJSON);
auto error_to_json_val =
error_to_json_tmpl->GetFunction(context).ToLocalChecked();
CHECK(deno_val->Set(context, deno::v8_str("errorToJSON"), error_to_json_val)
.FromJust());
CHECK(deno_val->SetAccessor(context, deno::v8_str("shared"), Shared)
.FromJust());

View file

@ -4,10 +4,10 @@
namespace deno {
std::string EncodeMessageAsJSON(v8::Local<v8::Context> context,
v8::Local<v8::Object> EncodeMessageAsObject(v8::Local<v8::Context> context,
v8::Local<v8::Message> message) {
auto* isolate = context->GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::EscapableHandleScope handle_scope(isolate);
v8::Context::Scope context_scope(context);
auto stack_trace = message->GetStackTrace();
@ -134,12 +134,33 @@ std::string EncodeMessageAsJSON(v8::Local<v8::Context> context,
}
CHECK(json_obj->Set(context, v8_str("frames"), frames).FromJust());
json_obj = handle_scope.Escape(json_obj);
return json_obj;
}
std::string EncodeMessageAsJSON(v8::Local<v8::Context> context,
v8::Local<v8::Message> message) {
auto* isolate = context->GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(context);
auto json_obj = EncodeMessageAsObject(context, message);
auto json_string = v8::JSON::Stringify(context, json_obj).ToLocalChecked();
v8::String::Utf8Value json_string_(isolate, json_string);
return std::string(ToCString(json_string_));
}
v8::Local<v8::Object> EncodeExceptionAsObject(v8::Local<v8::Context> context,
v8::Local<v8::Value> exception) {
auto* isolate = context->GetIsolate();
v8::EscapableHandleScope handle_scope(isolate);
v8::Context::Scope context_scope(context);
auto message = v8::Exception::CreateMessage(isolate, exception);
auto json_obj = EncodeMessageAsObject(context, message);
json_obj = handle_scope.Escape(json_obj);
return json_obj;
}
std::string EncodeExceptionAsJSON(v8::Local<v8::Context> context,
v8::Local<v8::Value> exception) {
auto* isolate = context->GetIsolate();
@ -167,5 +188,4 @@ void HandleExceptionMessage(v8::Local<v8::Context> context,
CHECK_NOT_NULL(d);
d->last_exception_ = json_str;
}
} // namespace deno

View file

@ -2,10 +2,17 @@
#ifndef EXCEPTIONS_H_
#define EXCEPTIONS_H_
#include <string>
#include "third_party/v8/include/v8.h"
namespace deno {
v8::Local<v8::Object> EncodeExceptionAsObject(v8::Local<v8::Context> context,
v8::Local<v8::Value> exception);
std::string EncodeExceptionAsJSON(v8::Local<v8::Context> context,
v8::Local<v8::Value> exception);
void HandleException(v8::Local<v8::Context> context,
v8::Local<v8::Value> exception);

View file

@ -133,6 +133,8 @@ static inline v8::Local<v8::String> v8_str(const char* x) {
void Print(const v8::FunctionCallbackInfo<v8::Value>& args);
void Recv(const v8::FunctionCallbackInfo<v8::Value>& args);
void Send(const v8::FunctionCallbackInfo<v8::Value>& args);
void EvalContext(const v8::FunctionCallbackInfo<v8::Value>& args);
void ErrorToJSON(const v8::FunctionCallbackInfo<v8::Value>& args);
void Shared(v8::Local<v8::Name> property,
const v8::PropertyCallbackInfo<v8::Value>& info);
void BuiltinModules(v8::Local<v8::Name> property,
@ -142,6 +144,8 @@ static intptr_t external_references[] = {
reinterpret_cast<intptr_t>(Print),
reinterpret_cast<intptr_t>(Recv),
reinterpret_cast<intptr_t>(Send),
reinterpret_cast<intptr_t>(EvalContext),
reinterpret_cast<intptr_t>(ErrorToJSON),
reinterpret_cast<intptr_t>(Shared),
reinterpret_cast<intptr_t>(BuiltinModules),
reinterpret_cast<intptr_t>(MessageCallback),
@ -153,9 +157,6 @@ Deno* NewFromSnapshot(void* user_data, deno_recv_cb cb);
void InitializeContext(v8::Isolate* isolate, v8::Local<v8::Context> context);
void HandleException(v8::Local<v8::Context> context,
v8::Local<v8::Value> exception);
void DeserializeInternalFields(v8::Local<v8::Object> holder, int index,
v8::StartupData payload, void* data);

View file

@ -290,6 +290,20 @@ TEST(LibDenoTest, Utf8Bug) {
deno_delete(d);
}
TEST(LibDenoTest, LibDenoEvalContext) {
Deno* d = deno_new(deno_config{0, snapshot, empty, nullptr});
deno_execute(d, nullptr, "a.js", "LibDenoEvalContext();");
EXPECT_EQ(nullptr, deno_last_exception(d));
deno_delete(d);
}
TEST(LibDenoTest, LibDenoEvalContextError) {
Deno* d = deno_new(deno_config{0, snapshot, empty, nullptr});
deno_execute(d, nullptr, "a.js", "LibDenoEvalContextError();");
EXPECT_EQ(nullptr, deno_last_exception(d));
deno_delete(d);
}
TEST(LibDenoTest, SharedAtomics) {
int32_t s[] = {0, 1, 2};
deno_buf shared = {nullptr, 0, reinterpret_cast<uint8_t*>(s), sizeof s};

View file

@ -147,3 +147,50 @@ global.Shared = () => {
ui8[1] = 43;
ui8[2] = 44;
};
global.LibDenoEvalContext = () => {
const [result, errInfo] = libdeno.evalContext("let a = 1; a");
assert(result === 1);
assert(!errInfo);
const [result2, errInfo2] = libdeno.evalContext("a = a + 1; a");
assert(result2 === 2);
assert(!errInfo2);
};
global.LibDenoEvalContextError = () => {
const [result, errInfo] = libdeno.evalContext("not_a_variable");
assert(!result);
assert(!!errInfo);
assert(errInfo.isNativeError); // is a native error (ReferenceError)
assert(!errInfo.isCompileError); // is NOT a compilation error
assert(errInfo.thrown.message === "not_a_variable is not defined");
const [result2, errInfo2] = libdeno.evalContext("throw 1");
assert(!result2);
assert(!!errInfo2);
assert(!errInfo2.isNativeError); // is NOT a native error
assert(!errInfo2.isCompileError); // is NOT a compilation error
assert(errInfo2.thrown === 1);
const [result3, errInfo3] =
libdeno.evalContext("class AError extends Error {}; throw new AError('e')");
assert(!result3);
assert(!!errInfo3);
assert(errInfo3.isNativeError); // extend from native error, still native error
assert(!errInfo3.isCompileError); // is NOT a compilation error
assert(errInfo3.thrown.message === "e");
const [result4, errInfo4] = libdeno.evalContext("{");
assert(!result4);
assert(!!errInfo4);
assert(errInfo4.isNativeError); // is a native error (SyntaxError)
assert(errInfo4.isCompileError); // is a compilation error! (braces not closed)
assert(errInfo4.thrown.message === "Unexpected end of input");
const [result5, errInfo5] = libdeno.evalContext("eval('{')");
assert(!result5);
assert(!!errInfo5);
assert(errInfo5.isNativeError); // is a native error (SyntaxError)
assert(!errInfo5.isCompileError); // is NOT a compilation error! (just eval)
assert(errInfo5.thrown.message === "Unexpected end of input");
};

View file

@ -178,5 +178,4 @@ extern "C" {
user_data: *const c_void,
id: deno_mod,
);
}

View file

@ -1,6 +1,8 @@
union Any {
Start,
StartRes,
FormatError,
FormatErrorRes,
WorkerGetMessage,
WorkerGetMessageRes,
WorkerPostMessage,
@ -163,6 +165,14 @@ table StartRes {
no_color: bool;
}
table FormatError {
error: string;
}
table FormatErrorRes {
error: string;
}
table WorkerGetMessage {
unused: int8;
}

View file

@ -10,6 +10,7 @@ use crate::isolate::Buf;
use crate::isolate::Isolate;
use crate::isolate::IsolateState;
use crate::isolate::Op;
use crate::js_errors::JSError;
use crate::libdeno;
use crate::msg;
use crate::msg_util;
@ -97,6 +98,7 @@ pub fn dispatch(
msg::Any::Environ => op_env,
msg::Any::Exit => op_exit,
msg::Any::Fetch => op_fetch,
msg::Any::FormatError => op_format_error,
msg::Any::Listen => op_listen,
msg::Any::MakeTempDir => op_make_temp_dir,
msg::Any::Metrics => op_metrics,
@ -283,6 +285,41 @@ fn op_start(
))
}
fn op_format_error(
state: &Arc<IsolateState>,
base: &msg::Base<'_>,
data: libdeno::deno_buf,
) -> Box<Op> {
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 = js_error.apply_source_map(&state.dir);
let js_error_string = 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<'_>,
@ -1271,7 +1308,8 @@ fn op_repl_readline(
debug!("op_repl_readline {} {}", rid, prompt);
blocking(base.sync(), move || -> OpResult {
let line = resources::readline(rid, &prompt)?;
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);

View file

@ -35,7 +35,7 @@ use std::net::{Shutdown, SocketAddr};
use std::process::ExitStatus;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Mutex;
use std::sync::{Arc, Mutex};
use tokio;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::TcpStream;
@ -95,7 +95,7 @@ enum Repr {
TcpListener(tokio::net::TcpListener, Option<futures::task::Task>),
TcpStream(tokio::net::TcpStream),
HttpBody(HttpBody),
Repl(Repl),
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
@ -334,7 +334,7 @@ pub fn add_hyper_body(body: hyper::Body) -> Resource {
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));
let r = tg.insert(rid, Repr::Repl(Arc::new(Mutex::new(repl))));
assert!(r.is_none());
Resource { rid }
}
@ -462,14 +462,11 @@ pub fn child_status(rid: ResourceId) -> DenoResult<ChildStatus> {
}
}
pub fn readline(rid: ResourceId, prompt: &str) -> DenoResult<String> {
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)) => {
let line = r.readline(&prompt)?;
Ok(line)
}
Some(Repr::Repl(ref mut r)) => Ok(r.clone()),
_ => Err(bad_resource()),
}
}

View file

@ -19,7 +19,7 @@ class Repl(object):
def input(self, *lines, **kwargs):
exit_ = kwargs.pop("exit", True)
sleep_ = kwargs.pop("sleep", 0)
p = Popen([self.deno_exe], stdout=PIPE, stderr=PIPE, stdin=PIPE)
p = Popen([self.deno_exe, "-A"], stdout=PIPE, stderr=PIPE, stdin=PIPE)
try:
# Note: The repl takes a >100ms until it's ready.
time.sleep(sleep_)
@ -87,7 +87,7 @@ class Repl(object):
def test_reference_error(self):
out, err, code = self.input("not_a_variable")
assertEqual(out, '')
assertEqual(err, 'ReferenceError: not_a_variable is not defined\n')
assert "not_a_variable is not defined" in err
assertEqual(code, 0)
def test_set_timeout(self):
@ -108,16 +108,25 @@ class Repl(object):
assertEqual(err, '')
assertEqual(code, 0)
def test_async_op(self):
out, err, code = self.input(
"fetch('http://localhost:4545/tests/001_hello.js')" +
".then(res => res.text()).then(console.log)",
sleep=1)
assertEqual(out, 'Promise {}\nconsole.log("Hello World");\n\n')
assertEqual(err, '')
assertEqual(code, 0)
def test_syntax_error(self):
out, err, code = self.input("syntax error")
assertEqual(out, '')
assertEqual(err, "SyntaxError: Unexpected identifier\n")
assert "Unexpected identifier" in err
assertEqual(code, 0)
def test_type_error(self):
out, err, code = self.input("console()")
assertEqual(out, '')
assertEqual(err, 'TypeError: console is not a function\n')
assert "console is not a function" in err
assertEqual(code, 0)
def test_variable(self):
@ -126,6 +135,12 @@ class Repl(object):
assertEqual(err, '')
assertEqual(code, 0)
def test_lexical_scoped_variable(self):
out, err, code = self.input("let a = 123;", "a")
assertEqual(out, 'undefined\n123\n')
assertEqual(err, '')
assertEqual(code, 0)
def assertEqual(left, right):
if left != right: