'use strict'; const { ArrayPrototypeUnshift, Error, FunctionPrototypeBind, ObjectPrototypeHasOwnProperty, ObjectDefineProperty, Promise, ReflectApply, Symbol, } = primordials; const async_wrap = internalBinding('async_wrap'); const { setCallbackTrampoline } = async_wrap; /* async_hook_fields is a Uint32Array wrapping the uint32_t array of * Environment::AsyncHooks::fields_[]. Each index tracks the number of active * hooks for each type. * * async_id_fields is a Float64Array wrapping the double array of * Environment::AsyncHooks::async_id_fields_[]. Each index contains the ids for * the various asynchronous states of the application. These are: * kExecutionAsyncId: The async_id assigned to the resource responsible for the * current execution stack. * kTriggerAsyncId: The async_id of the resource that caused (or 'triggered') * the resource corresponding to the current execution stack. * kAsyncIdCounter: Incremental counter tracking the next assigned async_id. * kDefaultTriggerAsyncId: Written immediately before a resource's constructor * that sets the value of the init()'s triggerAsyncId. The precedence order * of retrieving the triggerAsyncId value is: * 1. the value passed directly to the constructor * 2. value set in kDefaultTriggerAsyncId * 3. executionAsyncId of the current resource. * * async_ids_stack is a Float64Array that contains part of the async ID * stack. Each pushAsyncContext() call adds two doubles to it, and each * popAsyncContext() call removes two doubles from it. * It has a fixed size, so if that is exceeded, calls to the native * side are used instead in pushAsyncContext() and popAsyncContext(). */ const { async_hook_fields, async_id_fields, execution_async_resources } = async_wrap; // Store the pair executionAsyncId and triggerAsyncId in a AliasedFloat64Array // in Environment::AsyncHooks::async_ids_stack_ which tracks the resource // responsible for the current execution stack. This is unwound as each resource // exits. In the case of a fatal exception this stack is emptied after calling // each hook's after() callback. const { pushAsyncContext: pushAsyncContext_, popAsyncContext: popAsyncContext_, executionAsyncResource: executionAsyncResource_, clearAsyncIdStack, } = async_wrap; // For performance reasons, only track Promises when a hook is enabled. const { enablePromiseHook, disablePromiseHook } = async_wrap; // Properties in active_hooks are used to keep track of the set of hooks being // executed in case another hook is enabled/disabled. The new set of hooks is // then restored once the active set of hooks is finished executing. const active_hooks = { // Array of all AsyncHooks that will be iterated whenever an async event // fires. Using var instead of (preferably const) in order to assign // active_hooks.tmp_array if a hook is enabled/disabled during hook // execution. array: [], // Use a counter to track nested calls of async hook callbacks and make sure // the active_hooks.array isn't altered mid execution. call_depth: 0, // Use to temporarily store and updated active_hooks.array if the user // enables or disables a hook while hooks are being processed. If a hook is // enabled() or disabled() during hook execution then the current set of // active hooks is duplicated and set equal to active_hooks.tmp_array. Any // subsequent changes are on the duplicated array. When all hooks have // completed executing active_hooks.tmp_array is assigned to // active_hooks.array. tmp_array: null, // Keep track of the field counts held in active_hooks.tmp_array. Because the // async_hook_fields can't be reassigned, store each uint32 in an array that // is written back to async_hook_fields when active_hooks.array is restored. tmp_fields: null }; const { registerDestroyHook } = async_wrap; const { enqueueMicrotask } = internalBinding('task_queue'); const { resource_symbol, owner_symbol } = internalBinding('symbols'); // Each constant tracks how many callbacks there are for any given step of // async execution. These are tracked so if the user didn't include callbacks // for a given step, that step can bail out early. const { kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve, kCheck, kExecutionAsyncId, kAsyncIdCounter, kTriggerAsyncId, kDefaultTriggerAsyncId, kStackLength, kUsesExecutionAsyncResource } = async_wrap.constants; const { async_id_symbol, trigger_async_id_symbol } = internalBinding('symbols'); // Used in AsyncHook and AsyncResource. const init_symbol = Symbol('init'); const before_symbol = Symbol('before'); const after_symbol = Symbol('after'); const destroy_symbol = Symbol('destroy'); const promise_resolve_symbol = Symbol('promiseResolve'); const emitBeforeNative = emitHookFactory(before_symbol, 'emitBeforeNative'); const emitAfterNative = emitHookFactory(after_symbol, 'emitAfterNative'); const emitDestroyNative = emitHookFactory(destroy_symbol, 'emitDestroyNative'); const emitPromiseResolveNative = emitHookFactory(promise_resolve_symbol, 'emitPromiseResolveNative'); let domain_cb; function useDomainTrampoline(fn) { domain_cb = fn; } function callbackTrampoline(asyncId, resource, cb, ...args) { const index = async_hook_fields[kStackLength] - 1; execution_async_resources[index] = resource; if (asyncId !== 0 && hasHooks(kBefore)) emitBeforeNative(asyncId); let result; if (asyncId === 0 && typeof domain_cb === 'function') { ArrayPrototypeUnshift(args, cb); result = ReflectApply(domain_cb, this, args); } else { result = ReflectApply(cb, this, args); } if (asyncId !== 0 && hasHooks(kAfter)) emitAfterNative(asyncId); execution_async_resources.pop(); return result; } setCallbackTrampoline(callbackTrampoline); const topLevelResource = {}; function executionAsyncResource() { // Indicate to the native layer that this function is likely to be used, // in which case it will inform JS about the current async resource via // the trampoline above. async_hook_fields[kUsesExecutionAsyncResource] = 1; const index = async_hook_fields[kStackLength] - 1; if (index === -1) return topLevelResource; const resource = execution_async_resources[index] || executionAsyncResource_(index); return lookupPublicResource(resource); } // Used to fatally abort the process if a callback throws. function fatalError(e) { if (typeof e.stack === 'string') { process._rawDebug(e.stack); } else { const o = { message: e }; // eslint-disable-next-line no-restricted-syntax Error.captureStackTrace(o, fatalError); process._rawDebug(o.stack); } const { getOptionValue } = require('internal/options'); if (getOptionValue('--abort-on-uncaught-exception')) { process.abort(); } process.exit(1); } function lookupPublicResource(resource) { if (typeof resource !== 'object' || resource === null) return resource; // TODO(addaleax): Merge this with owner_symbol and use it across all // AsyncWrap instances. const publicResource = resource[resource_symbol]; if (publicResource !== undefined) return publicResource; return resource; } // Emit From Native // // Used by C++ to call all init() callbacks. Because some state can be setup // from C++ there's no need to perform all the same operations as in // emitInitScript. function emitInitNative(asyncId, type, triggerAsyncId, resource) { active_hooks.call_depth += 1; resource = lookupPublicResource(resource); // Use a single try/catch for all hooks to avoid setting up one per iteration. try { // Using var here instead of let because "for (var ...)" is faster than let. // Refs: https://github.com/nodejs/node/pull/30380#issuecomment-552948364 for (var i = 0; i < active_hooks.array.length; i++) { if (typeof active_hooks.array[i][init_symbol] === 'function') { active_hooks.array[i][init_symbol]( asyncId, type, triggerAsyncId, resource ); } } } catch (e) { fatalError(e); } finally { active_hooks.call_depth -= 1; } // Hooks can only be restored if there have been no recursive hook calls. // Also the active hooks do not need to be restored if enable()/disable() // weren't called during hook execution, in which case active_hooks.tmp_array // will be null. if (active_hooks.call_depth === 0 && active_hooks.tmp_array !== null) { restoreActiveHooks(); } } // Called from native. The asyncId stack handling is taken care of there // before this is called. function emitHook(symbol, asyncId) { active_hooks.call_depth += 1; // Use a single try/catch for all hook to avoid setting up one per // iteration. try { // Using var here instead of let because "for (var ...)" is faster than let. // Refs: https://github.com/nodejs/node/pull/30380#issuecomment-552948364 for (var i = 0; i < active_hooks.array.length; i++) { if (typeof active_hooks.array[i][symbol] === 'function') { active_hooks.array[i][symbol](asyncId); } } } catch (e) { fatalError(e); } finally { active_hooks.call_depth -= 1; } // Hooks can only be restored if there have been no recursive hook calls. // Also the active hooks do not need to be restored if enable()/disable() // weren't called during hook execution, in which case // active_hooks.tmp_array will be null. if (active_hooks.call_depth === 0 && active_hooks.tmp_array !== null) { restoreActiveHooks(); } } function emitHookFactory(symbol, name) { const fn = FunctionPrototypeBind(emitHook, undefined, symbol); // Set the name property of the function as it looks good in the stack trace. ObjectDefineProperty(fn, 'name', { value: name }); return fn; } // Manage Active Hooks // function getHookArrays() { if (active_hooks.call_depth === 0) return [active_hooks.array, async_hook_fields]; // If this hook is being enabled while in the middle of processing the array // of currently active hooks then duplicate the current set of active hooks // and store this there. This shouldn't fire until the next time hooks are // processed. if (active_hooks.tmp_array === null) storeActiveHooks(); return [active_hooks.tmp_array, active_hooks.tmp_fields]; } function storeActiveHooks() { active_hooks.tmp_array = active_hooks.array.slice(); // Don't want to make the assumption that kInit to kDestroy are indexes 0 to // 4. So do this the long way. active_hooks.tmp_fields = []; copyHooks(active_hooks.tmp_fields, async_hook_fields); } function copyHooks(destination, source) { destination[kInit] = source[kInit]; destination[kBefore] = source[kBefore]; destination[kAfter] = source[kAfter]; destination[kDestroy] = source[kDestroy]; destination[kPromiseResolve] = source[kPromiseResolve]; } // Then restore the correct hooks array in case any hooks were added/removed // during hook callback execution. function restoreActiveHooks() { active_hooks.array = active_hooks.tmp_array; copyHooks(async_hook_fields, active_hooks.tmp_fields); active_hooks.tmp_array = null; active_hooks.tmp_fields = null; } function trackPromise(promise, parent, silent) { const asyncId = getOrSetAsyncId(promise); promise[trigger_async_id_symbol] = parent ? getOrSetAsyncId(parent) : getDefaultTriggerAsyncId(); if (!silent && initHooksExist()) { const triggerId = promise[trigger_async_id_symbol]; emitInitScript(asyncId, 'PROMISE', triggerId, promise); } } function fastPromiseHook(type, promise, parent) { if (type === kInit || !promise[async_id_symbol]) { const silent = type !== kInit; if (parent instanceof Promise) { trackPromise(promise, parent, silent); } else { trackPromise(promise, null, silent); } if (!silent) return; } const asyncId = promise[async_id_symbol]; switch (type) { case kBefore: const triggerId = promise[trigger_async_id_symbol]; emitBeforeScript(asyncId, triggerId, promise); break; case kAfter: if (hasHooks(kAfter)) { emitAfterNative(asyncId); } if (asyncId === executionAsyncId()) { // This condition might not be true if async_hooks was enabled during // the promise callback execution. // Popping it off the stack can be skipped in that case, because it is // known that it would correspond to exactly one call with // PromiseHookType::kBefore that was not witnessed by the PromiseHook. popAsyncContext(asyncId); } break; case kPromiseResolve: emitPromiseResolveNative(asyncId); break; } } let wantPromiseHook = false; function enableHooks() { async_hook_fields[kCheck] += 1; } let promiseHookMode = -1; function updatePromiseHookMode() { wantPromiseHook = true; if (destroyHooksExist()) { if (promiseHookMode !== 1) { promiseHookMode = 1; enablePromiseHook(); } } else if (promiseHookMode !== 0) { promiseHookMode = 0; enablePromiseHook(fastPromiseHook); } } function disableHooks() { async_hook_fields[kCheck] -= 1; wantPromiseHook = false; // Delay the call to `disablePromiseHook()` because we might currently be // between the `before` and `after` calls of a Promise. enqueueMicrotask(disablePromiseHookIfNecessary); } function disablePromiseHookIfNecessary() { if (!wantPromiseHook) { promiseHookMode = -1; disablePromiseHook(); } } // Internal Embedder API // // Increment the internal id counter and return the value. Important that the // counter increment first. Since it's done the same way in // Environment::new_async_uid() function newAsyncId() { return ++async_id_fields[kAsyncIdCounter]; } function getOrSetAsyncId(object) { if (ObjectPrototypeHasOwnProperty(object, async_id_symbol)) { return object[async_id_symbol]; } return object[async_id_symbol] = newAsyncId(); } // Return the triggerAsyncId meant for the constructor calling it. It's up to // the user to safeguard this call and make sure it's zero'd out when the // constructor is complete. function getDefaultTriggerAsyncId() { const defaultTriggerAsyncId = async_id_fields[kDefaultTriggerAsyncId]; // If defaultTriggerAsyncId isn't set, use the executionAsyncId if (defaultTriggerAsyncId < 0) return async_id_fields[kExecutionAsyncId]; return defaultTriggerAsyncId; } function clearDefaultTriggerAsyncId() { async_id_fields[kDefaultTriggerAsyncId] = -1; } function defaultTriggerAsyncIdScope(triggerAsyncId, block, ...args) { if (triggerAsyncId === undefined) return block(...args); // CHECK(NumberIsSafeInteger(triggerAsyncId)) // CHECK(triggerAsyncId > 0) const oldDefaultTriggerAsyncId = async_id_fields[kDefaultTriggerAsyncId]; async_id_fields[kDefaultTriggerAsyncId] = triggerAsyncId; try { return block(...args); } finally { async_id_fields[kDefaultTriggerAsyncId] = oldDefaultTriggerAsyncId; } } function hasHooks(key) { return async_hook_fields[key] > 0; } function enabledHooksExist() { return hasHooks(kCheck); } function initHooksExist() { return hasHooks(kInit); } function afterHooksExist() { return hasHooks(kAfter); } function destroyHooksExist() { return hasHooks(kDestroy); } function emitInitScript(asyncId, type, triggerAsyncId, resource) { // Short circuit all checks for the common case. Which is that no hooks have // been set. Do this to remove performance impact for embedders (and core). if (!hasHooks(kInit)) return; if (triggerAsyncId === null) { triggerAsyncId = getDefaultTriggerAsyncId(); } emitInitNative(asyncId, type, triggerAsyncId, resource); } function emitBeforeScript(asyncId, triggerAsyncId, resource) { pushAsyncContext(asyncId, triggerAsyncId, resource); if (hasHooks(kBefore)) emitBeforeNative(asyncId); } function emitAfterScript(asyncId) { if (hasHooks(kAfter)) emitAfterNative(asyncId); popAsyncContext(asyncId); } function emitDestroyScript(asyncId) { // Return early if there are no destroy callbacks, or invalid asyncId. if (!hasHooks(kDestroy) || asyncId <= 0) return; async_wrap.queueDestroyAsyncId(asyncId); } function hasAsyncIdStack() { return hasHooks(kStackLength); } // This is the equivalent of the native push_async_ids() call. function pushAsyncContext(asyncId, triggerAsyncId, resource) { const offset = async_hook_fields[kStackLength]; execution_async_resources[offset] = resource; if (offset * 2 >= async_wrap.async_ids_stack.length) return pushAsyncContext_(asyncId, triggerAsyncId); async_wrap.async_ids_stack[offset * 2] = async_id_fields[kExecutionAsyncId]; async_wrap.async_ids_stack[offset * 2 + 1] = async_id_fields[kTriggerAsyncId]; async_hook_fields[kStackLength]++; async_id_fields[kExecutionAsyncId] = asyncId; async_id_fields[kTriggerAsyncId] = triggerAsyncId; } // This is the equivalent of the native pop_async_ids() call. function popAsyncContext(asyncId) { const stackLength = async_hook_fields[kStackLength]; if (stackLength === 0) return false; if (enabledHooksExist() && async_id_fields[kExecutionAsyncId] !== asyncId) { // Do the same thing as the native code (i.e. crash hard). return popAsyncContext_(asyncId); } const offset = stackLength - 1; async_id_fields[kExecutionAsyncId] = async_wrap.async_ids_stack[2 * offset]; async_id_fields[kTriggerAsyncId] = async_wrap.async_ids_stack[2 * offset + 1]; execution_async_resources.pop(); async_hook_fields[kStackLength] = offset; return offset > 0; } function executionAsyncId() { return async_id_fields[kExecutionAsyncId]; } function triggerAsyncId() { return async_id_fields[kTriggerAsyncId]; } module.exports = { executionAsyncId, triggerAsyncId, // Private API getHookArrays, symbols: { async_id_symbol, trigger_async_id_symbol, init_symbol, before_symbol, after_symbol, destroy_symbol, promise_resolve_symbol, owner_symbol }, constants: { kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve }, enableHooks, disableHooks, updatePromiseHookMode, clearDefaultTriggerAsyncId, clearAsyncIdStack, hasAsyncIdStack, executionAsyncResource, // Internal Embedder API newAsyncId, getOrSetAsyncId, getDefaultTriggerAsyncId, defaultTriggerAsyncIdScope, enabledHooksExist, initHooksExist, afterHooksExist, destroyHooksExist, emitInit: emitInitScript, emitBefore: emitBeforeScript, emitAfter: emitAfterScript, emitDestroy: emitDestroyScript, registerDestroyHook, useDomainTrampoline, nativeHooks: { init: emitInitNative, before: emitBeforeNative, after: emitAfterNative, destroy: emitDestroyNative, promise_resolve: emitPromiseResolveNative } };