1'use strict'; 2 3const { 4 ArrayPrototypeSlice, 5 ErrorCaptureStackTrace, 6 ObjectPrototypeHasOwnProperty, 7 ObjectDefineProperty, 8 Symbol, 9} = primordials; 10 11const async_wrap = internalBinding('async_wrap'); 12const { setCallbackTrampoline } = async_wrap; 13/* async_hook_fields is a Uint32Array wrapping the uint32_t array of 14 * Environment::AsyncHooks::fields_[]. Each index tracks the number of active 15 * hooks for each type. 16 * 17 * async_id_fields is a Float64Array wrapping the double array of 18 * Environment::AsyncHooks::async_id_fields_[]. Each index contains the ids for 19 * the various asynchronous states of the application. These are: 20 * kExecutionAsyncId: The async_id assigned to the resource responsible for the 21 * current execution stack. 22 * kTriggerAsyncId: The async_id of the resource that caused (or 'triggered') 23 * the resource corresponding to the current execution stack. 24 * kAsyncIdCounter: Incremental counter tracking the next assigned async_id. 25 * kDefaultTriggerAsyncId: Written immediately before a resource's constructor 26 * that sets the value of the init()'s triggerAsyncId. The precedence order 27 * of retrieving the triggerAsyncId value is: 28 * 1. the value passed directly to the constructor 29 * 2. value set in kDefaultTriggerAsyncId 30 * 3. executionAsyncId of the current resource. 31 * 32 * async_ids_stack is a Float64Array that contains part of the async ID 33 * stack. Each pushAsyncContext() call adds two doubles to it, and each 34 * popAsyncContext() call removes two doubles from it. 35 * It has a fixed size, so if that is exceeded, calls to the native 36 * side are used instead in pushAsyncContext() and popAsyncContext(). 37 */ 38const { 39 async_hook_fields, 40 async_id_fields, 41 execution_async_resources, 42} = async_wrap; 43// Store the pair executionAsyncId and triggerAsyncId in a AliasedFloat64Array 44// in Environment::AsyncHooks::async_ids_stack_ which tracks the resource 45// responsible for the current execution stack. This is unwound as each resource 46// exits. In the case of a fatal exception this stack is emptied after calling 47// each hook's after() callback. 48const { 49 pushAsyncContext: pushAsyncContext_, 50 popAsyncContext: popAsyncContext_, 51 executionAsyncResource: executionAsyncResource_, 52 clearAsyncIdStack, 53} = async_wrap; 54// Properties in active_hooks are used to keep track of the set of hooks being 55// executed in case another hook is enabled/disabled. The new set of hooks is 56// then restored once the active set of hooks is finished executing. 57const active_hooks = { 58 // Array of all AsyncHooks that will be iterated whenever an async event 59 // fires. Using var instead of (preferably const) in order to assign 60 // active_hooks.tmp_array if a hook is enabled/disabled during hook 61 // execution. 62 array: [], 63 // Use a counter to track nested calls of async hook callbacks and make sure 64 // the active_hooks.array isn't altered mid execution. 65 call_depth: 0, 66 // Use to temporarily store and updated active_hooks.array if the user 67 // enables or disables a hook while hooks are being processed. If a hook is 68 // enabled() or disabled() during hook execution then the current set of 69 // active hooks is duplicated and set equal to active_hooks.tmp_array. Any 70 // subsequent changes are on the duplicated array. When all hooks have 71 // completed executing active_hooks.tmp_array is assigned to 72 // active_hooks.array. 73 tmp_array: null, 74 // Keep track of the field counts held in active_hooks.tmp_array. Because the 75 // async_hook_fields can't be reassigned, store each uint32 in an array that 76 // is written back to async_hook_fields when active_hooks.array is restored. 77 tmp_fields: null, 78}; 79 80const { registerDestroyHook } = async_wrap; 81const { enqueueMicrotask } = internalBinding('task_queue'); 82const { resource_symbol, owner_symbol } = internalBinding('symbols'); 83 84// Each constant tracks how many callbacks there are for any given step of 85// async execution. These are tracked so if the user didn't include callbacks 86// for a given step, that step can bail out early. 87const { 88 kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve, 89 kCheck, kExecutionAsyncId, kAsyncIdCounter, kTriggerAsyncId, 90 kDefaultTriggerAsyncId, kStackLength, kUsesExecutionAsyncResource, 91} = async_wrap.constants; 92 93const { async_id_symbol, 94 trigger_async_id_symbol } = internalBinding('symbols'); 95 96// Lazy load of internal/util/inspect; 97let inspect; 98 99// Used in AsyncHook and AsyncResource. 100const init_symbol = Symbol('init'); 101const before_symbol = Symbol('before'); 102const after_symbol = Symbol('after'); 103const destroy_symbol = Symbol('destroy'); 104const promise_resolve_symbol = Symbol('promiseResolve'); 105const emitBeforeNative = emitHookFactory(before_symbol, 'emitBeforeNative'); 106const emitAfterNative = emitHookFactory(after_symbol, 'emitAfterNative'); 107const emitDestroyNative = emitHookFactory(destroy_symbol, 'emitDestroyNative'); 108const emitPromiseResolveNative = 109 emitHookFactory(promise_resolve_symbol, 'emitPromiseResolveNative'); 110 111let domain_cb; 112function useDomainTrampoline(fn) { 113 domain_cb = fn; 114} 115 116function callbackTrampoline(asyncId, resource, cb, ...args) { 117 const index = async_hook_fields[kStackLength] - 1; 118 execution_async_resources[index] = resource; 119 120 if (asyncId !== 0 && hasHooks(kBefore)) 121 emitBeforeNative(asyncId); 122 123 let result; 124 if (asyncId === 0 && typeof domain_cb === 'function') { 125 args.unshift(cb); 126 result = domain_cb.apply(this, args); 127 } else { 128 result = cb.apply(this, args); 129 } 130 131 if (asyncId !== 0 && hasHooks(kAfter)) 132 emitAfterNative(asyncId); 133 134 execution_async_resources.pop(); 135 return result; 136} 137 138const topLevelResource = {}; 139 140function executionAsyncResource() { 141 // Indicate to the native layer that this function is likely to be used, 142 // in which case it will inform JS about the current async resource via 143 // the trampoline above. 144 async_hook_fields[kUsesExecutionAsyncResource] = 1; 145 146 const index = async_hook_fields[kStackLength] - 1; 147 if (index === -1) return topLevelResource; 148 const resource = execution_async_resources[index] || 149 executionAsyncResource_(index); 150 return lookupPublicResource(resource); 151} 152 153function inspectExceptionValue(e) { 154 inspect ??= require('internal/util/inspect').inspect; 155 return { message: inspect(e) }; 156} 157 158// Used to fatally abort the process if a callback throws. 159function fatalError(e) { 160 if (typeof e?.stack === 'string') { 161 process._rawDebug(e.stack); 162 } else { 163 const o = inspectExceptionValue(e); 164 ErrorCaptureStackTrace(o, fatalError); 165 process._rawDebug(o.stack); 166 } 167 168 const { getOptionValue } = require('internal/options'); 169 if (getOptionValue('--abort-on-uncaught-exception')) { 170 process.abort(); 171 } 172 process.exit(1); 173} 174 175function lookupPublicResource(resource) { 176 if (typeof resource !== 'object' || resource === null) return resource; 177 // TODO(addaleax): Merge this with owner_symbol and use it across all 178 // AsyncWrap instances. 179 const publicResource = resource[resource_symbol]; 180 if (publicResource !== undefined) 181 return publicResource; 182 return resource; 183} 184 185// Emit From Native // 186 187// Used by C++ to call all init() callbacks. Because some state can be setup 188// from C++ there's no need to perform all the same operations as in 189// emitInitScript. 190function emitInitNative(asyncId, type, triggerAsyncId, resource) { 191 active_hooks.call_depth += 1; 192 resource = lookupPublicResource(resource); 193 // Use a single try/catch for all hooks to avoid setting up one per iteration. 194 try { 195 // Using var here instead of let because "for (var ...)" is faster than let. 196 // Refs: https://github.com/nodejs/node/pull/30380#issuecomment-552948364 197 // eslint-disable-next-line no-var 198 for (var i = 0; i < active_hooks.array.length; i++) { 199 if (typeof active_hooks.array[i][init_symbol] === 'function') { 200 active_hooks.array[i][init_symbol]( 201 asyncId, type, triggerAsyncId, 202 resource, 203 ); 204 } 205 } 206 } catch (e) { 207 fatalError(e); 208 } finally { 209 active_hooks.call_depth -= 1; 210 } 211 212 // Hooks can only be restored if there have been no recursive hook calls. 213 // Also the active hooks do not need to be restored if enable()/disable() 214 // weren't called during hook execution, in which case active_hooks.tmp_array 215 // will be null. 216 if (active_hooks.call_depth === 0 && active_hooks.tmp_array !== null) { 217 restoreActiveHooks(); 218 } 219} 220 221// Called from native. The asyncId stack handling is taken care of there 222// before this is called. 223function emitHook(symbol, asyncId) { 224 active_hooks.call_depth += 1; 225 // Use a single try/catch for all hook to avoid setting up one per 226 // iteration. 227 try { 228 // Using var here instead of let because "for (var ...)" is faster than let. 229 // Refs: https://github.com/nodejs/node/pull/30380#issuecomment-552948364 230 // eslint-disable-next-line no-var 231 for (var i = 0; i < active_hooks.array.length; i++) { 232 if (typeof active_hooks.array[i][symbol] === 'function') { 233 active_hooks.array[i][symbol](asyncId); 234 } 235 } 236 } catch (e) { 237 fatalError(e); 238 } finally { 239 active_hooks.call_depth -= 1; 240 } 241 242 // Hooks can only be restored if there have been no recursive hook calls. 243 // Also the active hooks do not need to be restored if enable()/disable() 244 // weren't called during hook execution, in which case 245 // active_hooks.tmp_array will be null. 246 if (active_hooks.call_depth === 0 && active_hooks.tmp_array !== null) { 247 restoreActiveHooks(); 248 } 249} 250 251function emitHookFactory(symbol, name) { 252 const fn = emitHook.bind(undefined, symbol); 253 254 // Set the name property of the function as it looks good in the stack trace. 255 ObjectDefineProperty(fn, 'name', { 256 __proto__: null, 257 value: name, 258 }); 259 return fn; 260} 261 262// Manage Active Hooks // 263 264function getHookArrays() { 265 if (active_hooks.call_depth === 0) 266 return [active_hooks.array, async_hook_fields]; 267 // If this hook is being enabled while in the middle of processing the array 268 // of currently active hooks then duplicate the current set of active hooks 269 // and store this there. This shouldn't fire until the next time hooks are 270 // processed. 271 if (active_hooks.tmp_array === null) 272 storeActiveHooks(); 273 return [active_hooks.tmp_array, active_hooks.tmp_fields]; 274} 275 276 277function storeActiveHooks() { 278 active_hooks.tmp_array = ArrayPrototypeSlice(active_hooks.array); 279 // Don't want to make the assumption that kInit to kDestroy are indexes 0 to 280 // 4. So do this the long way. 281 active_hooks.tmp_fields = []; 282 copyHooks(active_hooks.tmp_fields, async_hook_fields); 283} 284 285function copyHooks(destination, source) { 286 destination[kInit] = source[kInit]; 287 destination[kBefore] = source[kBefore]; 288 destination[kAfter] = source[kAfter]; 289 destination[kDestroy] = source[kDestroy]; 290 destination[kPromiseResolve] = source[kPromiseResolve]; 291} 292 293 294// Then restore the correct hooks array in case any hooks were added/removed 295// during hook callback execution. 296function restoreActiveHooks() { 297 active_hooks.array = active_hooks.tmp_array; 298 copyHooks(async_hook_fields, active_hooks.tmp_fields); 299 300 active_hooks.tmp_array = null; 301 active_hooks.tmp_fields = null; 302} 303 304function trackPromise(promise, parent) { 305 if (promise[async_id_symbol]) { 306 return; 307 } 308 309 // Get trigger id from parent async id before making the async id of the 310 // child so if a new one must be made it will be lower than the child. 311 const triggerAsyncId = parent ? getOrSetAsyncId(parent) : 312 getDefaultTriggerAsyncId(); 313 314 promise[async_id_symbol] = newAsyncId(); 315 promise[trigger_async_id_symbol] = triggerAsyncId; 316} 317 318function promiseInitHook(promise, parent) { 319 trackPromise(promise, parent); 320 const asyncId = promise[async_id_symbol]; 321 const triggerAsyncId = promise[trigger_async_id_symbol]; 322 emitInitScript(asyncId, 'PROMISE', triggerAsyncId, promise); 323} 324 325function promiseInitHookWithDestroyTracking(promise, parent) { 326 promiseInitHook(promise, parent); 327 destroyTracking(promise, parent); 328} 329 330function destroyTracking(promise, parent) { 331 trackPromise(promise, parent); 332 const asyncId = promise[async_id_symbol]; 333 registerDestroyHook(promise, asyncId); 334} 335 336function promiseBeforeHook(promise) { 337 trackPromise(promise); 338 const asyncId = promise[async_id_symbol]; 339 const triggerId = promise[trigger_async_id_symbol]; 340 emitBeforeScript(asyncId, triggerId, promise); 341} 342 343function promiseAfterHook(promise) { 344 trackPromise(promise); 345 const asyncId = promise[async_id_symbol]; 346 if (hasHooks(kAfter)) { 347 emitAfterNative(asyncId); 348 } 349 if (asyncId === executionAsyncId()) { 350 // This condition might not be true if async_hooks was enabled during 351 // the promise callback execution. 352 // Popping it off the stack can be skipped in that case, because it is 353 // known that it would correspond to exactly one call with 354 // PromiseHookType::kBefore that was not witnessed by the PromiseHook. 355 popAsyncContext(asyncId); 356 } 357} 358 359function promiseResolveHook(promise) { 360 trackPromise(promise); 361 const asyncId = promise[async_id_symbol]; 362 emitPromiseResolveNative(asyncId); 363} 364 365let wantPromiseHook = false; 366function enableHooks() { 367 async_hook_fields[kCheck] += 1; 368 369 setCallbackTrampoline(callbackTrampoline); 370} 371 372let stopPromiseHook; 373function updatePromiseHookMode() { 374 wantPromiseHook = true; 375 let initHook; 376 if (initHooksExist()) { 377 initHook = destroyHooksExist() ? promiseInitHookWithDestroyTracking : 378 promiseInitHook; 379 } else if (destroyHooksExist()) { 380 initHook = destroyTracking; 381 } 382 if (stopPromiseHook) stopPromiseHook(); 383 const promiseHooks = require('internal/promise_hooks'); 384 stopPromiseHook = promiseHooks.createHook({ 385 init: initHook, 386 before: promiseBeforeHook, 387 after: promiseAfterHook, 388 settled: promiseResolveHooksExist() ? promiseResolveHook : undefined, 389 }); 390} 391 392function disableHooks() { 393 async_hook_fields[kCheck] -= 1; 394 395 wantPromiseHook = false; 396 397 setCallbackTrampoline(); 398 399 // Delay the call to `disablePromiseHook()` because we might currently be 400 // between the `before` and `after` calls of a Promise. 401 enqueueMicrotask(disablePromiseHookIfNecessary); 402} 403 404function disablePromiseHookIfNecessary() { 405 if (!wantPromiseHook && stopPromiseHook) { 406 stopPromiseHook(); 407 } 408} 409 410// Internal Embedder API // 411 412// Increment the internal id counter and return the value. Important that the 413// counter increment first. Since it's done the same way in 414// Environment::new_async_uid() 415function newAsyncId() { 416 return ++async_id_fields[kAsyncIdCounter]; 417} 418 419function getOrSetAsyncId(object) { 420 if (ObjectPrototypeHasOwnProperty(object, async_id_symbol)) { 421 return object[async_id_symbol]; 422 } 423 424 return object[async_id_symbol] = newAsyncId(); 425} 426 427 428// Return the triggerAsyncId meant for the constructor calling it. It's up to 429// the user to safeguard this call and make sure it's zero'd out when the 430// constructor is complete. 431function getDefaultTriggerAsyncId() { 432 const defaultTriggerAsyncId = async_id_fields[kDefaultTriggerAsyncId]; 433 // If defaultTriggerAsyncId isn't set, use the executionAsyncId 434 if (defaultTriggerAsyncId < 0) 435 return async_id_fields[kExecutionAsyncId]; 436 return defaultTriggerAsyncId; 437} 438 439 440function clearDefaultTriggerAsyncId() { 441 async_id_fields[kDefaultTriggerAsyncId] = -1; 442} 443 444/** 445 * Sets a default top level trigger ID to be used 446 * @template {Array<unknown>} T 447 * @template {unknown} R 448 * @param {number} triggerAsyncId 449 * @param { (...T: args) => R } block 450 * @param {T} args 451 * @returns {R} 452 */ 453function defaultTriggerAsyncIdScope(triggerAsyncId, block, ...args) { 454 if (triggerAsyncId === undefined) 455 return block.apply(null, args); 456 // CHECK(NumberIsSafeInteger(triggerAsyncId)) 457 // CHECK(triggerAsyncId > 0) 458 const oldDefaultTriggerAsyncId = async_id_fields[kDefaultTriggerAsyncId]; 459 async_id_fields[kDefaultTriggerAsyncId] = triggerAsyncId; 460 461 try { 462 return block.apply(null, args); 463 } finally { 464 async_id_fields[kDefaultTriggerAsyncId] = oldDefaultTriggerAsyncId; 465 } 466} 467 468function hasHooks(key) { 469 return async_hook_fields[key] > 0; 470} 471 472function enabledHooksExist() { 473 return hasHooks(kCheck); 474} 475 476function initHooksExist() { 477 return hasHooks(kInit); 478} 479 480function afterHooksExist() { 481 return hasHooks(kAfter); 482} 483 484function destroyHooksExist() { 485 return hasHooks(kDestroy); 486} 487 488function promiseResolveHooksExist() { 489 return hasHooks(kPromiseResolve); 490} 491 492 493function emitInitScript(asyncId, type, triggerAsyncId, resource) { 494 // Short circuit all checks for the common case. Which is that no hooks have 495 // been set. Do this to remove performance impact for embedders (and core). 496 if (!hasHooks(kInit)) 497 return; 498 499 if (triggerAsyncId === null) { 500 triggerAsyncId = getDefaultTriggerAsyncId(); 501 } 502 503 emitInitNative(asyncId, type, triggerAsyncId, resource); 504} 505 506 507function emitBeforeScript(asyncId, triggerAsyncId, resource) { 508 pushAsyncContext(asyncId, triggerAsyncId, resource); 509 510 if (hasHooks(kBefore)) 511 emitBeforeNative(asyncId); 512} 513 514 515function emitAfterScript(asyncId) { 516 if (hasHooks(kAfter)) 517 emitAfterNative(asyncId); 518 519 popAsyncContext(asyncId); 520} 521 522 523function emitDestroyScript(asyncId) { 524 // Return early if there are no destroy callbacks, or invalid asyncId. 525 if (!hasHooks(kDestroy) || asyncId <= 0) 526 return; 527 async_wrap.queueDestroyAsyncId(asyncId); 528} 529 530 531function hasAsyncIdStack() { 532 return hasHooks(kStackLength); 533} 534 535 536// This is the equivalent of the native push_async_ids() call. 537function pushAsyncContext(asyncId, triggerAsyncId, resource) { 538 const offset = async_hook_fields[kStackLength]; 539 execution_async_resources[offset] = resource; 540 if (offset * 2 >= async_wrap.async_ids_stack.length) 541 return pushAsyncContext_(asyncId, triggerAsyncId); 542 async_wrap.async_ids_stack[offset * 2] = async_id_fields[kExecutionAsyncId]; 543 async_wrap.async_ids_stack[offset * 2 + 1] = async_id_fields[kTriggerAsyncId]; 544 async_hook_fields[kStackLength]++; 545 async_id_fields[kExecutionAsyncId] = asyncId; 546 async_id_fields[kTriggerAsyncId] = triggerAsyncId; 547} 548 549 550// This is the equivalent of the native pop_async_ids() call. 551function popAsyncContext(asyncId) { 552 const stackLength = async_hook_fields[kStackLength]; 553 if (stackLength === 0) return false; 554 555 if (enabledHooksExist() && async_id_fields[kExecutionAsyncId] !== asyncId) { 556 // Do the same thing as the native code (i.e. crash hard). 557 return popAsyncContext_(asyncId); 558 } 559 560 const offset = stackLength - 1; 561 async_id_fields[kExecutionAsyncId] = async_wrap.async_ids_stack[2 * offset]; 562 async_id_fields[kTriggerAsyncId] = async_wrap.async_ids_stack[2 * offset + 1]; 563 execution_async_resources.pop(); 564 async_hook_fields[kStackLength] = offset; 565 return offset > 0; 566} 567 568 569function executionAsyncId() { 570 return async_id_fields[kExecutionAsyncId]; 571} 572 573function triggerAsyncId() { 574 return async_id_fields[kTriggerAsyncId]; 575} 576 577 578module.exports = { 579 executionAsyncId, 580 triggerAsyncId, 581 // Private API 582 getHookArrays, 583 symbols: { 584 async_id_symbol, trigger_async_id_symbol, 585 init_symbol, before_symbol, after_symbol, destroy_symbol, 586 promise_resolve_symbol, owner_symbol, 587 }, 588 constants: { 589 kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve, 590 }, 591 enableHooks, 592 disableHooks, 593 updatePromiseHookMode, 594 clearDefaultTriggerAsyncId, 595 clearAsyncIdStack, 596 hasAsyncIdStack, 597 executionAsyncResource, 598 // Internal Embedder API 599 newAsyncId, 600 getOrSetAsyncId, 601 getDefaultTriggerAsyncId, 602 defaultTriggerAsyncIdScope, 603 enabledHooksExist, 604 initHooksExist, 605 afterHooksExist, 606 destroyHooksExist, 607 emitInit: emitInitScript, 608 emitBefore: emitBeforeScript, 609 emitAfter: emitAfterScript, 610 emitDestroy: emitDestroyScript, 611 pushAsyncContext, 612 popAsyncContext, 613 registerDestroyHook, 614 useDomainTrampoline, 615 nativeHooks: { 616 init: emitInitNative, 617 before: emitBeforeNative, 618 after: emitAfterNative, 619 destroy: emitDestroyNative, 620 promise_resolve: emitPromiseResolveNative, 621 }, 622 asyncWrap: { 623 Providers: async_wrap.Providers, 624 }, 625}; 626