-
-
Notifications
You must be signed in to change notification settings - Fork 31.6k
n-api: emit uncaught-exception on calling into modules #36510
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
#include "node_buffer.h" | ||
#include "node_errors.h" | ||
#include "node_internals.h" | ||
#include "node_process.h" | ||
#include "node_url.h" | ||
#include "threadpoolwork-inl.h" | ||
#include "tracing/traced_value.h" | ||
|
@@ -23,6 +24,11 @@ node_napi_env__::node_napi_env__(v8::Local<v8::Context> context, | |
CHECK_NOT_NULL(node_env()); | ||
} | ||
|
||
node_napi_env__::~node_napi_env__() { | ||
destructing = true; | ||
FinalizeAll(); | ||
} | ||
|
||
bool node_napi_env__::can_call_into_js() const { | ||
return node_env()->can_call_into_js(); | ||
} | ||
|
@@ -35,19 +41,64 @@ v8::Maybe<bool> node_napi_env__::mark_arraybuffer_as_untransferable( | |
} | ||
|
||
void node_napi_env__::CallFinalizer(napi_finalize cb, void* data, void* hint) { | ||
CallFinalizer<true>(cb, data, hint); | ||
} | ||
|
||
template <bool enforceUncaughtExceptionPolicy> | ||
void node_napi_env__::CallFinalizer(napi_finalize cb, void* data, void* hint) { | ||
if (destructing) { | ||
// we can not defer finalizers when the environment is being destructed. | ||
v8::HandleScope handle_scope(isolate); | ||
v8::Context::Scope context_scope(context()); | ||
CallbackIntoModule<enforceUncaughtExceptionPolicy>( | ||
[&](napi_env env) { cb(env, data, hint); }); | ||
return; | ||
} | ||
// we need to keep the env live until the finalizer has been run | ||
// EnvRefHolder provides an exception safe wrapper to Ref and then | ||
// Unref once the lambda is freed | ||
EnvRefHolder liveEnv(static_cast<napi_env>(this)); | ||
node_env()->SetImmediate( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I find that we already have a outer SetImmediate in BufferFinalizer: https://github.com/nodejs/node/blob/master/src/node_api.cc#L65 Anyway, this is not related to the intention of this PR. I'm going to revert this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I merged the setimmediate in bufferfinalizer to this one since they are doing the same thing. |
||
[=, liveEnv = std::move(liveEnv)](node::Environment* node_env) { | ||
napi_env env = liveEnv.env(); | ||
node_napi_env__* env = static_cast<node_napi_env__*>(liveEnv.env()); | ||
v8::HandleScope handle_scope(env->isolate); | ||
v8::Context::Scope context_scope(env->context()); | ||
env->CallIntoModule([&](napi_env env) { cb(env, data, hint); }); | ||
env->CallbackIntoModule<enforceUncaughtExceptionPolicy>( | ||
[&](napi_env env) { cb(env, data, hint); }); | ||
}); | ||
} | ||
|
||
void node_napi_env__::trigger_fatal_exception(v8::Local<v8::Value> local_err) { | ||
v8::Local<v8::Message> local_msg = | ||
v8::Exception::CreateMessage(isolate, local_err); | ||
node::errors::TriggerUncaughtException(isolate, local_err, local_msg); | ||
} | ||
|
||
// option enforceUncaughtExceptionPolicy is added for not breaking existing | ||
// running n-api add-ons, and should be deprecated in the next major Node.js | ||
// release. | ||
template <bool enforceUncaughtExceptionPolicy, typename T> | ||
void node_napi_env__::CallbackIntoModule(T&& call) { | ||
CallIntoModule(call, [](napi_env env_, v8::Local<v8::Value> local_err) { | ||
node_napi_env__* env = static_cast<node_napi_env__*>(env_); | ||
node::Environment* node_env = env->node_env(); | ||
if (!node_env->options()->force_node_api_uncaught_exceptions_policy && | ||
!enforceUncaughtExceptionPolicy) { | ||
ProcessEmitDeprecationWarning( | ||
node_env, | ||
"Uncaught N-API callback exception detected, please run node " | ||
"with option --force-node-api-uncaught-exceptions-policy=true" | ||
"to handle those exceptions properly.", | ||
"DEP0XXX"); | ||
return; | ||
} | ||
// If there was an unhandled exception in the complete callback, | ||
// report it as a fatal exception. (There is no JavaScript on the | ||
// callstack that can possibly handle it.) | ||
env->trigger_fatal_exception(local_err); | ||
}); | ||
} | ||
|
||
namespace v8impl { | ||
|
||
namespace { | ||
|
@@ -60,20 +111,10 @@ class BufferFinalizer : private Finalizer { | |
static_cast<BufferFinalizer*>(hint)}; | ||
finalizer->_finalize_data = data; | ||
|
||
node::Environment* node_env = | ||
static_cast<node_napi_env>(finalizer->_env)->node_env(); | ||
node_env->SetImmediate( | ||
[finalizer = std::move(finalizer)](node::Environment* env) { | ||
if (finalizer->_finalize_callback == nullptr) return; | ||
|
||
v8::HandleScope handle_scope(finalizer->_env->isolate); | ||
v8::Context::Scope context_scope(finalizer->_env->context()); | ||
|
||
finalizer->_env->CallIntoModule([&](napi_env env) { | ||
finalizer->_finalize_callback( | ||
env, finalizer->_finalize_data, finalizer->_finalize_hint); | ||
}); | ||
}); | ||
if (finalizer->_finalize_callback == nullptr) return; | ||
finalizer->_env->CallFinalizer(finalizer->_finalize_callback, | ||
finalizer->_finalize_data, | ||
finalizer->_finalize_hint); | ||
} | ||
|
||
struct Deleter { | ||
|
@@ -102,13 +143,6 @@ static inline napi_env NewEnv(v8::Local<v8::Context> context, | |
return result; | ||
} | ||
|
||
static inline void trigger_fatal_exception(napi_env env, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: It might reduce the diff if you moved trigger_fatal_exception back here. |
||
v8::Local<v8::Value> local_err) { | ||
v8::Local<v8::Message> local_msg = | ||
v8::Exception::CreateMessage(env->isolate, local_err); | ||
node::errors::TriggerUncaughtException(env->isolate, local_err, local_msg); | ||
} | ||
|
||
class ThreadSafeFunction : public node::AsyncResource { | ||
public: | ||
ThreadSafeFunction(v8::Local<v8::Function> func, | ||
|
@@ -325,7 +359,7 @@ class ThreadSafeFunction : public node::AsyncResource { | |
v8::Local<v8::Function>::New(env->isolate, ref); | ||
js_callback = v8impl::JsValueFromV8LocalValue(js_cb); | ||
} | ||
env->CallIntoModule( | ||
env->CallbackIntoModule<false>( | ||
[&](napi_env env) { call_js_cb(env, js_callback, context, data); }); | ||
} | ||
|
||
|
@@ -336,7 +370,9 @@ class ThreadSafeFunction : public node::AsyncResource { | |
v8::HandleScope scope(env->isolate); | ||
if (finalize_cb) { | ||
CallbackScope cb_scope(this); | ||
env->CallIntoModule( | ||
// Do not use CallFinalizer since it will defer the invocation, which | ||
// would lead to accessing a deleted ThreadSafeFunction. | ||
env->CallbackIntoModule<false>( | ||
[&](napi_env env) { finalize_cb(env, finalize_data, context); }); | ||
} | ||
EmptyQueueAndDelete(); | ||
|
@@ -719,7 +755,7 @@ napi_status NAPI_CDECL napi_fatal_exception(napi_env env, napi_value err) { | |
CHECK_ARG(env, err); | ||
|
||
v8::Local<v8::Value> local_err = v8impl::V8LocalValueFromJsValue(err); | ||
v8impl::trigger_fatal_exception(env, local_err); | ||
static_cast<node_napi_env>(env)->trigger_fatal_exception(local_err); | ||
|
||
return napi_clear_last_error(env); | ||
} | ||
|
@@ -1064,16 +1100,9 @@ class Work : public node::AsyncResource, public node::ThreadPoolWork { | |
|
||
CallbackScope callback_scope(this); | ||
|
||
_env->CallIntoModule( | ||
[&](napi_env env) { | ||
_complete(env, ConvertUVErrorCode(status), _data); | ||
}, | ||
[](napi_env env, v8::Local<v8::Value> local_err) { | ||
// If there was an unhandled exception in the complete callback, | ||
// report it as a fatal exception. (There is no JavaScript on the | ||
// callstack that can possibly handle it.) | ||
v8impl::trigger_fatal_exception(env, local_err); | ||
}); | ||
_env->CallbackIntoModule<true>([&](napi_env env) { | ||
_complete(env, ConvertUVErrorCode(status), _data); | ||
}); | ||
|
||
// Note: Don't access `work` after this point because it was | ||
// likely deleted by the complete callback. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -432,6 +432,12 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { | |
&EnvironmentOptions::force_async_hooks_checks, | ||
kAllowedInEnvironment, | ||
true); | ||
AddOption( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm sorry I did not pick up on this earlier. At some point last year we added easier handling for boolean options. This is an example of another one AddOption("--force-async-hooks-checks",
"disable checks for async_hooks",
&EnvironmentOptions::force_async_hooks_checks,
kAllowedInEnvironment,
true); That adds 2 possible flags --force-async-hooks-checks
--no-force-async-hooks-checks I think we should use the same technique to add the boolean option we are adding. In that context what likely makes sense is AddOption("--node-api-uncaught-exceptions-policy",
"disable checks for async_hooks",
&EnvironmentOptions::force_async_hooks_checks,
kAllowedInEnvironment,
true); This would make the option on by default and provide the following flags
We can switch the default by just changing the true/false in the part that adds the option in we are forced to fall back to older behavior. |
||
"--force-node-api-uncaught-exceptions-policy", | ||
"enforces 'uncaughtException' event on Node API asynchronous callbacks", | ||
&EnvironmentOptions::force_node_api_uncaught_exceptions_policy, | ||
kAllowedInEnvironment, | ||
false); | ||
AddOption("--addons", | ||
"disable loading native addons", | ||
&EnvironmentOptions::allow_native_addons, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
'use strict'; | ||
// Flags: --expose-gc --force-node-api-uncaught-exceptions-policy | ||
|
||
const common = require('../../common'); | ||
const test_reference = require(`./build/${common.buildType}/test_reference`); | ||
const assert = require('assert'); | ||
|
||
process.on('uncaughtException', common.mustCall((err) => { | ||
assert.throws(() => { throw err; }, /finalizer error/); | ||
})); | ||
|
||
(async function() { | ||
{ | ||
test_reference.createExternalWithJsFinalize( | ||
common.mustCall(() => { | ||
throw new Error('finalizer error'); | ||
})); | ||
} | ||
global.gc(); | ||
})().then(common.mustCall()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already part of
~napi_env__()
– is there a reason for that?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CallFinalizer
is a virtual method and should not be called from the base constructor's destructor. I'd think I need to remove theFinalizeAll
in~napi_env__()
.