diff options
author | Ryan Dahl <ry@tinyclouds.org> | 2018-12-06 23:05:36 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-12-06 23:05:36 -0500 |
commit | c113df1bb8a0c7d0c560ad32c0291c918c7da7b4 (patch) | |
tree | 0d15de448be602c22aecb2ec65ac7667c437a209 | |
parent | 568ac0c9026b6f4012e2511a026bb5eb31a06020 (diff) |
Process source maps in Rust instead of JS (#1280)
- Improves speed and binary size significantly.
- Makes deno_last_exception() output a JSON structure.
- Isolate::execute and Isolate::event_loop now return
structured, mapped JSError objects on errors.
- Removes libdeno functions:
libdeno.setGlobalErrorHandler()
libdeno.setPromiseRejectHandler()
libdeno.setPromiseErrorExaminer()
In collaboration with Ryan Dahl.
-rw-r--r-- | BUILD.gn | 4 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | build_extra/rust/BUILD.gn | 42 | ||||
-rw-r--r-- | js/compiler.ts | 48 | ||||
-rw-r--r-- | js/libdeno.ts | 4 | ||||
-rw-r--r-- | js/main.ts | 33 | ||||
-rw-r--r-- | js/promise_util.ts | 50 | ||||
-rw-r--r-- | js/unit_tests.ts | 1 | ||||
-rw-r--r-- | js/v8_source_maps.ts | 285 | ||||
-rw-r--r-- | js/v8_source_maps_test.ts | 17 | ||||
-rw-r--r-- | libdeno/api.cc | 37 | ||||
-rw-r--r-- | libdeno/binding.cc | 312 | ||||
-rw-r--r-- | libdeno/deno.h | 14 | ||||
-rw-r--r-- | libdeno/internal.h | 19 | ||||
-rw-r--r-- | libdeno/libdeno_test.cc | 65 | ||||
-rw-r--r-- | libdeno/libdeno_test.js | 48 | ||||
-rw-r--r-- | libdeno/snapshot_creator.cc | 4 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | src/deno_dir.rs | 20 | ||||
-rw-r--r-- | src/isolate.rs | 42 | ||||
-rw-r--r-- | src/js_errors.rs | 543 | ||||
-rw-r--r-- | src/main.rs | 17 | ||||
-rw-r--r-- | tests/async_error.ts.out | 5 | ||||
-rw-r--r-- | tests/error_001.ts.out | 5 | ||||
-rw-r--r-- | tests/error_002.ts.out | 8 | ||||
-rw-r--r-- | tests/error_004_missing_module.ts.out | 12 | ||||
-rw-r--r-- | tests/error_005_missing_dynamic_import.ts.out | 12 | ||||
-rw-r--r-- | tests/error_006_import_ext_failure.ts.out | 12 | ||||
-rw-r--r-- | tests/error_007_any.ts.out | 2 | ||||
m--------- | third_party | 0 |
30 files changed, 856 insertions, 809 deletions
@@ -39,6 +39,8 @@ main_extern = [ "$rust_build:remove_dir_all", "$rust_build:ring", "$rust_build:rustyline", + "$rust_build:serde_json", + "$rust_build:source_map_mappings", "$rust_build:tempfile", "$rust_build:tokio", "$rust_build:tokio_executor", @@ -84,7 +86,6 @@ ts_sources = [ "js/platform.ts", "js/plugins.d.ts", "js/process.ts", - "js/promise_util.ts", "js/read_dir.ts", "js/read_file.ts", "js/read_link.ts", @@ -101,7 +102,6 @@ ts_sources = [ "js/types.ts", "js/url_search_params.ts", "js/util.ts", - "js/v8_source_maps.ts", "js/write_file.ts", "tsconfig.json", diff --git a/Cargo.toml b/Cargo.toml index 82aab94ff..d7cd0a5fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,8 @@ rand = "=0.6.1" remove_dir_all = "=0.5.1" rustyline = "=2.1.0" ring = "=0.13.5" +serde_json = "1.0.33" +source-map-mappings = "0.5.0" tempfile = "=3.0.5" tokio = "=0.1.13" tokio-executor = "=0.1.5" diff --git a/build_extra/rust/BUILD.gn b/build_extra/rust/BUILD.gn index de6a01c20..f872dccb1 100644 --- a/build_extra/rust/BUILD.gn +++ b/build_extra/rust/BUILD.gn @@ -1101,3 +1101,45 @@ rust_crate("tokio_process") { ] } } + +rust_crate("vlq") { + source_root = "$registry_github/vlq-0.5.1/src/lib.rs" +} + +rust_crate("source_map_mappings") { + source_root = "$registry_github/source-map-mappings-0.5.0/src/lib.rs" + extern = [ + ":rand", + ":vlq", + ] +} + +rust_crate("ryu") { + source_root = "$registry_github/ryu-0.2.7/src/lib.rs" + features = [ "small" ] +} + +rust_crate("serde") { + source_root = "$registry_github/serde-1.0.80/src/lib.rs" + features = [ + "default", + "std", + ] +} + +rust_crate("serde_json") { + source_root = "$registry_github/serde_json-1.0.33/src/lib.rs" + features = [ + "arbitrary_precision", + "default", + "preserve_order", + "indexmap", + "raw_value", + ] + extern = [ + ":indexmap", + ":itoa", + ":ryu", + ":serde", + ] +} diff --git a/js/compiler.ts b/js/compiler.ts index c9fa4611c..e0bfb77bb 100644 --- a/js/compiler.ts +++ b/js/compiler.ts @@ -3,10 +3,8 @@ import * as ts from "typescript"; import { MediaType } from "gen/msg_generated"; import { assetSourceCode } from "./assets"; -import { libdeno } from "./libdeno"; import * as os from "./os"; import { CodeProvider } from "./runner"; -import { RawSourceMap } from "./types"; import { assert, log, notImplemented } from "./util"; const EOL = "\n"; @@ -71,7 +69,7 @@ export class ModuleMetaData implements ts.IScriptSnapshot { public readonly mediaType: MediaType, public readonly sourceCode: SourceCode = "", public outputCode: OutputCode = "", - public sourceMap: SourceMap | RawSourceMap = "" + public sourceMap: SourceMap = "" ) { if (outputCode !== "" || fileName.endsWith(".d.ts")) { this.scriptVersion = "1"; @@ -131,8 +129,6 @@ export class Compiler ContainingFile, Map<ModuleSpecifier, ModuleFileName> >(); - // Keep track of state of the last module requested via `getGeneratedContents` - private _lastModule: ModuleMetaData | undefined; // A reference to the log utility, so it can be monkey patched during testing private _log = log; // A map of module file names to module meta data @@ -369,19 +365,15 @@ export class Compiler moduleMetaData.outputCode = `${ outputFile.text }\n//# sourceURL=${fileName}`; - moduleMetaData.sourceMap = JSON.parse(sourceMapFile.text); + moduleMetaData.sourceMap = sourceMapFile.text; } moduleMetaData.scriptVersion = "1"; - const sourceMap = - moduleMetaData.sourceMap === "string" - ? moduleMetaData.sourceMap - : JSON.stringify(moduleMetaData.sourceMap); this._os.codeCache( fileName, sourceCode, moduleMetaData.outputCode, - sourceMap + moduleMetaData.sourceMap ); return moduleMetaData.outputCode; } @@ -398,36 +390,10 @@ export class Compiler } /** Given a fileName, return what was generated by the compiler. */ - getGeneratedContents = (fileName: string): string | RawSourceMap => { - this._log("compiler.getGeneratedContents", fileName); - if (fileName === "gen/bundle/main.js") { - assert(libdeno.mainSource.length > 0); - return libdeno.mainSource; - } else if (fileName === "main.js.map") { - return libdeno.mainSourceMap; - } else if (fileName === "deno_main.js") { - return ""; - } else if (!fileName.endsWith(".map")) { - const moduleMetaData = this._moduleMetaDataMap.get(fileName); - if (!moduleMetaData) { - this._lastModule = undefined; - return ""; - } - this._lastModule = moduleMetaData; - return moduleMetaData.outputCode; - } else { - if (this._lastModule && this._lastModule.sourceMap) { - // Assuming the the map will always be asked for after the source - // code. - const { sourceMap } = this._lastModule; - this._lastModule = undefined; - return sourceMap; - } else { - // Errors thrown here are caught by source-map. - throw new Error(`Unable to find source map: "${fileName}"`); - } - } - }; + getGeneratedSourceMap(fileName: string): string { + const moduleMetaData = this._moduleMetaDataMap.get(fileName); + return moduleMetaData ? moduleMetaData.sourceMap : ""; + } /** Get the output code for a module based on its filename. A call to * `.getFilename()` should occur before attempting to get the output code as diff --git a/js/libdeno.ts b/js/libdeno.ts index c61027324..5448165a1 100644 --- a/js/libdeno.ts +++ b/js/libdeno.ts @@ -1,5 +1,4 @@ // Copyright 2018 the Deno authors. All rights reserved. MIT license. -import { RawSourceMap } from "./types"; import { globalEval } from "./global_eval"; // The libdeno functions are moved so that users can't access them. @@ -39,9 +38,6 @@ interface Libdeno { ) => void; setPromiseErrorExaminer: (handler: () => boolean) => void; - - mainSource: string; - mainSourceMap: RawSourceMap; } const window = globalEval("this"); diff --git a/js/main.ts b/js/main.ts index 241e8d7d1..818713593 100644 --- a/js/main.ts +++ b/js/main.ts @@ -11,22 +11,9 @@ import { Runner } from "./runner"; import { libdeno } from "./libdeno"; import { args } from "./deno"; import { sendSync, handleAsyncMsgFromRust } from "./dispatch"; -import { promiseErrorExaminer, promiseRejectHandler } from "./promise_util"; import { replLoop } from "./repl"; -import * as sourceMaps from "./v8_source_maps"; import { version } from "typescript"; -// Install the source maps handler and do some pre-calculations so all of it is -// available in the snapshot -const compiler = Compiler.instance(); -sourceMaps.install({ - installPrepareStackTrace: true, - getGeneratedContents: compiler.getGeneratedContents -}); -const consumer = sourceMaps.loadConsumer("gen/bundle/main.js"); -assert(consumer != null); -consumer!.computeColumnSpans(); - function sendStart(): msg.StartRes { const builder = flatbuffers.createBuilder(); msg.Start.startStart(builder); @@ -39,27 +26,9 @@ function sendStart(): msg.StartRes { return startRes; } -function onGlobalError( - message: string, - source: string, - lineno: number, - colno: number, - error: any // tslint:disable-line:no-any -) { - if (error instanceof Error) { - console.log(error.stack); - } else { - console.log(`Thrown: ${String(error)}`); - } - os.exit(1); -} - /* tslint:disable-next-line:no-default-export */ export default function denoMain() { libdeno.recv(handleAsyncMsgFromRust); - libdeno.setGlobalErrorHandler(onGlobalError); - libdeno.setPromiseRejectHandler(promiseRejectHandler); - libdeno.setPromiseErrorExaminer(promiseErrorExaminer); // First we send an empty "Start" message to let the privileged side know we // are ready. The response should be a "StartRes" message containing the CLI @@ -68,6 +37,8 @@ export default function denoMain() { setLogDebug(startResMsg.debugFlag()); + const compiler = Compiler.instance(); + // handle `--types` if (startResMsg.typesFlag()) { const defaultLibFileName = compiler.getDefaultLibFileName(); diff --git a/js/promise_util.ts b/js/promise_util.ts deleted file mode 100644 index 3c789124d..000000000 --- a/js/promise_util.ts +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2018 the Deno authors. All rights reserved. MIT license. -import { PromiseRejectEvent } from "./libdeno"; - -/* tslint:disable-next-line:no-any */ -const rejectMap = new Map<Promise<any>, string>(); -// For uncaught promise rejection errors - -/* tslint:disable-next-line:no-any */ -const otherErrorMap = new Map<Promise<any>, string>(); -// For reject after resolve / resolve after resolve errors - -export function promiseRejectHandler( - error: Error | string, - event: PromiseRejectEvent, - /* tslint:disable-next-line:no-any */ - promise: Promise<any> -) { - switch (event) { - case "RejectWithNoHandler": - rejectMap.set(promise, (error as Error).stack || "RejectWithNoHandler"); - break; - case "HandlerAddedAfterReject": - rejectMap.delete(promise); - break; - case "ResolveAfterResolved": - // Should not warn. See #1272 - break; - default: - // error is string here - otherErrorMap.set(promise, `Promise warning: ${error as string}`); - } -} - -// Return true when continue, false to die on uncaught promise reject -export function promiseErrorExaminer(): boolean { - if (otherErrorMap.size > 0) { - for (const msg of otherErrorMap.values()) { - console.log(msg); - } - otherErrorMap.clear(); - } - if (rejectMap.size > 0) { - for (const msg of rejectMap.values()) { - console.log(msg); - } - rejectMap.clear(); - return false; - } - return true; -} diff --git a/js/unit_tests.ts b/js/unit_tests.ts index 4863b7ac4..55877091e 100644 --- a/js/unit_tests.ts +++ b/js/unit_tests.ts @@ -35,5 +35,4 @@ import "./text_encoding_test.ts"; import "./timers_test.ts"; import "./truncate_test.ts"; import "./url_search_params_test.ts"; -import "./v8_source_maps_test.ts"; import "./write_file_test.ts"; diff --git a/js/v8_source_maps.ts b/js/v8_source_maps.ts deleted file mode 100644 index dcdde16c7..000000000 --- a/js/v8_source_maps.ts +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright 2014 Evan Wallace -// Copyright 2018 the Deno authors. All rights reserved. MIT license. -// Originated from source-map-support but has been heavily modified for deno. - -import { SourceMapConsumer, MappedPosition } from "source-map"; -import { CallSite, RawSourceMap } from "./types"; -import { atob } from "./text_encoding"; - -const consumers = new Map<string, SourceMapConsumer>(); - -interface Options { - // A callback the returns generated file contents. - getGeneratedContents: GetGeneratedContentsCallback; - // Usually set the following to true. Set to false for testing. - installPrepareStackTrace: boolean; -} - -interface Position { - source: string; // Filename - column: number; - line: number; -} - -type GetGeneratedContentsCallback = (fileName: string) => string | RawSourceMap; - -let getGeneratedContents: GetGeneratedContentsCallback; - -// @internal -export function install(options: Options) { - getGeneratedContents = options.getGeneratedContents; - if (options.installPrepareStackTrace) { - Error.prepareStackTrace = prepareStackTraceWrapper; - } -} - -// @internal -export function prepareStackTraceWrapper( - error: Error, - stack: CallSite[] -): string { - try { - return prepareStackTrace(error, stack); - } catch (prepareStackError) { - Error.prepareStackTrace = undefined; - console.log("=====Error inside of prepareStackTrace===="); - console.log(prepareStackError.stack.toString()); - console.log("=====Original error======================="); - throw error; - } -} - -// @internal -export function prepareStackTrace(error: Error, stack: CallSite[]): string { - const frames = stack.map( - frame => `\n at ${wrapCallSite(frame).toString()}` - ); - return `${error.toString()}${frames.join("")}`; -} - -// @internal -export function wrapCallSite(frame: CallSite): CallSite { - if (frame.isNative()) { - return frame; - } - - // Most call sites will return the source file from getFileName(), but code - // passed to eval() ending in "//# sourceURL=..." will return the source file - // from getScriptNameOrSourceURL() instead - const source = frame.getFileName() || frame.getScriptNameOrSourceURL(); - - if (source) { - const line = frame.getLineNumber() || 0; - const column = (frame.getColumnNumber() || 1) - 1; - const position = mapSourcePosition({ source, line, column }); - frame = cloneCallSite(frame); - Object.assign(frame, { - getFileName: () => position.source, - getLineNumber: () => position.line, - getColumnNumber: () => Number(position.column) + 1, - getScriptNameOrSourceURL: () => position.source, - toString: () => CallSiteToString(frame) - }); - return frame; - } - - // Code called using eval() needs special handling - let origin = (frame.isEval() && frame.getEvalOrigin()) || undefined; - if (origin) { - origin = mapEvalOrigin(origin); - frame = cloneCallSite(frame); - Object.assign(frame, { - getEvalOrigin: () => origin, - toString: () => CallSiteToString(frame) - }); - return frame; - } - - // If we get here then we were unable to change the source position - return frame; -} - -function cloneCallSite( - frame: CallSite - // mixin: Partial<CallSite> & { toString: () => string } -): CallSite { - const obj = {} as CallSite; - const props = Object.getOwnPropertyNames( - Object.getPrototypeOf(frame) - ) as Array<keyof CallSite>; - for (const name of props) { - obj[name] = /^(?:is|get)/.test(name) - ? () => frame[name].call(frame) - : frame[name]; - } - return obj; -} - -// Taken from source-map-support, original copied from V8's messages.js -// MIT License. Copyright (c) 2014 Evan Wallace -function CallSiteToString(frame: CallSite): string { - let fileLocation = ""; - if (frame.isNative()) { - fileLocation = "native"; - } else { - const fileName = frame.getScriptNameOrSourceURL(); - if (!fileName && frame.isEval()) { - fileLocation = frame.getEvalOrigin() || ""; - fileLocation += ", "; // Expecting source position to follow. - } - - if (fileName) { - fileLocation += fileName; - } else { - // Source code does not originate from a file and is not native, but we - // can still get the source position inside the source string, e.g. in - // an eval string. - fileLocation += "<anonymous>"; - } - const lineNumber = frame.getLineNumber(); - if (lineNumber != null) { - fileLocation += `:${lineNumber}`; - const columnNumber = frame.getColumnNumber(); - if (columnNumber) { - fileLocation += `:${columnNumber}`; - } - } - } - - let line = ""; - const functionName = frame.getFunctionName(); - let addSuffix = true; - const isConstructor = frame.isConstructor(); - const isMethodCall = !(frame.isToplevel() || isConstructor); - if (isMethodCall) { - let typeName = frame.getTypeName(); - // Fixes shim to be backward compatible with Node v0 to v4 - if (typeName === "[object Object]") { - typeName = "null"; - } - const methodName = frame.getMethodName(); - if (functionName) { - if (typeName && functionName.indexOf(typeName) !== 0) { - line += `${typeName}.`; - } - line += functionName; - if ( - methodName && - functionName.indexOf("." + methodName) !== - functionName.length - methodName.length - 1 - ) { - line += ` [as ${methodName} ]`; - } - } else { - line += `${typeName}.${methodName || "<anonymous>"}`; - } - } else if (isConstructor) { - line += `new ${functionName || "<anonymous>"}`; - } else if (functionName) { - line += functionName; - } else { - line += fileLocation; - addSuffix = false; - } - if (addSuffix) { - line += ` (${fileLocation})`; - } - return line; -} - -// Regex for detecting source maps -const reSourceMap = /^data:application\/json[^,]+base64,/; - -export function loadConsumer(source: string): SourceMapConsumer | null { - let consumer = consumers.get(source); - if (consumer == null) { - const code = getGeneratedContents(source); - if (!code) { - return null; - } - if (typeof code !== "string") { - throw new Error("expected string"); - } - - let sourceMappingURL = retrieveSourceMapURL(code); - if (!sourceMappingURL) { - throw Error("No source map?"); - } - - let sourceMapData: string | RawSourceMap; - if (reSourceMap.test(sourceMappingURL)) { - // Support source map URL as a data url - const rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(",") + 1); - sourceMapData = atob(rawData); - sourceMappingURL = source; - } else { - // TODO Support source map URLs relative to the source URL - // sourceMappingURL = supportRelativeURL(source, sourceMappingURL); - sourceMapData = getGeneratedContents(sourceMappingURL); - } - - const rawSourceMap = - typeof sourceMapData === "string" - ? (JSON.parse(sourceMapData) as RawSourceMap) - : sourceMapData; - consumer = new SourceMapConsumer(rawSourceMap); - consumers.set(source, consumer); - } - return consumer; -} - -// tslint:disable-next-line:max-line-length -const sourceMapUrlRe = /(?:\/\/[@#][ \t]+sourceMappingURL=([^\s'"]+?)[ \t]*$)|(?:\/\*[@#][ \t]+sourceMappingURL=([^\*]+?)[ \t]*(?:\*\/)[ \t]*$)/gm; - -function retrieveSourceMapURL(fileData: string): string | null { - // Keep executing the search to find the *last* sourceMappingURL to avoid - // picking up sourceMappingURLs from comments, strings, etc. - let lastMatch, match; - while ((match = sourceMapUrlRe.exec(fileData))) { - lastMatch = match; - } - if (!lastMatch) { - return null; - } - return lastMatch[1]; -} - -export function mapSourcePosition(position: Position): MappedPosition { - const consumer = loadConsumer(position.source); - if (consumer == null) { - return position; - } - return consumer.originalPositionFor(position); -} - -const stackEvalRe = /^eval at ([^(]+) \((.+):(\d+):(\d+)\)$/; -const nestedEvalRe = /^eval at ([^(]+) \((.+)\)$/; - -// Parses code generated by FormatEvalOrigin(), a function inside V8: -// https://code.google.com/p/v8/source/browse/trunk/src/messages.js -function mapEvalOrigin(origin: string): string { - // Most eval() calls are in this format - let match = stackEvalRe.exec(origin); - if (match) { - const position = mapSourcePosition({ - source: match[2], - line: Number(match[3]), - column: Number(match[4]) - 1 - }); - const pos = [ - position.source, - position.line, - Number(position.column) + 1 - ].join(":"); - return `eval at ${match[1]} (${pos})`; - } - - // Parse nested eval() calls using recursion - match = nestedEvalRe.exec(origin); - if (match) { - return `eval at ${match[1]} (${mapEvalOrigin(match[2])})`; - } - - // Make sure we still return useful information if we didn't find anything - return origin; -} diff --git a/js/v8_source_maps_test.ts b/js/v8_source_maps_test.ts deleted file mode 100644 index 90c123431..000000000 --- a/js/v8_source_maps_test.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2018 the Deno authors. All rights reserved. MIT license. -import { test, assert, assertEqual } from "./test_util.ts"; - -// This test demonstrates a bug: -// https://github.com/denoland/deno/issues/808 -test(function evalErrorFormatted() { - let err; - try { - eval("boom"); - } catch (e) { - err = e; - } - assert(!!err); - // tslint:disable-next-line:no-unused-expression - err.stack; // This would crash if err.stack is malformed - assertEqual(err.name, "ReferenceError"); -}); diff --git a/libdeno/api.cc b/libdeno/api.cc index 6f4ac826f..388ab6146 100644 --- a/libdeno/api.cc +++ b/libdeno/api.cc @@ -38,7 +38,7 @@ Deno* deno_new(deno_buf snapshot, deno_config config) { if (!snapshot.data_ptr) { // If no snapshot is provided, we initialize the context with empty // main source code and source maps. - deno::InitializeContext(isolate, context, "", "", ""); + deno::InitializeContext(isolate, context, "", ""); } d->context_.Reset(isolate, context); } @@ -47,7 +47,7 @@ Deno* deno_new(deno_buf snapshot, deno_config config) { } Deno* deno_new_snapshotter(deno_config config, const char* js_filename, - const char* js_source, const char* source_map) { + const char* js_source) { auto* creator = new v8::SnapshotCreator(deno::external_references); auto* isolate = creator->GetIsolate(); auto* d = new deno::DenoIsolate(deno::empty_buf, config); @@ -61,8 +61,7 @@ Deno* deno_new_snapshotter(deno_config config, const char* js_filename, creator->SetDefaultContext(context, v8::SerializeInternalFieldsCallback( deno::SerializeInternalFields, nullptr)); - deno::InitializeContext(isolate, context, js_filename, js_source, - source_map); + deno::InitializeContext(isolate, context, js_filename, js_source); } return reinterpret_cast<Deno*>(d); } @@ -96,7 +95,11 @@ void deno_set_v8_flags(int* argc, char** argv) { const char* deno_last_exception(Deno* d_) { auto* d = unwrap(d_); - return d->last_exception_.c_str(); + if (d->last_exception_.length() > 0) { + return d->last_exception_.c_str(); + } else { + return nullptr; + } } int deno_execute(Deno* d_, void* user_data, const char* js_filename, @@ -154,31 +157,19 @@ int deno_respond(Deno* d_, void* user_data, int32_t req_id, deno_buf buf) { void deno_check_promise_errors(Deno* d_) { auto* d = unwrap(d_); - if (d->pending_promise_events_ > 0) { + if (d->pending_promise_map_.size() > 0) { auto* isolate = d->isolate_; v8::Locker locker(isolate); v8::Isolate::Scope isolate_scope(isolate); v8::HandleScope handle_scope(isolate); - auto context = d->context_.Get(d->isolate_); v8::Context::Scope context_scope(context); - v8::TryCatch try_catch(d->isolate_); - auto promise_error_examiner_ = d->promise_error_examiner_.Get(d->isolate_); - if (promise_error_examiner_.IsEmpty()) { - d->last_exception_ = - "libdeno.setPromiseErrorExaminer has not been called."; - return; - } - v8::Local<v8::Value> args[0]; - auto result = promise_error_examiner_->Call(context->Global(), 0, args); - if (try_catch.HasCaught()) { - deno::HandleException(context, try_catch.Exception()); - } - d->pending_promise_events_ = 0; // reset - if (!result->BooleanValue(context).FromJust()) { - // Has uncaught promise reject error, exiting... - exit(1); + auto it = d->pending_promise_map_.begin(); + while (it != d->pending_promise_map_.end()) { + auto error = it->second.Get(isolate); + deno::HandleException(context, error); + it = d->pending_promise_map_.erase(it); } } } diff --git a/libdeno/binding.cc b/libdeno/binding.cc index b5d7ecafa..2a68d89aa 100644 --- a/libdeno/binding.cc +++ b/libdeno/binding.cc @@ -72,95 +72,94 @@ static inline v8::Local<v8::String> v8_str(const char* x) { .ToLocalChecked(); } -void HandleExceptionStr(v8::Local<v8::Context> context, - v8::Local<v8::Value> exception, - std::string* exception_str) { +std::string EncodeExceptionAsJSON(v8::Local<v8::Context> context, + v8::Local<v8::Value> exception) { auto* isolate = context->GetIsolate(); - DenoIsolate* d = FromIsolate(isolate); - v8::HandleScope handle_scope(isolate); v8::Context::Scope context_scope(context); auto message = v8::Exception::CreateMessage(isolate, exception); auto stack_trace = message->GetStackTrace(); - auto line = - v8::Integer::New(isolate, message->GetLineNumber(context).FromJust()); - auto column = - v8::Integer::New(isolate, message->GetStartColumn(context).FromJust()); - - auto global_error_handler_ = d->global_error_handler_.Get(isolate); - - if (!global_error_handler_.IsEmpty()) { - // global_error_handler_ is set so we try to handle the exception in - // javascript. - v8::Local<v8::Value> args[5]; - args[0] = exception->ToString(context).ToLocalChecked(); - args[1] = message->GetScriptResourceName(); - args[2] = line; - args[3] = column; - args[4] = exception; - global_error_handler_->Call(context->Global(), 5, args); - /* message, source, lineno, colno, error */ - return; - } + // Encode the exception into a JS object, which we will then turn into JSON. + auto json_obj = v8::Object::New(isolate); + + auto exception_str = exception->ToString(context).ToLocalChecked(); + // Alternate and very similar string. Not sure which is appropriate. + // auto exception_str = message->Get(); + CHECK(json_obj->Set(context, v8_str("message"), exception_str).FromJust()); - char buf[12 * 1024]; + v8::Local<v8::Array> frames; if (!stack_trace.IsEmpty()) { - // No javascript error handler, but we do have a stack trace. Format it - // into a string and add to last_exception_. - std::string msg; - v8::String::Utf8Value exceptionStr(isolate, exception); - msg += ToCString(exceptionStr); - msg += "\n"; - - for (int i = 0; i < stack_trace->GetFrameCount(); ++i) { + uint32_t count = static_cast<uint32_t>(stack_trace->GetFrameCount()); + frames = v8::Array::New(isolate, count); + + for (uint32_t i = 0; i < count; ++i) { auto frame = stack_trace->GetFrame(isolate, i); - v8::String::Utf8Value script_name(isolate, frame->GetScriptName()); - int l = frame->GetLineNumber(); - int c = frame->GetColumn(); - snprintf(buf, sizeof(buf), "%s %d:%d\n", ToCString(script_name), l, c); - msg += buf; + auto frame_obj = v8::Object::New(isolate); + CHECK(frames->Set(context, i, frame_obj).FromJust()); + auto line = v8::Integer::New(isolate, frame->GetLineNumber()); + auto column = v8::Integer::New(isolate, frame->GetColumn()); + CHECK(frame_obj->Set(context, v8_str("line"), line).FromJust()); + CHECK(frame_obj->Set(context, v8_str("column"), column).FromJust()); + CHECK(frame_obj + ->Set(context, v8_str("functionName"), frame->GetFunctionName()) + .FromJust()); + CHECK(frame_obj + ->Set(context, v8_str("scriptName"), + frame->GetScriptNameOrSourceURL()) + .FromJust()); + CHECK(frame_obj + ->Set(context, v8_str("isEval"), + v8::Boolean::New(isolate, frame->IsEval())) + .FromJust()); + CHECK(frame_obj + ->Set(context, v8_str("isConstructor"), + v8::Boolean::New(isolate, frame->IsConstructor())) + .FromJust()); + CHECK(frame_obj + ->Set(context, v8_str("isWasm"), + v8::Boolean::New(isolate, frame->IsWasm())) + .FromJust()); } - *exception_str += msg; } else { - // No javascript error handler, no stack trace. Format the little info we - // have into a string and add to last_exception_. - v8::String::Utf8Value exceptionStr(isolate, exception); - v8::String::Utf8Value script_name(isolate, - message->GetScriptResourceName()); - v8::String::Utf8Value line_str(isolate, line); - v8::String::Utf8Value col_str(isolate, column); - snprintf(buf, sizeof(buf), "%s\n%s %s:%s\n", ToCString(exceptionStr), - ToCString(script_name), ToCString(line_str), ToCString(col_str)); - *exception_str += buf; + // No stack trace. We only have one stack frame of info.. + frames = v8::Array::New(isolate, 1); + + auto frame_obj = v8::Object::New(isolate); + CHECK(frames->Set(context, 0, frame_obj).FromJust()); + + auto line = + v8::Integer::New(isolate, message->GetLineNumber(context).FromJust()); + auto column = + v8::Integer::New(isolate, message->GetStartColumn(context).FromJust()); + + CHECK(frame_obj->Set(context, v8_str("line"), line).FromJust()); + CHECK(frame_obj->Set(context, v8_str("column"), column).FromJust()); + CHECK(frame_obj + ->Set(context, v8_str("scriptName"), + message->GetScriptResourceName()) + .FromJust()); } + + CHECK(json_obj->Set(context, v8_str("frames"), frames).FromJust()); + + auto json_string = v8::JSON::Stringify(context, json_obj).ToLocalChecked(); + v8::String::Utf8Value json_string_(isolate, json_string); + return std::string(ToCString(json_string_)); } void HandleException(v8::Local<v8::Context> context, v8::Local<v8::Value> exception) { v8::Isolate* isolate = context->GetIsolate(); DenoIsolate* d = FromIsolate(isolate); - std::string exception_str; - HandleExceptionStr(context, exception, &exception_str); + std::string json_str = EncodeExceptionAsJSON(context, exception); if (d != nullptr) { - d->last_exception_ = exception_str; + d->last_exception_ = json_str; } else { - std::cerr << "Pre-Deno Exception " << exception_str << std::endl; - exit(1); - } -} - -const char* PromiseRejectStr(enum v8::PromiseRejectEvent e) { - switch (e) { - case v8::PromiseRejectEvent::kPromiseRejectWithNoHandler: - return "RejectWithNoHandler"; - case v8::PromiseRejectEvent::kPromiseHandlerAddedAfterReject: - return "HandlerAddedAfterReject"; - case v8::PromiseRejectEvent::kPromiseResolveAfterResolved: - return "ResolveAfterResolved"; - case v8::PromiseRejectEvent::kPromiseRejectAfterResolved: - return "RejectAfterResolved"; + // This shouldn't happen in normal circumstances. Added for debugging. + std::cerr << "Pre-Deno Exception " << json_str << std::endl; + CHECK(false); } } @@ -169,40 +168,35 @@ void PromiseRejectCallback(v8::PromiseRejectMessage promise_reject_message) { DenoIsolate* d = static_cast<DenoIsolate*>(isolate->GetData(0)); DCHECK_EQ(d->isolate_, isolate); v8::HandleScope handle_scope(d->isolate_); - auto exception = promise_reject_message.GetValue(); + auto error = promise_reject_message.GetValue(); auto context = d->context_.Get(d->isolate_); auto promise = promise_reject_message.GetPromise(); - auto event = promise_reject_message.GetEvent(); v8::Context::Scope context_scope(context); - auto promise_reject_handler = d->promise_reject_handler_.Get(isolate); - - if (!promise_reject_handler.IsEmpty()) { - v8::Local<v8::Value> args[3]; - args[1] = v8_str(PromiseRejectStr(event)); - args[2] = promise; - /* error, event, promise */ - if (event == v8::PromiseRejectEvent::kPromiseRejectWithNoHandler) { - d->pending_promise_events_++; - // exception only valid for kPromiseRejectWithNoHandler - args[0] = exception; - } else if (event == - v8::PromiseRejectEvent::kPromiseHandlerAddedAfterReject) { - d->pending_promise_events_--; // unhandled event cancelled - if (d->pending_promise_events_ < 0) { - d->pending_promise_events_ = 0; - } - // Placeholder, not actually used - args[0] = v8_str("Promise handler added"); - } else if (event == v8::PromiseRejectEvent::kPromiseResolveAfterResolved) { - d->pending_promise_events_++; - args[0] = v8_str("Promise resolved after resolved"); - } else if (event == v8::PromiseRejectEvent::kPromiseRejectAfterResolved) { - d->pending_promise_events_++; - args[0] = v8_str("Promise rejected after resolved"); - } - promise_reject_handler->Call(context->Global(), 3, args); - return; + + int promise_id = promise->GetIdentityHash(); + switch (promise_reject_message.GetEvent()) { + case v8::kPromiseRejectWithNoHandler: + // Insert the error into the pending_promise_map_ using the promise's id + // as the key. + d->pending_promise_map_.emplace(std::piecewise_construct, + std::make_tuple(promise_id), + std::make_tuple(d->isolate_, error)); + break; + + case v8::kPromiseHandlerAddedAfterReject: + d->pending_promise_map_.erase(promise_id); + break; + + case v8::kPromiseRejectAfterResolved: + break; + + case v8::kPromiseResolveAfterResolved: + // Should not warn. See #1272 + break; + + default: + CHECK(false && "unreachable"); } } @@ -359,69 +353,6 @@ void Shared(v8::Local<v8::Name> property, info.GetReturnValue().Set(ab); } -// Sets the global error handler. -void SetGlobalErrorHandler(const v8::FunctionCallbackInfo<v8::Value>& args) { - v8::Isolate* isolate = args.GetIsolate(); - DenoIsolate* d = FromIsolate(isolate); - DCHECK_EQ(d->isolate_, isolate); - - v8::HandleScope handle_scope(isolate); - - if (!d->global_error_handler_.IsEmpty()) { - isolate->ThrowException( - v8_str("libdeno.setGlobalErrorHandler already called.")); - return; - } - - v8::Local<v8::Value> v = args[0]; - CHECK(v->IsFunction()); - v8::Local<v8::Function> func = v8::Local<v8::Function>::Cast(v); - - d->global_error_handler_.Reset(isolate, func); -} - -// Sets the promise uncaught reject handler -void SetPromiseRejectHandler(const v8::FunctionCallbackInfo<v8::Value>& args) { - v8::Isolate* isolate = args.GetIsolate(); - DenoIsolate* d = FromIsolate(isolate); - DCHECK_EQ(d->isolate_, isolate); - - v8::HandleScope handle_scope(isolate); - - if (!d->promise_reject_handler_.IsEmpty()) { - isolate->ThrowException( - v8_str("libdeno.setPromiseRejectHandler already called.")); - return; - } - - v8::Local<v8::Value> v = args[0]; - CHECK(v->IsFunction()); - v8::Local<v8::Function> func = v8::Local<v8::Function>::Cast(v); - - d->promise_reject_handler_.Reset(isolate, func); -} - -// Sets the promise uncaught reject handler -void SetPromiseErrorExaminer(const v8::FunctionCallbackInfo<v8::Value>& args) { - v8::Isolate* isolate = args.GetIsolate(); - DenoIsolate* d = FromIsolate(isolate); - DCHECK_EQ(d->isolate_, isolate); - - v8::HandleScope handle_scope(isolate); - - if (!d->promise_error_examiner_.IsEmpty()) { - isolate->ThrowException( - v8_str("libdeno.setPromiseErrorExaminer already called.")); - return; - } - - v8::Local<v8::Value> v = args[0]; - CHECK(v->IsFunction()); - v8::Local<v8::Function> func = v8::Local<v8::Function>::Cast(v); - - d->promise_error_examiner_.Reset(isolate, func); -} - bool ExecuteV8StringSource(v8::Local<v8::Context> context, const char* js_filename, v8::Local<v8::String> source) { @@ -466,8 +397,7 @@ bool Execute(v8::Local<v8::Context> context, const char* js_filename, } void InitializeContext(v8::Isolate* isolate, v8::Local<v8::Context> context, - const char* js_filename, const char* js_source, - const char* source_map) { + const char* js_filename, const char* js_source) { CHECK_NE(js_source, nullptr); CHECK_NE(js_filename, nullptr); v8::HandleScope handle_scope(isolate); @@ -493,61 +423,8 @@ void InitializeContext(v8::Isolate* isolate, v8::Local<v8::Context> context, CHECK(deno_val->SetAccessor(context, deno::v8_str("shared"), Shared) .FromJust()); - auto set_global_error_handler_tmpl = - v8::FunctionTemplate::New(isolate, SetGlobalErrorHandler); - auto set_global_error_handler_val = - set_global_error_handler_tmpl->GetFunction(context).ToLocalChecked(); - CHECK(deno_val - ->Set(context, deno::v8_str("setGlobalErrorHandler"), - set_global_error_handler_val) - .FromJust()); - - auto set_promise_reject_handler_tmpl = - v8::FunctionTemplate::New(isolate, SetPromiseRejectHandler); - auto set_promise_reject_handler_val = - set_promise_reject_handler_tmpl->GetFunction(context).ToLocalChecked(); - CHECK(deno_val - ->Set(context, deno::v8_str("setPromiseRejectHandler"), - set_promise_reject_handler_val) - .FromJust()); - - auto set_promise_error_examiner_tmpl = - v8::FunctionTemplate::New(isolate, SetPromiseErrorExaminer); - auto set_promise_error_examiner_val = - set_promise_error_examiner_tmpl->GetFunction(context).ToLocalChecked(); - CHECK(deno_val - ->Set(context, deno::v8_str("setPromiseErrorExaminer"), - set_promise_error_examiner_val) - .FromJust()); - { - if (source_map != nullptr) { - v8::TryCatch try_catch(isolate); - v8::ScriptOrigin origin(v8_str("set_source_map.js")); - std::string source_map_parens = - std::string("(") + std::string(source_map) + std::string(")"); - auto source_map_v8_str = deno::v8_str(source_map_parens.c_str()); - auto script = v8::Script::Compile(context, source_map_v8_str, &origin); - if (script.IsEmpty()) { - DCHECK(try_catch.HasCaught()); - HandleException(context, try_catch.Exception()); - return; - } - auto source_map_obj = script.ToLocalChecked()->Run(context); - if (source_map_obj.IsEmpty()) { - DCHECK(try_catch.HasCaught()); - HandleException(context, try_catch.Exception()); - return; - } - CHECK(deno_val - ->Set(context, deno::v8_str("mainSourceMap"), - source_map_obj.ToLocalChecked()) - .FromJust()); - } - auto source = deno::v8_str(js_source); - CHECK( - deno_val->Set(context, deno::v8_str("mainSource"), source).FromJust()); bool r = deno::ExecuteV8StringSource(context, js_filename, source); CHECK(r); @@ -558,10 +435,11 @@ void DenoIsolate::AddIsolate(v8::Isolate* isolate) { isolate_ = isolate; // Leaving this code here because it will probably be useful later on, but // disabling it now as I haven't got tests for the desired behavior. - // d->isolate->SetCaptureStackTraceForUncaughtExceptions(true); // d->isolate->SetAbortOnUncaughtExceptionCallback(AbortOnUncaughtExceptionCallback); // d->isolate->AddMessageListener(MessageCallback2); // d->isolate->SetFatalErrorHandler(FatalErrorCallback2); + isolate_->SetCaptureStackTraceForUncaughtExceptions( + true, 10, v8::StackTrace::kDetailed); isolate_->SetPromiseRejectCallback(deno::PromiseRejectCallback); isolate_->SetData(0, this); } diff --git a/libdeno/deno.h b/libdeno/deno.h index fb0b13746..48356bc86 100644 --- a/libdeno/deno.h +++ b/libdeno/deno.h @@ -37,7 +37,7 @@ typedef struct { Deno* deno_new(deno_buf snapshot, deno_config config); Deno* deno_new_snapshotter(deno_config config, const char* js_filename, - const char* js_source, const char* source_map); + const char* js_source); // Generate a snapshot. The resulting buf can be used with deno_new. // The caller must free the returned data by calling delete[] buf.data_ptr. @@ -48,6 +48,12 @@ void deno_delete(Deno* d); // Returns false on error. // Get error text with deno_last_exception(). // 0 = fail, 1 = success +// +// TODO change return value to be const char*. On success the return +// value is nullptr, on failure it is the JSON exception text that +// is returned by deno_last_exception(). Remove deno_last_exception(). +// The return string is valid until the next execution of deno_execute or +// deno_respond (as deno_last_exception is now). int deno_execute(Deno* d, void* user_data, const char* js_filename, const char* js_source); @@ -69,6 +75,12 @@ int deno_execute(Deno* d, void* user_data, const char* js_filename, // // A non-zero return value, means a JS exception was encountered during the // libdeno.recv() callback. Check deno_last_exception() for exception text. +// +// TODO change return value to be const char*. On success the return +// value is nullptr, on failure it is the JSON exception text that +// is returned by deno_last_exception(). Remove deno_last_exception(). +// The return string is valid until the next execution of deno_execute or +// deno_respond (as deno_last_exception is now). int deno_respond(Deno* d, void* user_data, int32_t req_id, deno_buf buf); void deno_check_promise_errors(Deno* d); diff --git a/libdeno/internal.h b/libdeno/internal.h index 8a366d515..3dc4d8d0e 100644 --- a/libdeno/internal.h +++ b/libdeno/internal.h @@ -19,7 +19,6 @@ class DenoIsolate { current_args_(nullptr), snapshot_creator_(nullptr), global_import_buf_ptr_(nullptr), - pending_promise_events_(0), recv_cb_(config.recv_cb), next_req_id_(0), user_data_(nullptr) { @@ -47,13 +46,13 @@ class DenoIsolate { const v8::FunctionCallbackInfo<v8::Value>* current_args_; v8::SnapshotCreator* snapshot_creator_; void* global_import_buf_ptr_; - int32_t pending_promise_events_; deno_recv_cb recv_cb_; int32_t next_req_id_; void* user_data_; v8::Persistent<v8::Context> context_; std::map<int32_t, v8::Persistent<v8::Value>> async_data_map_; + std::map<int, v8::Persistent<v8::Value>> pending_promise_map_; std::string last_exception_; v8::Persistent<v8::Function> recv_; v8::Persistent<v8::Function> global_error_handler_; @@ -91,26 +90,16 @@ void Recv(const v8::FunctionCallbackInfo<v8::Value>& args); void Send(const v8::FunctionCallbackInfo<v8::Value>& args); void Shared(v8::Local<v8::Name> property, const v8::PropertyCallbackInfo<v8::Value>& info); -void SetGlobalErrorHandler(const v8::FunctionCallbackInfo<v8::Value>& args); -void SetPromiseRejectHandler(const v8::FunctionCallbackInfo<v8::Value>& args); -void SetPromiseErrorExaminer(const v8::FunctionCallbackInfo<v8::Value>& args); static intptr_t external_references[] = { - reinterpret_cast<intptr_t>(Print), - reinterpret_cast<intptr_t>(Recv), - reinterpret_cast<intptr_t>(Send), - reinterpret_cast<intptr_t>(Shared), - reinterpret_cast<intptr_t>(SetGlobalErrorHandler), - reinterpret_cast<intptr_t>(SetPromiseRejectHandler), - reinterpret_cast<intptr_t>(SetPromiseErrorExaminer), - 0}; + reinterpret_cast<intptr_t>(Print), reinterpret_cast<intptr_t>(Recv), + reinterpret_cast<intptr_t>(Send), reinterpret_cast<intptr_t>(Shared), 0}; static const deno_buf empty_buf = {nullptr, 0, nullptr, 0}; Deno* NewFromSnapshot(void* user_data, deno_recv_cb cb); void InitializeContext(v8::Isolate* isolate, v8::Local<v8::Context> context, - const char* js_filename, const char* js_source, - const char* source_map); + const char* js_filename, const char* js_source); void HandleException(v8::Local<v8::Context> context, v8::Local<v8::Value> exception); diff --git a/libdeno/libdeno_test.cc b/libdeno/libdeno_test.cc index f7173210a..33a4702ae 100644 --- a/libdeno/libdeno_test.cc +++ b/libdeno/libdeno_test.cc @@ -15,14 +15,14 @@ TEST(LibDenoTest, InitializesCorrectlyWithoutSnapshot) { } TEST(LibDenoTest, SnapshotterInitializesCorrectly) { - Deno* d = deno_new_snapshotter(deno_config{empty, nullptr}, "a.js", - "a = 1 + 2", nullptr); + Deno* d = + deno_new_snapshotter(deno_config{empty, nullptr}, "a.js", "a = 1 + 2"); deno_delete(d); } TEST(LibDenoTest, Snapshotter) { - Deno* d1 = deno_new_snapshotter(deno_config{empty, nullptr}, "a.js", - "a = 1 + 2", nullptr); + Deno* d1 = + deno_new_snapshotter(deno_config{empty, nullptr}, "a.js", "a = 1 + 2"); deno_buf test_snapshot = deno_get_snapshot(d1); deno_delete(d1); @@ -182,23 +182,18 @@ TEST(LibDenoTest, SnapshotBug) { } TEST(LibDenoTest, GlobalErrorHandling) { - static int count = 0; - auto recv_cb = [](auto _, int req_id, auto buf, auto data_buf) { - assert_null(data_buf); - count++; - EXPECT_EQ(static_cast<size_t>(1), buf.data_len); - EXPECT_EQ(buf.data_ptr[0], 42); - }; - Deno* d = deno_new(snapshot, deno_config{empty, recv_cb}); - EXPECT_FALSE(deno_execute(d, nullptr, "a.js", "GlobalErrorHandling()")); - EXPECT_EQ(count, 1); - deno_delete(d); -} - -TEST(LibDenoTest, DoubleGlobalErrorHandlingFails) { Deno* d = deno_new(snapshot, deno_config{empty, nullptr}); - EXPECT_FALSE( - deno_execute(d, nullptr, "a.js", "DoubleGlobalErrorHandlingFails()")); + EXPECT_FALSE(deno_execute(d, nullptr, "a.js", "GlobalErrorHandling()")); + // We only check that it starts with this string, so we don't have to check + // the second frame, which contains line numbers in libdeno_test.js and may + // change over time. + std::string expected = + "{\"message\":\"ReferenceError: notdefined is not defined\"," + "\"frames\":[{\"line\":3,\"column\":2,\"functionName\":\"\"," + "\"scriptName\":\"helloworld.js\",\"isEval\":true," + "\"isConstructor\":false,\"isWasm\":false},"; + std::string actual(deno_last_exception(d), 0, expected.length()); + EXPECT_STREQ(expected.c_str(), actual.c_str()); deno_delete(d); } @@ -225,17 +220,31 @@ TEST(LibDenoTest, DataBuf) { deno_delete(d); } -TEST(LibDenoTest, PromiseRejectCatchHandling) { +TEST(LibDenoTest, CheckPromiseErrors) { static int count = 0; - auto recv_cb = [](auto _, int req_id, auto buf, auto data_buf) { - // If no error, nothing should be sent, and count should - // not increment - count++; - }; + auto recv_cb = [](auto _, int req_id, auto buf, auto data_buf) { count++; }; Deno* d = deno_new(snapshot, deno_config{empty, recv_cb}); - EXPECT_TRUE(deno_execute(d, nullptr, "a.js", "PromiseRejectCatchHandling()")); + EXPECT_EQ(deno_last_exception(d), nullptr); + EXPECT_TRUE(deno_execute(d, nullptr, "a.js", "CheckPromiseErrors()")); + EXPECT_EQ(deno_last_exception(d), nullptr); + EXPECT_EQ(count, 1); + // We caught the exception. So still no errors after calling + // deno_check_promise_errors(). + deno_check_promise_errors(d); + EXPECT_EQ(deno_last_exception(d), nullptr); + deno_delete(d); +} - EXPECT_EQ(count, 0); +TEST(LibDenoTest, LastException) { + Deno* d = deno_new(empty, deno_config{empty, nullptr}); + EXPECT_EQ(deno_last_exception(d), nullptr); + EXPECT_FALSE(deno_execute(d, nullptr, "a.js", "\n\nthrow Error('boo');\n\n")); + EXPECT_STREQ(deno_last_exception(d), + "{\"message\":\"Error: boo\"," + "\"frames\":[{\"line\":3,\"column\":7," + "\"functionName\":\"\",\"scriptName\":\"a.js\"," + "\"isEval\":false," + "\"isConstructor\":false,\"isWasm\":false}]}"); deno_delete(d); } diff --git a/libdeno/libdeno_test.js b/libdeno/libdeno_test.js index c9eaaa0ca..bb36b02d9 100644 --- a/libdeno/libdeno_test.js +++ b/libdeno/libdeno_test.js @@ -99,23 +99,9 @@ global.SnapshotBug = () => { }; global.GlobalErrorHandling = () => { - libdeno.setGlobalErrorHandler((message, source, line, col, error) => { - libdeno.print(`line ${line} col ${col}`, true); - assert("ReferenceError: notdefined is not defined" === message); - assert(source === "helloworld.js"); - assert(line === 3); - assert(col === 1); - assert(error instanceof Error); - libdeno.send(new Uint8Array([42])); - }); eval("\n\n notdefined()\n//# sourceURL=helloworld.js"); }; -global.DoubleGlobalErrorHandlingFails = () => { - libdeno.setGlobalErrorHandler((message, source, line, col, error) => {}); - libdeno.setGlobalErrorHandler((message, source, line, col, error) => {}); -}; - // Allocate this buf at the top level to avoid GC. const dataBuf = new Uint8Array([3, 4]); @@ -134,33 +120,7 @@ global.DataBuf = () => { b[1] = 8; }; -global.PromiseRejectCatchHandling = () => { - let count = 0; - let promiseRef = null; - // When we have an error, libdeno sends something - function assertOrSend(cond) { - if (!cond) { - libdeno.send(new Uint8Array([42])); - } - } - libdeno.setPromiseErrorExaminer(() => { - assertOrSend(count === 2); - }); - libdeno.setPromiseRejectHandler((error, event, promise) => { - count++; - if (event === "RejectWithNoHandler") { - assertOrSend(error instanceof Error); - assertOrSend(error.message === "message"); - assertOrSend(count === 1); - promiseRef = promise; - } else if (event === "HandlerAddedAfterReject") { - assertOrSend(count === 2); - assertOrSend(promiseRef === promise); - } - // Should never reach 3! - assertOrSend(count !== 3); - }); - +global.CheckPromiseErrors = () => { async function fn() { throw new Error("message"); } @@ -169,10 +129,10 @@ global.PromiseRejectCatchHandling = () => { try { await fn(); } catch (e) { - assertOrSend(count === 2); + libdeno.send(new Uint8Array([42])); } })(); -} +}; global.Shared = () => { const ab = libdeno.shared; @@ -185,4 +145,4 @@ global.Shared = () => { ui8[0] = 42; ui8[1] = 43; ui8[2] = 44; -} +}; diff --git a/libdeno/snapshot_creator.cc b/libdeno/snapshot_creator.cc index cfdd35804..fe9011c9d 100644 --- a/libdeno/snapshot_creator.cc +++ b/libdeno/snapshot_creator.cc @@ -30,9 +30,7 @@ int main(int argc, char** argv) { deno_init(); deno_config config = {deno::empty_buf, nullptr}; - Deno* d = deno_new_snapshotter( - config, js_fn, js_source.c_str(), - source_map_fn != nullptr ? source_map.c_str() : nullptr); + Deno* d = deno_new_snapshotter(config, js_fn, js_source.c_str()); auto snapshot = deno_get_snapshot(d); diff --git a/package.json b/package.json index e591e09b2..08e8ea436 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "@types/base64-js": "^1.2.5", "@types/flatbuffers": "^1.9.0", "@types/prettier": "=1.15.3", - "@types/source-map-support": "^0.4.1", "@types/text-encoding": "0.0.33", "base64-js": "^1.3.0", "flatbuffers": "^1.9.0", @@ -19,7 +18,6 @@ "rollup-plugin-string": "^2.0.2", "rollup-plugin-typescript2": "^0.16.1", "rollup-pluginutils": "^2.3.0", - "source-map-support": "^0.5.6", "ts-node": "^7.0.1", "ts-simple-ast": "17.1.0", "tslint": "^5.10.0", diff --git a/src/deno_dir.rs b/src/deno_dir.rs index 8f13fd843..a6a741ec1 100644 --- a/src/deno_dir.rs +++ b/src/deno_dir.rs @@ -5,7 +5,9 @@ use errors::DenoResult; use errors::ErrorKind; use fs as deno_fs; use http_util; +use js_errors::SourceMapGetter; use msg; + use ring; use std; use std::fmt::Write; @@ -346,6 +348,24 @@ impl DenoDir { } } +impl SourceMapGetter for DenoDir { + fn get_source_map(&self, script_name: &str) -> Option<String> { + match self.code_fetch(script_name, ".") { + Err(_e) => { + return None; + } + Ok(out) => match out.maybe_source_map { + None => { + return None; + } + Some(source_map) => { + return Some(source_map); + } + }, + } + } +} + fn get_cache_filename(basedir: &Path, url: &Url) -> PathBuf { let host = url.host_str().unwrap(); let host_port = match url.port() { diff --git a/src/isolate.rs b/src/isolate.rs index 6b2814ec4..8e78f770b 100644 --- a/src/isolate.rs +++ b/src/isolate.rs @@ -8,6 +8,7 @@ use deno_dir; use errors::DenoError; use errors::DenoResult; use flags; +use js_errors::JSError; use libdeno; use permissions::DenoPermissions; @@ -26,8 +27,6 @@ use std::time::Instant; use tokio; use tokio_util; -type DenoException<'a> = &'a str; - // 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. @@ -184,11 +183,25 @@ impl Isolate { self.timeout_due.set(inst); } + pub fn last_exception(&self) -> Option<JSError> { + let ptr = unsafe { libdeno::deno_last_exception(self.libdeno_isolate) }; + if ptr == std::ptr::null() { + None + } else { + let cstr = unsafe { CStr::from_ptr(ptr) }; + let v8_exception = cstr.to_str().unwrap(); + debug!("v8_exception\n{}\n", v8_exception); + let js_error = JSError::from_v8_exception(v8_exception).unwrap(); + let js_error_mapped = js_error.apply_source_map(&self.state.dir); + return Some(js_error_mapped); + } + } + pub fn execute( &self, js_filename: &str, js_source: &str, - ) -> Result<(), DenoException> { + ) -> Result<(), JSError> { let filename = CString::new(js_filename).unwrap(); let source = CString::new(js_source).unwrap(); let r = unsafe { @@ -200,9 +213,8 @@ impl Isolate { ) }; if r == 0 { - let ptr = unsafe { libdeno::deno_last_exception(self.libdeno_isolate) }; - let cstr = unsafe { CStr::from_ptr(ptr) }; - return Err(cstr.to_str().unwrap()); + let js_error = self.last_exception().unwrap(); + return Err(js_error); } Ok(()) } @@ -249,7 +261,7 @@ impl Isolate { // TODO Use Park abstraction? Note at time of writing Tokio default runtime // does not have new_with_park(). - pub fn event_loop(&self) { + pub fn event_loop(&self) -> Result<(), JSError> { // Main thread event loop. while !self.is_idle() { match recv_deadline(&self.rx, self.get_timeout_due()) { @@ -258,9 +270,16 @@ impl Isolate { Err(e) => panic!("recv_deadline() failed: {:?}", e), } self.check_promise_errors(); + if let Some(err) = self.last_exception() { + return Err(err); + } } // Check on done self.check_promise_errors(); + if let Some(err) = self.last_exception() { + return Err(err); + } + Ok(()) } #[inline] @@ -389,7 +408,7 @@ mod tests { } "#, ).expect("execute error"); - isolate.event_loop(); + isolate.event_loop().ok(); }); } @@ -435,8 +454,8 @@ mod tests { const data = new Uint8Array([42, 43, 44, 45, 46]); libdeno.send(control, data); "#, - ).expect("execute error"); - isolate.event_loop(); + ).expect("execute error");; + isolate.event_loop().unwrap(); let metrics = &isolate.state.metrics; assert_eq!(metrics.ops_dispatched.load(Ordering::SeqCst), 1); assert_eq!(metrics.ops_completed.load(Ordering::SeqCst), 1); @@ -471,6 +490,7 @@ mod tests { const control = new Uint8Array([4, 5, 6]); const data = new Uint8Array([42, 43, 44, 45, 46]); let r = libdeno.send(control, data); + libdeno.recv(() => {}); if (r != null) throw Error("expected null"); "#, ).expect("execute error"); @@ -486,7 +506,7 @@ mod tests { // with metrics_dispatch_async() to properly validate them. } - isolate.event_loop(); + isolate.event_loop().unwrap(); // Make sure relevant metrics are updated after task is executed. { diff --git a/src/js_errors.rs b/src/js_errors.rs new file mode 100644 index 000000000..63862db3f --- /dev/null +++ b/src/js_errors.rs @@ -0,0 +1,543 @@ +// Copyright 2018 the Deno authors. All rights reserved. MIT license. + +// Note that source_map_mappings requires 0-indexed line and column numbers but +// V8 Exceptions are 1-indexed. + +// TODO: This currently only applies to uncaught exceptions. It would be nice to +// also have source maps for situations like this: +// const err = new Error("Boo!"); +// console.log(err.stack); +// It would require calling into Rust from Error.prototype.prepareStackTrace. + +use serde_json; +use source_map_mappings::parse_mappings; +use source_map_mappings::Bias; +use source_map_mappings::Mappings; +use std::collections::HashMap; + +pub trait SourceMapGetter { + /// Returns the raw source map file. + fn get_source_map(&self, script_name: &str) -> Option<String>; +} + +struct SourceMap { + mappings: Mappings, + sources: Vec<String>, +} + +/// Cached filename lookups. The key can be None if a previous lookup failed to +/// find a SourceMap. +type CachedMaps = HashMap<String, Option<SourceMap>>; + +#[derive(Debug, PartialEq)] +pub struct StackFrame { + pub line: u32, // zero indexed + pub column: u32, // zero indexed + pub source_url: String, // TODO rename to 'script_name' + pub function_name: String, + pub is_eval: bool, + pub is_constructor: bool, + pub is_wasm: bool, +} + +#[derive(Debug, PartialEq)] +pub struct JSError { + pub message: String, + pub frames: Vec<StackFrame>, +} + +impl ToString for StackFrame { + fn to_string(&self) -> String { + // Note when we print to string, we change from 0-indexed to 1-indexed. + let (line, column) = (self.line + 1, self.column + 1); + if self.function_name.len() > 0 { + format!( + " at {} ({}:{}:{})", + self.function_name, self.source_url, line, column + ) + } else if self.is_eval { + format!(" at eval ({}:{}:{})", self.source_url, line, column) + } else { + format!(" at {}:{}:{}", self.source_url, line, column) + } + } +} + +impl ToString for JSError { + fn to_string(&self) -> String { + let mut s = self.message.clone(); + for frame in &self.frames { + s.push_str("\n"); + s.push_str(&frame.to_string()); + } + s + } +} + +impl StackFrame { + // TODO Maybe use serde_derive? + fn from_json_value(v: &serde_json::Value) -> Option<Self> { + if !v.is_object() { + return None; + } + let obj = v.as_object().unwrap(); + + let line_v = &obj["line"]; + if !line_v.is_u64() { + return None; + } + let line = line_v.as_u64().unwrap() as u32; + + let column_v = &obj["column"]; + if !column_v.is_u64() { + return None; + } + let column = column_v.as_u64().unwrap() as u32; + + let script_name_v = &obj["scriptName"]; + if !script_name_v.is_string() { + return None; + } + let script_name = String::from(script_name_v.as_str().unwrap()); + + // Optional fields. See EncodeExceptionAsJSON() in libdeno. + // Sometimes V8 doesn't provide all the frame information. + + let mut function_name = String::from(""); // default + if obj.contains_key("functionName") { + let function_name_v = &obj["functionName"]; + if function_name_v.is_string() { + function_name = String::from(function_name_v.as_str().unwrap()); + } + } + + let mut is_eval = false; // default + if obj.contains_key("isEval") { + let is_eval_v = &obj["isEval"]; + if is_eval_v.is_boolean() { + is_eval = is_eval_v.as_bool().unwrap(); + } + } + + let mut is_constructor = false; // default + if obj.contains_key("isConstructor") { + let is_constructor_v = &obj["isConstructor"]; + if is_constructor_v.is_boolean() { + is_constructor = is_constructor_v.as_bool().unwrap(); + } + } + + let mut is_wasm = false; // default + if obj.contains_key("isWasm") { + let is_wasm_v = &obj["isWasm"]; + if is_wasm_v.is_boolean() { + is_wasm = is_wasm_v.as_bool().unwrap(); + } + } + + Some(StackFrame { + line: line - 1, + column: column - 1, + source_url: script_name, + function_name, + is_eval, + is_constructor, + is_wasm, + }) + } + + fn apply_source_map( + &self, + mappings_map: &mut CachedMaps, + getter: &SourceMapGetter, + ) -> StackFrame { + let maybe_sm = get_mappings(self.source_url.as_ref(), mappings_map, getter); + let frame_pos = (self.source_url.to_owned(), self.line, self.column); + let (source_url, line, column) = match maybe_sm { + None => frame_pos, + Some(sm) => match sm.mappings.original_location_for( + self.line, + self.column, + 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, + original.original_line, + original.original_column, + ) + } + }, + }, + }; + + StackFrame { + source_url, + function_name: self.function_name.clone(), + line, + column, + is_eval: self.is_eval, + is_constructor: self.is_constructor, + is_wasm: self.is_wasm, + } + } +} + +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 => return None, + Some(mappings_str) => { + match parse_mappings::<()>(mappings_str.as_bytes()) { + Err(_) => return 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()); + } + } + } + + return Some(SourceMap { sources, mappings }); + } + } + } + }, + _ => return None, + } + } +} + +impl JSError { + /// Creates a new JSError by parsing the raw exception JSON string from V8. + pub fn from_v8_exception(json_str: &str) -> Option<Self> { + let v = serde_json::from_str::<serde_json::Value>(json_str); + if v.is_err() { + return None; + } + let v = v.unwrap(); + + if !v.is_object() { + return None; + } + let obj = v.as_object().unwrap(); + + let message_v = &obj["message"]; + if !message_v.is_string() { + return None; + } + let message = String::from(message_v.as_str().unwrap()); + + let frames_v = &obj["frames"]; + if !frames_v.is_array() { + return None; + } + let frame_values = frames_v.as_array().unwrap(); + + let mut frames = Vec::<StackFrame>::new(); + for frame_v in frame_values { + match StackFrame::from_json_value(frame_v) { + None => return None, + Some(frame) => frames.push(frame), + } + } + + Some(JSError { message, frames }) + } + + pub fn apply_source_map(&self, getter: &SourceMapGetter) -> Self { + let message = self.message.clone(); + let mut mappings_map: CachedMaps = HashMap::new(); + let mut frames = Vec::<StackFrame>::new(); + for frame in &self.frames { + let f = frame.apply_source_map(&mut mappings_map, getter); + frames.push(f); + } + JSError { message, frames } + } +} + +fn parse_map_string( + source_url: &str, + getter: &SourceMapGetter, +) -> Option<SourceMap> { + match source_url { + "gen/bundle/main.js" => { + let s = + include_str!(concat!(env!("GN_OUT_DIR"), "/gen/bundle/main.js.map")); + SourceMap::from_json(s) + } + _ => match getter.get_source_map(source_url) { + None => None, + Some(raw_source_map) => SourceMap::from_json(&raw_source_map), + }, + } +} + +fn get_mappings<'a>( + source_url: &str, + mappings_map: &'a mut CachedMaps, + getter: &SourceMapGetter, +) -> &'a Option<SourceMap> { + mappings_map + .entry(source_url.to_string()) + .or_insert_with(|| parse_map_string(source_url, getter)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn error1() -> JSError { + JSError { + message: "Error: foo bar".to_string(), + frames: vec![ + StackFrame { + line: 4, + column: 16, + source_url: "foo_bar.ts".to_string(), + function_name: "foo".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + StackFrame { + line: 5, + column: 20, + source_url: "bar_baz.ts".to_string(), + function_name: "qat".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + StackFrame { + line: 1, + column: 1, + source_url: "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<String> { + 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.to_string()) + } + } + + #[test] + fn stack_frame_from_json_value_1() { + let v = serde_json::from_str::<serde_json::Value>( + r#"{ + "line":2, + "column":11, + "functionName":"foo", + "scriptName":"/Users/rld/src/deno/tests/error_001.ts", + "isEval":true, + "isConstructor":false, + "isWasm":false + }"#, + ).unwrap(); + let r = StackFrame::from_json_value(&v); + assert_eq!( + r, + Some(StackFrame { + line: 1, + column: 10, + source_url: "/Users/rld/src/deno/tests/error_001.ts".to_string(), + function_name: "foo".to_string(), + is_eval: true, + is_constructor: false, + is_wasm: false, + }) + ); + } + + #[test] + fn stack_frame_from_json_value_2() { + let v = serde_json::from_str::<serde_json::Value>( + r#"{ + "scriptName": "/Users/rld/src/deno/tests/error_001.ts", + "line": 2, + "column": 11 + }"#, + ).unwrap(); + let r = StackFrame::from_json_value(&v); + assert!(r.is_some()); + let f = r.unwrap(); + assert_eq!(f.line, 1); + assert_eq!(f.column, 10); + assert_eq!(f.source_url, "/Users/rld/src/deno/tests/error_001.ts"); + } + + #[test] + fn js_error_from_v8_exception() { + let r = JSError::from_v8_exception( + r#"{ + "message":"Uncaught Error: bad", + "frames":[ + { + "line":2, + "column":11, + "functionName":"foo", + "scriptName":"/Users/rld/src/deno/tests/error_001.ts", + "isEval":true, + "isConstructor":false, + "isWasm":false + }, { + "line":5, + "column":5, + "functionName":"bar", + "scriptName":"/Users/rld/src/deno/tests/error_001.ts", + "isEval":true, + "isConstructor":false, + "isWasm":false + } + ]}"#, + ); + assert!(r.is_some()); + let e = r.unwrap(); + assert_eq!(e.message, "Uncaught Error: bad"); + assert_eq!(e.frames.len(), 2); + assert_eq!( + e.frames[0], + StackFrame { + line: 1, + column: 10, + source_url: "/Users/rld/src/deno/tests/error_001.ts".to_string(), + function_name: "foo".to_string(), + is_eval: true, + is_constructor: false, + is_wasm: false, + } + ) + } + + #[test] + fn stack_frame_to_string() { + let e = error1(); + assert_eq!(" at foo (foo_bar.ts:5:17)", e.frames[0].to_string()); + assert_eq!(" at qat (bar_baz.ts:6:21)", e.frames[1].to_string()); + } + + #[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", e.to_string()); + } + + #[test] + fn js_error_apply_source_map_1() { + let e = error1(); + let getter = MockSourceMapGetter {}; + let actual = e.apply_source_map(&getter); + let expected = JSError { + message: "Error: foo bar".to_string(), + frames: vec![ + StackFrame { + line: 5, + column: 12, + source_url: "foo_bar.ts".to_string(), + function_name: "foo".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + StackFrame { + line: 4, + column: 14, + source_url: "bar_baz.ts".to_string(), + function_name: "qat".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + StackFrame { + line: 1, + column: 1, + source_url: "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() { + // Because this is accessing the live bundle, this test might be more fragile + let e = JSError { + message: "TypeError: baz".to_string(), + frames: vec![StackFrame { + line: 11, + column: 12, + source_url: "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 = e.apply_source_map(&getter); + assert_eq!(actual.message, "TypeError: baz"); + assert_eq!(actual.frames.len(), 1); + assert_eq!(actual.frames[0].line, 15); + assert_eq!(actual.frames[0].column, 16); + assert_eq!(actual.frames[0].source_url, "deno/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/src/main.rs b/src/main.rs index f2e241ab1..cdb7d8dd0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,8 @@ extern crate rand; extern crate remove_dir_all; extern crate ring; extern crate rustyline; +extern crate serde_json; +extern crate source_map_mappings; extern crate tempfile; extern crate tokio; extern crate tokio_executor; @@ -33,6 +35,7 @@ mod fs; mod http_body; mod http_util; pub mod isolate; +pub mod js_errors; pub mod libdeno; pub mod msg; pub mod msg_util; @@ -68,6 +71,13 @@ impl log::Log for Logger { fn flush(&self) {} } +fn print_err_and_exit(err: js_errors::JSError) { + // TODO Currently tests depend on exception going to stdout. It should go + // to stderr. https://github.com/denoland/deno/issues/964 + println!("{}", err.to_string()); + std::process::exit(1); +} + fn main() { // Rust does not die on panic by default. And -Cpanic=abort is broken. // https://github.com/rust-lang/cargo/issues/2738 @@ -102,10 +112,7 @@ fn main() { tokio_util::init(|| { isolate .execute("deno_main.js", "denoMain();") - .unwrap_or_else(|err| { - error!("{}", err); - std::process::exit(1); - }); - isolate.event_loop(); + .unwrap_or_else(print_err_and_exit); + isolate.event_loop().unwrap_or_else(print_err_and_exit); }); } diff --git a/tests/async_error.ts.out b/tests/async_error.ts.out index 1205f10b0..602d76424 100644 --- a/tests/async_error.ts.out +++ b/tests/async_error.ts.out @@ -4,8 +4,7 @@ world Error: error at foo ([WILDCARD]tests/async_error.ts:4:9) at eval ([WILDCARD]tests/async_error.ts:7:1) - at Runner.eval [as _globalEval] (<anonymous>) - at Runner._gatherDependencies ([WILDCARD]/js/runner.ts:[WILDCARD]) - at Runner.run ([WILDCARD]/js/runner.ts:[WILDCARD]) + at _gatherDependencies ([WILDCARD]/js/runner.ts:[WILDCARD]) + at run ([WILDCARD]/js/runner.ts:[WILDCARD]) at denoMain ([WILDCARD]/js/main.ts:[WILDCARD]) at deno_main.js:1:1 diff --git a/tests/error_001.ts.out b/tests/error_001.ts.out index 5d0c2244c..7ca70a6af 100644 --- a/tests/error_001.ts.out +++ b/tests/error_001.ts.out @@ -2,8 +2,7 @@ Error: bad at foo (file://[WILDCARD]tests/error_001.ts:2:9) at bar (file://[WILDCARD]tests/error_001.ts:6:3) at eval (file://[WILDCARD]tests/error_001.ts:9:1) - at Runner.eval [as _globalEval] (<anonymous>) - at Runner._gatherDependencies ([WILDCARD]/js/runner.ts:[WILDCARD]) - at Runner.run ([WILDCARD]/js/runner.ts:[WILDCARD]) + at _gatherDependencies ([WILDCARD]/js/runner.ts:[WILDCARD]) + at run ([WILDCARD]/js/runner.ts:[WILDCARD]) at denoMain ([WILDCARD]/js/main.ts:[WILDCARD]) at deno_main.js:1:1 diff --git a/tests/error_002.ts.out b/tests/error_002.ts.out index 082acfb33..88f5138f3 100644 --- a/tests/error_002.ts.out +++ b/tests/error_002.ts.out @@ -1,8 +1,8 @@ Error: exception from mod1 - at Object.throwsError (file://[WILDCARD]/tests/subdir/mod1.ts:16:9) + at throwsError (file://[WILDCARD]/tests/subdir/mod1.ts:16:9) at foo (file://[WILDCARD]/tests/error_002.ts:4:3) - at Module.eval [as factory ] (file://[WILDCARD]/tests/error_002.ts:7:1) - at Runner._drainRunQueue ([WILDCARD]/js/runner.ts:[WILDCARD]) - at Runner.run ([WILDCARD]/js/runner.ts:[WILDCARD]) + at eval (file://[WILDCARD]/tests/error_002.ts:7:1) + at _drainRunQueue ([WILDCARD]/js/runner.ts:[WILDCARD]) + at run ([WILDCARD]/js/runner.ts:[WILDCARD]) at denoMain ([WILDCARD]/js/main.ts:[WILDCARD]) at deno_main.js:1:1 diff --git a/tests/error_004_missing_module.ts.out b/tests/error_004_missing_module.ts.out index 061c64701..e78692489 100644 --- a/tests/error_004_missing_module.ts.out +++ b/tests/error_004_missing_module.ts.out @@ -1,11 +1,11 @@ NotFound: Cannot resolve module "bad-module.ts" from "[WILDCARD]/tests/error_004_missing_module.ts" + at DenoError ([WILDCARD]/js/errors.ts:[WILDCARD]) at maybeError ([WILDCARD]/js/errors.ts:[WILDCARD]) at maybeThrowError ([WILDCARD]/js/errors.ts:[WILDCARD]) at sendSync ([WILDCARD]/js/dispatch.ts:[WILDCARD]) - at Object.codeFetch ([WILDCARD]/js/os.ts:[WILDCARD]) - at Compiler._resolveModule ([WILDCARD]/js/compiler.ts:[WILDCARD]) + at codeFetch ([WILDCARD]/js/os.ts:[WILDCARD]) + at _resolveModule ([WILDCARD]/js/compiler.ts:[WILDCARD]) at moduleNames.map.name ([WILDCARD]/js/compiler.ts:[WILDCARD]) - at Array.map (<anonymous>) - at Compiler.resolveModuleNames ([WILDCARD]/js/compiler.ts:[WILDCARD]) - at Object.compilerHost.resolveModuleNames (<anonymous>) - at resolveModuleNamesWorker (<anonymous>) + at resolveModuleNames ([WILDCARD]/js/compiler.ts:[WILDCARD]) + at compilerHost.resolveModuleNames ([WILDCARD]typescript.js:[WILDCARD]) + at resolveModuleNamesWorker ([WILDCARD]typescript.js:[WILDCARD]) diff --git a/tests/error_005_missing_dynamic_import.ts.out b/tests/error_005_missing_dynamic_import.ts.out index 5eae8b73a..7cf79a9b4 100644 --- a/tests/error_005_missing_dynamic_import.ts.out +++ b/tests/error_005_missing_dynamic_import.ts.out @@ -1,11 +1,11 @@ NotFound: Cannot resolve module "bad-module.ts" from "[WILDCARD]/tests/error_005_missing_dynamic_import.ts" + at DenoError ([WILDCARD]/js/errors.ts:[WILDCARD]) at maybeError ([WILDCARD]/js/errors.ts:[WILDCARD]) at maybeThrowError ([WILDCARD]/js/errors.ts:[WILDCARD]) at sendSync ([WILDCARD]/js/dispatch.ts:[WILDCARD]) - at Object.codeFetch ([WILDCARD]/js/os.ts:[WILDCARD]) - at Compiler._resolveModule ([WILDCARD]/js/compiler.ts:[WILDCARD]) + at codeFetch ([WILDCARD]/js/os.ts:[WILDCARD]) + at _resolveModule ([WILDCARD]/js/compiler.ts:[WILDCARD]) at moduleNames.map.name ([WILDCARD]/js/compiler.ts:[WILDCARD]) - at Array.map (<anonymous>) - at Compiler.resolveModuleNames ([WILDCARD]/js/compiler.ts:[WILDCARD]) - at Object.compilerHost.resolveModuleNames (<anonymous>) - at resolveModuleNamesWorker (<anonymous>) + at resolveModuleNames ([WILDCARD]/js/compiler.ts:[WILDCARD]) + at compilerHost.resolveModuleNames ([WILDCARD]) + at resolveModuleNamesWorker ([WILDCARD]) diff --git a/tests/error_006_import_ext_failure.ts.out b/tests/error_006_import_ext_failure.ts.out index 72a1c7686..aa56cc316 100644 --- a/tests/error_006_import_ext_failure.ts.out +++ b/tests/error_006_import_ext_failure.ts.out @@ -1,11 +1,11 @@ NotFound: Cannot resolve module "./non-existent" from "[WILDCARD]/tests/error_006_import_ext_failure.ts" + at DenoError ([WILDCARD]/js/errors.ts:[WILDCARD]) at maybeError ([WILDCARD]/js/errors.ts:[WILDCARD]) at maybeThrowError ([WILDCARD]/js/errors.ts:[WILDCARD]) at sendSync ([WILDCARD]/js/dispatch.ts:[WILDCARD]) - at Object.codeFetch ([WILDCARD]/js/os.ts:[WILDCARD]) - at Compiler._resolveModule ([WILDCARD]/js/compiler.ts:[WILDCARD]) + at codeFetch ([WILDCARD]/js/os.ts:[WILDCARD]) + at _resolveModule ([WILDCARD]/js/compiler.ts:[WILDCARD]) at moduleNames.map.name ([WILDCARD]/js/compiler.ts:[WILDCARD]) - at Array.map (<anonymous>) - at Compiler.resolveModuleNames ([WILDCARD]/js/compiler.ts:[WILDCARD]) - at Object.compilerHost.resolveModuleNames (<anonymous>) - at resolveModuleNamesWorker (<anonymous>) + at resolveModuleNames ([WILDCARD]/js/compiler.ts:[WILDCARD]) + at compilerHost.resolveModuleNames ([WILDCARD]) + at resolveModuleNamesWorker ([WILDCARD]) diff --git a/tests/error_007_any.ts.out b/tests/error_007_any.ts.out index 13335a92c..5bf110299 100644 --- a/tests/error_007_any.ts.out +++ b/tests/error_007_any.ts.out @@ -1 +1 @@ -Thrown: [object Object] +[object Object] diff --git a/third_party b/third_party -Subproject e058979631fd3ecc55f8995a02eaa6ff8f35c32 +Subproject 7d8c9aa769778140e1619f545e706bf34545509 |