Fix promise reject issue (#936)

This commit is contained in:
Kevin (Kun) "Kassimo" Qian 2018-10-12 11:22:52 -07:00 committed by Ryan Dahl
parent c9f95d51da
commit 45d3b8955d
14 changed files with 299 additions and 6 deletions

View file

@ -91,6 +91,7 @@ ts_sources = [
"js/os.ts",
"js/platform.ts",
"js/plugins.d.ts",
"js/promise_util.ts",
"js/read_dir.ts",
"js/read_file.ts",
"js/read_link.ts",

View file

@ -3,6 +3,12 @@ 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 Libdeno {
recv(cb: MessageCallback): void;
@ -20,6 +26,17 @@ interface Libdeno {
) => void
) => void;
setPromiseRejectHandler: (
handler: (
error: Error | string,
event: PromiseRejectEvent,
/* tslint:disable-next-line:no-any */
promise: Promise<any>
) => void
) => void;
setPromiseErrorExaminer: (handler: () => boolean) => void;
mainSource: string;
mainSourceMap: RawSourceMap;
}

View file

@ -7,6 +7,7 @@ import { DenoCompiler } from "./compiler";
import { libdeno } from "./libdeno";
import { args } from "./deno";
import { sendSync, handleAsyncMsgFromRust } from "./dispatch";
import { promiseErrorExaminer, promiseRejectHandler } from "./promise_util";
function sendStart(): msg.StartRes {
const builder = new flatbuffers.Builder();
@ -39,6 +40,8 @@ function onGlobalError(
export default function denoMain() {
libdeno.recv(handleAsyncMsgFromRust);
libdeno.setGlobalErrorHandler(onGlobalError);
libdeno.setPromiseRejectHandler(promiseRejectHandler);
libdeno.setPromiseErrorExaminer(promiseErrorExaminer);
const compiler = DenoCompiler.instance();
// First we send an empty "Start" message to let the privileged side know we

46
js/promise_util.ts Normal file
View file

@ -0,0 +1,46 @@
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;
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;
}

View file

@ -138,15 +138,59 @@ void HandleException(v8::Local<v8::Context> context,
}
}
void ExitOnPromiseRejectCallback(
v8::PromiseRejectMessage promise_reject_message) {
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";
}
}
void PromiseRejectCallback(v8::PromiseRejectMessage promise_reject_message) {
auto* isolate = v8::Isolate::GetCurrent();
Deno* d = static_cast<Deno*>(isolate->GetData(0));
DCHECK_EQ(d->isolate, isolate);
v8::HandleScope handle_scope(d->isolate);
auto exception = promise_reject_message.GetValue();
auto context = d->context.Get(d->isolate);
HandleException(context, exception);
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;
}
}
void Print(const v8::FunctionCallbackInfo<v8::Value>& args) {
@ -279,6 +323,48 @@ void SetGlobalErrorHandler(const v8::FunctionCallbackInfo<v8::Value>& args) {
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();
Deno* d = reinterpret_cast<Deno*>(isolate->GetData(0));
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();
Deno* d = reinterpret_cast<Deno*>(isolate->GetData(0));
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) {
@ -354,6 +440,24 @@ void InitializeContext(v8::Isolate* isolate, v8::Local<v8::Context> context,
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());
{
auto source = deno::v8_str(js_source.c_str());
CHECK(
@ -389,6 +493,7 @@ void InitializeContext(v8::Isolate* isolate, v8::Local<v8::Context> context,
}
void AddIsolate(Deno* d, v8::Isolate* isolate) {
d->pending_promise_events = 0;
d->next_req_id = 0;
d->isolate = isolate;
// Leaving this code here because it will probably be useful later on, but
@ -397,7 +502,7 @@ void AddIsolate(Deno* d, v8::Isolate* isolate) {
// d->isolate->SetAbortOnUncaughtExceptionCallback(AbortOnUncaughtExceptionCallback);
// d->isolate->AddMessageListener(MessageCallback2);
// d->isolate->SetFatalErrorHandler(FatalErrorCallback2);
d->isolate->SetPromiseRejectCallback(deno::ExitOnPromiseRejectCallback);
d->isolate->SetPromiseRejectCallback(deno::PromiseRejectCallback);
d->isolate->SetData(0, d);
}
@ -490,6 +595,36 @@ int deno_respond(Deno* d, void* user_data, int32_t req_id, deno_buf buf) {
return 0;
}
void deno_check_promise_errors(Deno* d) {
if (d->pending_promise_events > 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);
}
}
}
void deno_delete(Deno* d) {
d->isolate->Dispose();
delete d;

View file

@ -59,6 +59,8 @@ int deno_execute(Deno* d, void* user_data, const char* js_filename,
// libdeno.recv() callback. Check deno_last_exception() for exception text.
int deno_respond(Deno* d, void* user_data, int32_t req_id, deno_buf buf);
void deno_check_promise_errors(Deno* d);
const char* deno_last_exception(Deno* d);
void deno_terminate_execution(Deno* d);

View file

@ -14,6 +14,9 @@ struct deno_s {
std::string last_exception;
v8::Persistent<v8::Function> recv;
v8::Persistent<v8::Function> global_error_handler;
v8::Persistent<v8::Function> promise_reject_handler;
v8::Persistent<v8::Function> promise_error_examiner;
int32_t pending_promise_events;
v8::Persistent<v8::Context> context;
v8::Persistent<v8::Map> async_data_map;
deno_recv_cb cb;
@ -32,10 +35,16 @@ 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 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>(Print),
reinterpret_cast<intptr_t>(Recv),
reinterpret_cast<intptr_t>(Send),
reinterpret_cast<intptr_t>(SetGlobalErrorHandler), 0};
reinterpret_cast<intptr_t>(SetGlobalErrorHandler),
reinterpret_cast<intptr_t>(SetPromiseRejectHandler),
reinterpret_cast<intptr_t>(SetPromiseErrorExaminer),
0};
Deno* NewFromSnapshot(void* user_data, deno_recv_cb cb);

View file

@ -193,3 +193,15 @@ TEST(LibDenoTest, DataBuf) {
EXPECT_EQ(data_buf_copy.data_ptr[1], 8);
deno_delete(d);
}
TEST(LibDenoTest, PromiseRejectCatchHandling) {
static int count = 0;
Deno* d = deno_new([](auto _, int req_id, auto buf, auto data_buf) {
// If no error, nothing should be sent, and count should not increment
count++;
});
EXPECT_TRUE(deno_execute(d, nullptr, "a.js", "PromiseRejectCatchHandling()"));
EXPECT_EQ(count, 0);
deno_delete(d);
}

View file

@ -133,3 +133,43 @@ global.DataBuf = () => {
b[0] = 9;
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);
});
async function fn() {
throw new Error("message");
}
(async () => {
try {
await fn();
} catch (e) {
assertOrSend(count === 2);
}
})();
}

View file

@ -195,6 +195,12 @@ impl Isolate {
}
}
fn check_promise_errors(&self) {
unsafe {
libdeno::deno_check_promise_errors(self.libdeno_isolate);
}
}
// TODO Use Park abstraction? Note at time of writing Tokio default runtime
// does not have new_with_park().
pub fn event_loop(&mut self) {
@ -205,7 +211,10 @@ impl Isolate {
Err(mpsc::RecvTimeoutError::Timeout) => self.timeout(),
Err(e) => panic!("recv_deadline() failed: {:?}", e),
}
self.check_promise_errors();
}
// Check on done
self.check_promise_errors();
}
fn ntasks_increment(&mut self) {

View file

@ -33,6 +33,7 @@ extern "C" {
pub fn deno_new(cb: DenoRecvCb) -> *const isolate;
pub fn deno_delete(i: *const isolate);
pub fn deno_last_exception(i: *const isolate) -> *const c_char;
pub fn deno_check_promise_errors(i: *const isolate);
pub fn deno_respond(
i: *const isolate,
user_data: *mut c_void,

14
tests/018_async_catch.ts Normal file
View file

@ -0,0 +1,14 @@
async function fn() {
throw new Error("message");
}
async function call() {
try {
console.log("before await fn()");
await fn();
console.log("after await fn()");
} catch (error) {
console.log("catch");
}
console.log("after try-catch");
}
call().catch(() => console.log("outer catch"));

View file

@ -0,0 +1,3 @@
before await fn()
catch
after try-catch

View file

@ -1,5 +1,6 @@
hello
before error
world
Error: error
at foo ([WILDCARD]tests/async_error.ts:4:9)
at eval ([WILDCARD]tests/async_error.ts:7:1)