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