diff --git a/doc/api/util.md b/doc/api/util.md index 1c7d2393a143dd..39c920ae965647 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -1631,6 +1631,80 @@ const module = new WebAssembly.Module(wasmBuffer); util.types.isWebAssemblyCompiledModule(module); // Returns true ``` +### util.createPromiseHook(hooks) + + +> Stability: 1 - Experimental + +* `hooks` {Object} + * `resolve` {Function} Called when a promise resolve function is called. The + promise is not fulfilled or rejected at this point. + * `rejectWithNoHandler` {Function} Called when a promise rejects without any + handler. + * `handlerAddedAfterReject` {Function} Called when a promise handler is added + to a promise that rejected without a handler. + * `rejectAfterResolved` {Function} Called when a promise reject function is + called after resolution. + * `resolveAfterResolved` {Function} Called when a promise resolve function is + called after resolution. + +Returns: {Object} + * `enable` {Function} Enables the hook + * `disable` {Function} Disables the hook + +Registers the promise hook `hook` to be called for certain promise debugging +events. + +```js +util.createPromiseHook({ + resolve(promise) { + console.log('Promise did resolve'); + }, + rejectWithNoHandler(promise, value) { + console.log('Promise did rejectWithNoHandler with', value); + }, + handlerAddedAfterReject(promise) { + console.log('Promise did handlerAddedAfterReject'); + }, +}).enable(); + +const x = Promise.resolve('success') + .then(() => Promise.reject('error')); + +setTimeout(() => { + x.catch(() => {}); +}, 10); + +// Promise did resolve +// Promise did resolve +// Promise did rejectWithNoHandler with 'error' +// Promise did resolve +// Promise did handlerAddedAfterReject +// Promise did resolve +// Promise did rejectWithNoHandler with 'error' +// Promise did resolve +// Promise did handlerAddedAfterReject +// Promise did resolve +``` + +```js +util.createPromiseHook({ + rejectAfterResolved(promise, value) { + console.log('Promise did rejectAfterResolved with', value); + }, +}).enable(); + +new Promise((resolve, reject) => { + resolve('success'); + + reject('oops'); +}); + +// Promise did rejectAfterResolved with 'oops' +``` + ## Deprecated APIs The following APIs are deprecated and should no longer be used. Existing diff --git a/lib/util.js b/lib/util.js index 3d3bbdb6120fa2..03c0a4a175a7be 100644 --- a/lib/util.js +++ b/lib/util.js @@ -21,6 +21,7 @@ 'use strict'; +const { internalBinding } = require('internal/bootstrap/loaders'); const errors = require('internal/errors'); const { ERR_FALSY_VALUE_REJECTION, @@ -37,8 +38,8 @@ const { kRejected, previewEntries } = process.binding('util'); +const { setPromiseHook } = internalBinding('safe_util'); -const { internalBinding } = require('internal/bootstrap/loaders'); const types = internalBinding('types'); Object.assign(types, require('internal/util/types')); const { @@ -1464,6 +1465,72 @@ function getSystemErrorName(err) { return internalErrorName(err); } +const promiseEventTypes = [ + 'init', + 'resolve', + 'before', + 'after', + 'rejectWithNoHandler', + 'handlerAddedAfterReject', + 'rejectAfterResolved', + 'resolveAfterResolved', +]; +const promiseHooks = new Set(); +let promiseHookWarned = false; +let resolveHookRef = 0; +function promiseHookCallback(type, promise, value) { + type = promiseEventTypes[type]; + for (const hooks of promiseHooks) { + try { + const hook = hooks[type]; + if (typeof hook === 'function') { + hook(promise, value); + } + } catch (e) { + process.nextTick(() => { throw e; }); + } + } +} +function createPromiseHook(hooks) { + if (!promiseHookWarned) { + promiseHookWarned = true; + process.emitWarning('The util.createPromiseHook API is experimental.', + 'ExperimentalWarning'); + } + + const hasResolve = 'resolve' in hooks; + + promiseEventTypes.forEach((type) => { + const hook = hooks[type]; + if (hook === undefined) { + return; + } + + if (typeof hook !== 'function') { + throw new ERR_INVALID_ARG_TYPE(`hooks.${type}`, 'function', hook); + } + }); + + return { + enable() { + promiseHooks.add(hooks); + if (hasResolve) { + resolveHookRef++; + } + setPromiseHook(promiseHookCallback, resolveHookRef > 0); + }, + disable() { + promiseHooks.delete(hooks); + if (hasResolve) { + resolveHookRef--; + } + setPromiseHook( + hooks.size === 0 ? null : promiseHookCallback, + resolveHookRef > 0); + }, + }; +} + // Keep the `exports =` so that various functions can still be monkeypatched module.exports = exports = { _errnoException: errors.errnoException, @@ -1505,6 +1572,8 @@ module.exports = exports = { TextEncoder, types, + createPromiseHook, + // Deprecated Old Stuff debug: deprecate(debug, 'util.debug is deprecated. Use console.error instead.', diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc index 94bb9413772dd0..cf2820fe8b6805 100644 --- a/src/bootstrapper.cc +++ b/src/bootstrapper.cc @@ -16,8 +16,6 @@ using v8::Local; using v8::MaybeLocal; using v8::Object; using v8::Promise; -using v8::PromiseRejectEvent; -using v8::PromiseRejectMessage; using v8::String; using v8::Value; @@ -52,30 +50,21 @@ void SetupNextTick(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(ret); } -void PromiseRejectCallback(PromiseRejectMessage message) { +void PromiseRejectCallback(PromiseHookType type, + Local promise, + Local value, + void* arg) { static std::atomic unhandledRejections{0}; static std::atomic rejectionsHandledAfter{0}; - Local promise = message.GetPromise(); - Isolate* isolate = promise->GetIsolate(); - PromiseRejectEvent event = message.GetEvent(); - - Environment* env = Environment::GetCurrent(isolate); + Environment* env = static_cast(arg); Local callback; - Local value; - if (event == v8::kPromiseRejectWithNoHandler) { + if (type == PromiseHookType::kRejectWithNoHandler) { callback = env->promise_reject_unhandled_function(); - value = message.GetValue(); - - if (value.IsEmpty()) - value = Undefined(isolate); - unhandledRejections++; - } else if (event == v8::kPromiseHandlerAddedAfterReject) { + } else if (type == PromiseHookType::kHandlerAddedAfterReject) { callback = env->promise_reject_handled_function(); - value = Undefined(isolate); - rejectionsHandledAfter++; } else { return; @@ -86,10 +75,13 @@ void PromiseRejectCallback(PromiseRejectMessage message) { "unhandled", unhandledRejections, "handledAfter", rejectionsHandledAfter); + if (value.IsEmpty()) { + value = Undefined(env->isolate()); + } Local args[] = { promise, value }; MaybeLocal ret = callback->Call(env->context(), - Undefined(isolate), + Undefined(env->isolate()), arraysize(args), args); @@ -99,14 +91,14 @@ void PromiseRejectCallback(PromiseRejectMessage message) { void SetupPromises(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - Isolate* isolate = env->isolate(); CHECK(args[0]->IsFunction()); CHECK(args[1]->IsFunction()); - isolate->SetPromiseRejectCallback(PromiseRejectCallback); env->set_promise_reject_unhandled_function(args[0].As()); env->set_promise_reject_handled_function(args[1].As()); + + env->AddPromiseRejectHook(PromiseRejectCallback, env); } #define BOOTSTRAP_METHOD(name, fn) env->SetMethod(bootstrapper, #name, fn) diff --git a/src/env.cc b/src/env.cc index 244c6d8be37685..4e40442ed387c1 100644 --- a/src/env.cc +++ b/src/env.cc @@ -405,21 +405,22 @@ void Environment::AtExit(void (*cb)(void* arg), void* arg) { at_exit_functions_.push_back(ExitCallback{cb, arg}); } -void Environment::AddPromiseHook(promise_hook_func fn, void* arg) { +bool Environment::AddPromiseHook(promise_hook_func fn, void* arg) { auto it = std::find_if( promise_hooks_.begin(), promise_hooks_.end(), [&](const PromiseHookCallback& hook) { return hook.cb_ == fn && hook.arg_ == arg; }); if (it != promise_hooks_.end()) { - it->enable_count_++; - return; + return false; } - promise_hooks_.push_back(PromiseHookCallback{fn, arg, 1}); + promise_hooks_.push_back(PromiseHookCallback{fn, arg}); if (promise_hooks_.size() == 1) { isolate_->SetPromiseHook(EnvPromiseHook); } + + return true; } bool Environment::RemovePromiseHook(promise_hook_func fn, void* arg) { @@ -431,9 +432,8 @@ bool Environment::RemovePromiseHook(promise_hook_func fn, void* arg) { if (it == promise_hooks_.end()) return false; - if (--it->enable_count_ > 0) return true; - promise_hooks_.erase(it); + if (promise_hooks_.empty()) { isolate_->SetPromiseHook(nullptr); } @@ -441,7 +441,39 @@ bool Environment::RemovePromiseHook(promise_hook_func fn, void* arg) { return true; } -void Environment::EnvPromiseHook(v8::PromiseHookType type, +bool Environment::AddPromiseRejectHook(promise_hook_func fn, void* arg) { + auto it = std::find_if(promise_reject_hooks_.begin(), + promise_reject_hooks_.end(), + [&](const PromiseHookCallback& hook) { + return hook.cb_ == fn && hook.arg_ == arg; + }); + if (it != promise_reject_hooks_.end()) { + return false; + } + promise_reject_hooks_.push_back(PromiseHookCallback{fn, arg}); + if (promise_reject_hooks_.size() == 1) { + isolate_->SetPromiseRejectCallback(EnvPromiseRejectCallback); + } + return true; +} + +bool Environment::RemovePromiseRejectHook(promise_hook_func fn, void* arg) { + auto it = std::find_if(promise_reject_hooks_.begin(), + promise_reject_hooks_.end(), + [&](const PromiseHookCallback& hook) { + return hook.cb_ == fn && hook.arg_ == arg; + }); + if (it == promise_reject_hooks_.end()) { + return false; + } + promise_reject_hooks_.erase(it); + if (promise_reject_hooks_.empty()) { + isolate_->SetPromiseRejectCallback(nullptr); + } + return true; +} + +void Environment::EnvPromiseHook(v8::PromiseHookType v8type, v8::Local promise, v8::Local parent) { Local context = promise->CreationContext(); @@ -458,11 +490,65 @@ void Environment::EnvPromiseHook(v8::PromiseHookType type, } Environment* env = Environment::GetCurrent(context); + + node::PromiseHookType type; + if (v8type == v8::PromiseHookType::kInit) { + type = node::PromiseHookType::kInit; + } else if (v8type == v8::PromiseHookType::kResolve) { + type = node::PromiseHookType::kResolve; + } else if (v8type == v8::PromiseHookType::kBefore) { + type = node::PromiseHookType::kBefore; + } else if (v8type == v8::PromiseHookType::kAfter) { + type = node::PromiseHookType::kAfter; + } else { + return; + } + for (const PromiseHookCallback& hook : env->promise_hooks_) { hook.cb_(type, promise, parent, hook.arg_); } } +void Environment::EnvPromiseRejectCallback(v8::PromiseRejectMessage message) { + v8::Local promise = message.GetPromise(); + Local context = promise->CreationContext(); + + // Grow the embedder data if necessary to make sure we are not out of bounds + // when reading the magic number. + context->SetAlignedPointerInEmbedderData( + ContextEmbedderIndex::kContextTagBoundary, nullptr); + int* magicNumberPtr = reinterpret_cast( + context->GetAlignedPointerFromEmbedderData( + ContextEmbedderIndex::kContextTag)); + if (magicNumberPtr != Environment::kNodeContextTagPtr) { + return; + } + + v8::PromiseRejectEvent event = message.GetEvent(); + + node::PromiseHookType type; + if (event == v8::PromiseRejectEvent::kPromiseRejectWithNoHandler) { + type = node::PromiseHookType::kRejectWithNoHandler; + } else if (event == v8::PromiseRejectEvent::kPromiseHandlerAddedAfterReject) { + type = node::PromiseHookType::kHandlerAddedAfterReject; + } else if (event == v8::PromiseRejectEvent::kPromiseRejectAfterResolved) { + type = node::PromiseHookType::kRejectAfterResolved; + } else if (event == v8::PromiseRejectEvent::kPromiseResolveAfterResolved) { + type = node::PromiseHookType::kResolveAfterResolved; + } else { + return; + } + + v8::Isolate* isolate = promise->GetIsolate(); + Local value = message.GetValue(); + + Environment* env = Environment::GetCurrent(isolate); + + for (const PromiseHookCallback& hook : env->promise_reject_hooks_) { + hook.cb_(type, promise, message.GetValue(), hook.arg_); + } +} + void Environment::RunAndClearNativeImmediates() { size_t count = native_immediate_callbacks_.size(); if (count > 0) { diff --git a/src/env.h b/src/env.h index 87085bf94656b4..f80892fa90add8 100644 --- a/src/env.h +++ b/src/env.h @@ -346,6 +346,7 @@ struct PackageConfig { V(tls_wrap_constructor_function, v8::Function) \ V(tty_constructor_template, v8::FunctionTemplate) \ V(udp_constructor_function, v8::Function) \ + V(util_promise_hook_callback, v8::Function) \ V(url_constructor_function, v8::Function) \ V(write_wrap_template, v8::ObjectTemplate) @@ -808,8 +809,10 @@ class Environment { inline HandleWrapQueue* handle_wrap_queue() { return &handle_wrap_queue_; } inline ReqWrapQueue* req_wrap_queue() { return &req_wrap_queue_; } - void AddPromiseHook(promise_hook_func fn, void* arg); + bool AddPromiseHook(promise_hook_func fn, void* arg); bool RemovePromiseHook(promise_hook_func fn, void* arg); + bool AddPromiseRejectHook(promise_hook_func fn, void* arg); + bool RemovePromiseRejectHook(promise_hook_func fn, void* arg); inline bool EmitProcessEnvWarning() { bool current_value = emit_env_nonstring_warning_; emit_env_nonstring_warning_ = false; @@ -945,9 +948,9 @@ class Environment { struct PromiseHookCallback { promise_hook_func cb_; void* arg_; - size_t enable_count_; }; std::vector promise_hooks_; + std::vector promise_reject_hooks_; struct NativeImmediateCallback { native_immediate_callback cb_; @@ -990,6 +993,7 @@ class Environment { static void EnvPromiseHook(v8::PromiseHookType type, v8::Local promise, v8::Local parent); + static void EnvPromiseRejectCallback(v8::PromiseRejectMessage message); template void ForEachBaseObject(T&& iterator); diff --git a/src/node.h b/src/node.h index a94612eb0b9de8..1d99f8a31e223d 100644 --- a/src/node.h +++ b/src/node.h @@ -616,9 +616,20 @@ NODE_EXTERN void AtExit(void (*cb)(void* arg), void* arg = 0); */ NODE_EXTERN void AtExit(Environment* env, void (*cb)(void* arg), void* arg = 0); -typedef void (*promise_hook_func) (v8::PromiseHookType type, +typedef enum { + kInit, + kResolve, + kBefore, + kAfter, + kRejectWithNoHandler, + kHandlerAddedAfterReject, + kRejectAfterResolved, + kResolveAfterResolved, +} PromiseHookType; + +typedef void (*promise_hook_func) (PromiseHookType type, v8::Local promise, - v8::Local parent, + v8::Local value, void* arg); typedef double async_id; diff --git a/src/node_internals.h b/src/node_internals.h index 0594d00580a779..372779e4543745 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -139,6 +139,7 @@ struct sockaddr; V(udp_wrap) \ V(url) \ V(util) \ + V(safe_util) \ V(uv) \ V(v8) \ V(worker) \ @@ -150,7 +151,7 @@ struct sockaddr; NODE_BUILTIN_ICU_MODULES(V) #define NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, priv, flags) \ - static node::node_module _module = { \ + static node::node_module _module_##modname = { \ NODE_MODULE_VERSION, \ flags, \ nullptr, \ @@ -162,7 +163,7 @@ struct sockaddr; nullptr \ }; \ void _register_ ## modname() { \ - node_module_register(&_module); \ + node_module_register(&_module_##modname); \ } diff --git a/src/node_util.cc b/src/node_util.cc index 41b1307bb4912c..114e1a6aaaaa14 100644 --- a/src/node_util.cc +++ b/src/node_util.cc @@ -6,9 +6,11 @@ namespace util { using v8::Array; using v8::Context; +using v8::Function; using v8::FunctionCallbackInfo; using v8::Integer; using v8::Local; +using v8::Number; using v8::Object; using v8::Private; using v8::Promise; @@ -140,7 +142,7 @@ void SafeGetenv(const FunctionCallbackInfo& args) { v8::NewStringType::kNormal).ToLocalChecked()); } -void Initialize(Local target, +void InitializeBase(Local target, Local unused, Local context) { Environment* env = Environment::GetCurrent(context); @@ -186,7 +188,68 @@ void Initialize(Local target, env->SetMethod(target, "safeGetenv", SafeGetenv); } +void InitializeUnsafe(Local target, + Local unused, + Local context) { + InitializeBase(target, unused, context); +} + +void PromiseHook(PromiseHookType type, + Local promise, + Local value, + void* arg) { + switch (type) { + case PromiseHookType::kResolve: + case PromiseHookType::kRejectWithNoHandler: + case PromiseHookType::kHandlerAddedAfterReject: + case PromiseHookType::kRejectAfterResolved: + case PromiseHookType::kResolveAfterResolved: { + Environment* env = static_cast(arg); + Local cb = env->util_promise_hook_callback(); + int argc = value.IsEmpty() ? 2 : 3; + Local args[] = { + Number::New(env->isolate(), type), + promise, + value, + }; + FatalTryCatch try_catch(env); + USE(cb->Call(env->context(), Undefined(env->isolate()), argc, args)); + break; + } + default: + return; + } +} + +void SetPromiseHook(const FunctionCallbackInfo& info) { + Environment* env = Environment::GetCurrent(info); + + if (info[0]->IsNull()) { + env->RemovePromiseRejectHook(PromiseHook, env); + env->RemovePromiseHook(PromiseHook, env); + } else { + CHECK(info[0]->IsFunction()); + CHECK(info[1]->IsBoolean()); + env->set_util_promise_hook_callback(info[0].As()); + env->AddPromiseRejectHook(PromiseHook, env); + if (info[1]->IsTrue()) { + env->AddPromiseHook(PromiseHook, env); + } + } +} + +void InitializeSafe(Local target, + Local unused, + Local context) { + InitializeBase(target, unused, context); + + Environment* env = Environment::GetCurrent(context); + + env->SetMethod(target, "setPromiseHook", SetPromiseHook); +} + } // namespace util } // namespace node -NODE_BUILTIN_MODULE_CONTEXT_AWARE(util, node::util::Initialize) +NODE_MODULE_CONTEXT_AWARE_INTERNAL(safe_util, node::util::InitializeSafe) +NODE_BUILTIN_MODULE_CONTEXT_AWARE(util, node::util::InitializeUnsafe) diff --git a/test/parallel/test-util-promise-hooks-async.js b/test/parallel/test-util-promise-hooks-async.js new file mode 100644 index 00000000000000..491167e844b688 --- /dev/null +++ b/test/parallel/test-util-promise-hooks-async.js @@ -0,0 +1,37 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const util = require('util'); + +const calls = []; + +const makeHook = (type) => (promise, value) => { + calls.push({ type, promise, value }); +}; +util.createPromiseHook({ + resolve: makeHook('resolve'), + rejectWithNoHandler: makeHook('rejectWithNoHandler'), + handlerAddedAfterReject: makeHook('handlerAddedAfterReject'), + rejectAfterResolved: makeHook('rejectAfterResolved'), + resolveAfterResolved: makeHook('resolveAfterResolved'), +}).enable(); + +const x = (async () => { + throw 'error'; // eslint-disable-line no-throw-literal +})(); + +const check = (item, type, value) => { + assert.strictEqual(item.type, type); + assert.strictEqual(item.value, value); +}; + +process.nextTick(() => { + x.catch(() => {}); + + assert.strictEqual(calls.length, 3); + + check(calls[0], 'resolve', undefined); + check(calls[1], 'rejectWithNoHandler', 'error'); + check(calls[2], 'handlerAddedAfterReject', undefined); +}); diff --git a/test/parallel/test-util-promise-hooks.js b/test/parallel/test-util-promise-hooks.js new file mode 100644 index 00000000000000..73c075a2851520 --- /dev/null +++ b/test/parallel/test-util-promise-hooks.js @@ -0,0 +1,61 @@ +'use strict'; + +const { disableCrashOnUnhandledRejection } = require('../common'); +disableCrashOnUnhandledRejection(); +process.on('unhandledRejection', () => {}); + +const assert = require('assert'); + +const util = require('util'); + +let calls = []; + +const makeHook = (type) => (promise, value) => { + calls.push({ type, promise, value }); +}; +util.createPromiseHook({ + resolve: makeHook('resolve'), + rejectWithNoHandler: makeHook('rejectWithNoHandler'), + handlerAddedAfterReject: makeHook('handlerAddedAfterReject'), + rejectAfterResolved: makeHook('rejectAfterResolved'), + resolveAfterResolved: makeHook('resolveAfterResolved'), +}).enable(); + +const first = Promise.resolve('success'); +let second; +const third = first.then(() => { + second = Promise.reject('error'); + return second; +}); + + +const check = (item, type, promise, value) => { + assert.strictEqual(item.type, type); + assert.strictEqual(item.promise, promise); + assert.strictEqual(item.value, value); +}; + +setTimeout(() => { + const fourth = third.catch(() => {}); + + calls = calls.filter((c) => + [first, second, third, fourth].includes(c.promise)); + + assert.strictEqual(calls.length, 8); + + check(calls[0], 'resolve', first, undefined); + + check(calls[1], 'resolve', second, undefined); + + check(calls[2], 'rejectWithNoHandler', second, 'error'); + + check(calls[3], 'resolve', third, undefined); + + check(calls[4], 'handlerAddedAfterReject', second, undefined); + + check(calls[5], 'resolve', third, undefined); + + check(calls[6], 'rejectWithNoHandler', third, 'error'); + + check(calls[7], 'handlerAddedAfterReject', third, undefined); +}, 10);