diff --git a/doc/api/async_hooks.md b/doc/api/async_hooks.md index 90ff343ae0817c..a42965a28ce853 100644 --- a/doc/api/async_hooks.md +++ b/doc/api/async_hooks.md @@ -107,7 +107,7 @@ specifics of all functions that can be passed to `callbacks` is in the const async_hooks = require('async_hooks'); const asyncHook = async_hooks.createHook({ - init(asyncId, type, triggerAsyncId, resource) { }, + init(asyncId, type, triggerAsyncId, resource, bootstrap) { }, destroy(asyncId) { } }); ``` @@ -116,7 +116,7 @@ The callbacks will be inherited via the prototype chain: ```js class MyAsyncCallbacks { - init(asyncId, type, triggerAsyncId, resource) { } + init(asyncId, type, triggerAsyncId, resource, bootstrap) { } destroy(asyncId) {} } @@ -203,7 +203,7 @@ Key events in the lifetime of asynchronous events have been categorized into four areas: instantiation, before/after the callback is called, and when the instance is destroyed. -##### init(asyncId, type, triggerAsyncId, resource) +##### init(asyncId, type, triggerAsyncId, resource, bootstrap) * `asyncId` {number} A unique ID for the async resource. * `type` {string} The type of the async resource. @@ -211,6 +211,8 @@ instance is destroyed. execution context this async resource was created. * `resource` {Object} Reference to the resource representing the async operation, needs to be released during _destroy_. +* `bootstrap` {boolean} Indicates whether this event was created during Node.js + bootstrap. Called when a class is constructed that has the _possibility_ to emit an asynchronous event. This _does not_ mean the instance must call @@ -319,8 +321,13 @@ elaborate to make calling context easier to see. ```js let indent = 0; +const bootstrapIds = new Set(); async_hooks.createHook({ - init(asyncId, type, triggerAsyncId) { + init(asyncId, type, triggerAsyncId, resource, bootstrap) { + if (bootstrap) { + bootstrapIds.add(asyncId); + return; + } const eid = async_hooks.executionAsyncId(); const indentStr = ' '.repeat(indent); fs.writeSync( @@ -329,18 +336,21 @@ async_hooks.createHook({ ` trigger: ${triggerAsyncId} execution: ${eid}\n`); }, before(asyncId) { + if (bootstrapIds.has(asyncId)) return; const indentStr = ' '.repeat(indent); fs.writeFileSync('log.out', `${indentStr}before: ${asyncId}\n`, { flag: 'a' }); indent += 2; }, after(asyncId) { + if (bootstrapIds.has(asyncId)) return; indent -= 2; const indentStr = ' '.repeat(indent); fs.writeFileSync('log.out', `${indentStr}after: ${asyncId}\n`, { flag: 'a' }); }, destroy(asyncId) { + if (bootstrapIds.has(asyncId)) return; const indentStr = ' '.repeat(indent); fs.writeFileSync('log.out', `${indentStr}destroy: ${asyncId}\n`, { flag: 'a' }); @@ -685,7 +695,7 @@ never be called. [`after` callback]: #async_hooks_after_asyncid [`before` callback]: #async_hooks_before_asyncid [`destroy` callback]: #async_hooks_destroy_asyncid -[`init` callback]: #async_hooks_init_asyncid_type_triggerasyncid_resource +[`init` callback]: #async_hooks_init_asyncid_type_triggerasyncid_resource_bootstrap [`promiseResolve` callback]: #async_hooks_promiseresolve_asyncid [Hook Callbacks]: #async_hooks_hook_callbacks [PromiseHooks]: https://docs.google.com/document/d/1rda3yKGHimKIhg5YeoAmCOtyURgsbTH_qaYR79FELlk/edit diff --git a/doc/api/n-api.md b/doc/api/n-api.md index d0bd46e790f737..e30e005a3db879 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -5152,7 +5152,7 @@ This API may only be called from the main thread. [Working with JavaScript Properties]: #n_api_working_with_javascript_properties [Working with JavaScript Values - Abstract Operations]: #n_api_working_with_javascript_values_abstract_operations [Working with JavaScript Values]: #n_api_working_with_javascript_values -[`init` hooks]: async_hooks.html#async_hooks_init_asyncid_type_triggerasyncid_resource +[`init` hooks]: async_hooks.html#async_hooks_init_asyncid_type_triggerasyncid_resource_bootstrap [`napi_add_finalizer`]: #n_api_napi_add_finalizer [`napi_async_init`]: #n_api_napi_async_init [`napi_cancel_async_work`]: #n_api_napi_cancel_async_work diff --git a/lib/async_hooks.js b/lib/async_hooks.js index 0c177055364e41..0b73eb1f0530db 100644 --- a/lib/async_hooks.js +++ b/lib/async_hooks.js @@ -28,6 +28,7 @@ const { emitAfter, emitDestroy, initHooksExist, + emitBootstrapHooksBuffer } = internal_async_hooks; // Get symbols @@ -92,6 +93,10 @@ class AsyncHook { enableHooks(); } + // If there are unemitted bootstrap hooks, emit them now. + // This only applies during sync execution of user code after bootsrap. + emitBootstrapHooksBuffer(); + return this; } diff --git a/lib/internal/async_hooks.js b/lib/internal/async_hooks.js index bf803885cacfa3..927797ad82bca6 100644 --- a/lib/internal/async_hooks.js +++ b/lib/internal/async_hooks.js @@ -125,7 +125,8 @@ function validateAsyncId(asyncId, type) { // 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) { +function emitInitNative(asyncId, type, triggerAsyncId, resource, + bootstrap = false) { active_hooks.call_depth += 1; // Use a single try/catch for all hooks to avoid setting up one per iteration. try { @@ -133,7 +134,7 @@ function emitInitNative(asyncId, type, triggerAsyncId, resource) { if (typeof active_hooks.array[i][init_symbol] === 'function') { active_hooks.array[i][init_symbol]( asyncId, type, triggerAsyncId, - resource + resource, bootstrap ); } } @@ -231,6 +232,114 @@ function restoreActiveHooks() { active_hooks.tmp_fields = null; } +// Bootstrap hooks are buffered to emit to userland listeners +let bootstrapHooks, bootstrapBuffer; +function bufferBootstrapHooks() { + const { getOptionValue } = require('internal/options'); + if (getOptionValue('--no-force-async-hooks-checks')) { + return; + } + async_hook_fields[kInit]++; + async_hook_fields[kBefore]++; + async_hook_fields[kAfter]++; + async_hook_fields[kDestroy]++; + async_hook_fields[kPromiseResolve]++; + async_hook_fields[kTotals] += 5; + bootstrapHooks = { + [init_symbol]: (asyncId, type, triggerAsyncId, resource) => { + bootstrapBuffer.push({ + type: kInit, + asyncId, + args: [type, triggerAsyncId, resource, true] + }); + }, + [before_symbol]: (asyncId) => { + bootstrapBuffer.push({ + type: kBefore, + asyncId, + args: null + }); + }, + [after_symbol]: (asyncId) => { + bootstrapBuffer.push({ + type: kAfter, + asyncId, + args: null + }); + }, + [destroy_symbol]: (asyncId) => { + bootstrapBuffer.push({ + type: kDestroy, + asyncId, + args: null + }); + }, + [promise_resolve_symbol]: (asyncId) => { + bootstrapBuffer.push({ + type: kPromiseResolve, + asyncId, + args: null + }); + } + }; + bootstrapBuffer = []; + active_hooks.array.push(bootstrapHooks); + if (async_hook_fields[kTotals] === 5) { + enableHooks(); + } +} + +function clearBootstrapHooksBuffer() { + if (!bootstrapBuffer) + return; + if (bootstrapHooks) { + stopBootstrapHooksBuffer(); + } + const _bootstrapBuffer = bootstrapBuffer; + bootstrapBuffer = null; + return _bootstrapBuffer; +} + +function stopBootstrapHooksBuffer() { + if (!bootstrapHooks) + return; + async_hook_fields[kInit]--; + async_hook_fields[kBefore]--; + async_hook_fields[kAfter]--; + async_hook_fields[kDestroy]--; + async_hook_fields[kPromiseResolve]--; + async_hook_fields[kTotals] -= 5; + active_hooks.array.splice(active_hooks.array.indexOf(bootstrapHooks), 1); + bootstrapHooks = null; + if (async_hook_fields[kTotals] === 0) { + disableHooks(); + // Ensure disable happens immediately and synchronously. + process._tickCallback(); + } +} + +function emitBootstrapHooksBuffer() { + const bootstrapBuffer = clearBootstrapHooksBuffer(); + if (!bootstrapBuffer || async_hook_fields[kTotals] === 0) { + return; + } + for (const { type, asyncId, args } of bootstrapBuffer) { + switch (type) { + case kInit: + emitInitNative(asyncId, ...args); + break; + case kBefore: + emitBeforeNative(asyncId); + break; + case kAfter: + emitAfterNative(asyncId); + break; + case kDestroy: + emitDestroyNative(asyncId); + break; + } + } +} let wantPromiseHook = false; function enableHooks() { @@ -318,7 +427,8 @@ function destroyHooksExist() { } -function emitInitScript(asyncId, type, triggerAsyncId, resource) { +function emitInitScript(asyncId, type, triggerAsyncId, resource, + bootstrap = false) { validateAsyncId(asyncId, 'asyncId'); if (triggerAsyncId !== null) validateAsyncId(triggerAsyncId, 'triggerAsyncId'); @@ -338,7 +448,7 @@ function emitInitScript(asyncId, type, triggerAsyncId, resource) { triggerAsyncId = getDefaultTriggerAsyncId(); } - emitInitNative(asyncId, type, triggerAsyncId, resource); + emitInitNative(asyncId, type, triggerAsyncId, resource, bootstrap); } @@ -468,5 +578,9 @@ module.exports = { after: emitAfterNative, destroy: emitDestroyNative, promise_resolve: emitPromiseResolveNative - } + }, + bufferBootstrapHooks, + clearBootstrapHooksBuffer, + emitBootstrapHooksBuffer, + stopBootstrapHooksBuffer }; diff --git a/lib/internal/bootstrap/pre_execution.js b/lib/internal/bootstrap/pre_execution.js index c1636d87f42814..13c5619f677534 100644 --- a/lib/internal/bootstrap/pre_execution.js +++ b/lib/internal/bootstrap/pre_execution.js @@ -5,6 +5,7 @@ const { Object, SafeWeakMap } = primordials; const { getOptionValue } = require('internal/options'); const { Buffer } = require('buffer'); const { ERR_MANIFEST_ASSERT_INTEGRITY } = require('internal/errors').codes; +const { bufferBootstrapHooks } = require('internal/async_hooks'); function prepareMainThreadExecution(expandArgv1 = false) { // Patch the process object with legacy properties and normalizations @@ -32,6 +33,10 @@ function prepareMainThreadExecution(expandArgv1 = false) { setupDebugEnv(); + // Buffer all async hooks emitted by core bootstrap + // to allow user listeners to attach to these + bufferBootstrapHooks(); + // Only main thread receives signals. setupSignalHandlers(); diff --git a/lib/internal/main/run_main_module.js b/lib/internal/main/run_main_module.js index 2cad569dcce9fd..101a38119906f6 100644 --- a/lib/internal/main/run_main_module.js +++ b/lib/internal/main/run_main_module.js @@ -14,4 +14,4 @@ markBootstrapComplete(); // --experimental-modules is on. // TODO(joyeecheung): can we move that logic to here? Note that this // is an undocumented method available via `require('module').runMain` -CJSModule.runMain(); +CJSModule.runMain(process.argv[1]); diff --git a/lib/internal/main/worker_thread.js b/lib/internal/main/worker_thread.js index 50a19be77dd78d..516ea98201b01f 100644 --- a/lib/internal/main/worker_thread.js +++ b/lib/internal/main/worker_thread.js @@ -140,8 +140,8 @@ port.on('message', (message) => { const { evalScript } = require('internal/process/execution'); evalScript('[worker eval]', filename); } else { - process.argv[1] = filename; // script filename - require('module').runMain(); + // script filename + require('module').runMain(process.argv[1] = filename); } } else if (message.type === STDIO_PAYLOAD) { const { stream, chunk, encoding } = message; diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 479044a26aaba9..d10a3fcf895331 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -62,7 +62,7 @@ const manifest = getOptionValue('--experimental-policy') ? require('internal/process/policy').manifest : null; const { compileFunction } = internalBinding('contextify'); - +const { clearBootstrapHooksBuffer } = require('internal/async_hooks'); const { ERR_INVALID_ARG_VALUE, ERR_INVALID_OPT_VALUE, @@ -821,10 +821,12 @@ Module.prototype.load = function(filename) { if (module !== undefined && module.module !== undefined) { if (module.module.getStatus() >= kInstantiated) module.module.setExport('default', exports); - } else { // preemptively cache + } else { + // Preemptively cache + // We use a function to defer promise creation for async hooks. ESMLoader.moduleMap.set( url, - new ModuleJob(ESMLoader, url, () => + () => new ModuleJob(ESMLoader, url, () => new ModuleWrap(function() { this.setExport('default', exports); }, ['default'], url) @@ -1007,12 +1009,11 @@ Module._extensions['.mjs'] = function(module, filename) { throw new ERR_REQUIRE_ESM(filename); }; -// Bootstrap main module. -Module.runMain = function() { +Module.runMain = function(mainPath) { // Load the main module--the command line argument. if (experimentalModules) { asyncESM.loaderPromise.then((loader) => { - return loader.import(pathToFileURL(process.argv[1]).href); + return loader.import(pathToFileURL(mainPath).href); }) .catch((e) => { internalBinding('errors').triggerUncaughtException( @@ -1020,9 +1021,10 @@ Module.runMain = function() { true /* fromPromise */ ); }); - return; + } else { + clearBootstrapHooksBuffer(); + Module._load(mainPath, null, true); } - Module._load(process.argv[1], null, true); }; function createRequireFromPath(filename) { diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 138cf8b5ecc3ed..a58b1ecf19ffb8 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -146,6 +146,9 @@ class Loader { async getModuleJob(specifier, parentURL) { const { url, format } = await this.resolve(specifier, parentURL); let job = this.moduleMap.get(url); + // CJS injects jobs as functions to defer promise creation for async hooks. + if (typeof job === 'function') + this.moduleMap.set(url, job = job()); if (job !== undefined) return job; diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index ef11e2ec833b89..1bdb4c0d28f2c9 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -11,6 +11,8 @@ const { ModuleWrap } = internalBinding('module_wrap'); const { decorateErrorStack } = require('internal/util'); const { getOptionValue } = require('internal/options'); const assert = require('internal/assert'); +const { clearBootstrapHooksBuffer, stopBootstrapHooksBuffer } = + require('internal/async_hooks'); const resolvedPromise = SafePromise.resolve(); function noop() {} @@ -106,7 +108,15 @@ class ModuleJob { const module = await this.instantiate(); const timeout = -1; const breakOnSigint = false; - return { module, result: module.evaluate(timeout, breakOnSigint) }; + if (this.isMain) + stopBootstrapHooksBuffer(); + const output = { + module, + result: module.evaluate(timeout, breakOnSigint) + }; + if (this.isMain) + clearBootstrapHooksBuffer(); + return output; } } Object.setPrototypeOf(ModuleJob.prototype, null); diff --git a/lib/internal/modules/esm/module_map.js b/lib/internal/modules/esm/module_map.js index 01521fb7885ee1..4fe2a6f36913bc 100644 --- a/lib/internal/modules/esm/module_map.js +++ b/lib/internal/modules/esm/module_map.js @@ -16,7 +16,7 @@ class ModuleMap extends SafeMap { } set(url, job) { validateString(url, 'url'); - if (job instanceof ModuleJob !== true) { + if (job instanceof ModuleJob !== true && typeof job !== 'function') { throw new ERR_INVALID_ARG_TYPE('job', 'ModuleJob', job); } debug(`Storing ${url} in ModuleMap`); diff --git a/test/es-module/test-esm-async-hooks.mjs b/test/es-module/test-esm-async-hooks.mjs new file mode 100644 index 00000000000000..af1cfa73f141cf --- /dev/null +++ b/test/es-module/test-esm-async-hooks.mjs @@ -0,0 +1,50 @@ +// Flags: --experimental-modules +import '../common/index.mjs'; +import assert from 'assert'; +import { createHook } from 'async_hooks'; + +const bootstrapCalls = {}; +const calls = {}; + +const asyncHook = createHook({ init, before, after, destroy, promiseResolve }); + +// Bootstrap hooks for modules pipeline will trigger sync with this enable +// call. +asyncHook.enable(); + +function init(asyncId, type, asyncTriggerId, resource, bootstrap) { + (bootstrap ? bootstrapCalls : calls)[asyncId] = { + init: true + }; +} + +function before(asyncId) { + (bootstrapCalls[asyncId] || calls[asyncId]).before = true; +} + +function after(asyncId) { + (bootstrapCalls[asyncId] || calls[asyncId]).after = true; +} + +function destroy(asyncId) { + (bootstrapCalls[asyncId] || calls[asyncId]).destroy = true; +} + +function promiseResolve(asyncId) { + (bootstrapCalls[asyncId] || calls[asyncId]).promiseResolve = true; +} + +// Ensure all hooks have inits +assert(Object.values(bootstrapCalls).every(({ init }) => init === true)); +assert(Object.values(calls).every(({ init }) => init === true)); + +// Ensure we have at least one of each type of callback +assert(Object.values(bootstrapCalls).some(({ before }) => before === true)); +assert(Object.values(bootstrapCalls).some(({ after }) => after === true)); +assert(Object.values(bootstrapCalls).some(({ destroy }) => destroy === true)); + +// Wait a tick to see that we have promise resolution +setTimeout(() => { + assert(Object.values(bootstrapCalls).some(({ promiseResolve }) => + promiseResolve === true)); +}, 10); diff --git a/test/parallel/test-internal-module-map-asserts.js b/test/parallel/test-internal-module-map-asserts.js index 4563fc605e0792..614da43aba0acb 100644 --- a/test/parallel/test-internal-module-map-asserts.js +++ b/test/parallel/test-internal-module-map-asserts.js @@ -12,7 +12,7 @@ const ModuleMap = require('internal/modules/esm/module_map'); code: 'ERR_INVALID_ARG_TYPE', type: TypeError, message: /^The "url" argument must be of type string/ - }, 15); + }, 12); const moduleMap = new ModuleMap(); @@ -21,7 +21,7 @@ const ModuleMap = require('internal/modules/esm/module_map'); // but I think it's useless, and was not simple to mock... const job = undefined; - [{}, [], true, 1, () => {}].forEach((value) => { + [{}, [], true, 1].forEach((value) => { assert.throws(() => moduleMap.get(value), errorReg); assert.throws(() => moduleMap.has(value), errorReg); assert.throws(() => moduleMap.set(value, job), errorReg); @@ -34,11 +34,11 @@ const ModuleMap = require('internal/modules/esm/module_map'); code: 'ERR_INVALID_ARG_TYPE', type: TypeError, message: /^The "job" argument must be of type ModuleJob/ - }, 5); + }, 4); const moduleMap = new ModuleMap(); - [{}, [], true, 1, () => {}].forEach((value) => { + [{}, [], true, 1].forEach((value) => { assert.throws(() => moduleMap.set('', value), errorReg); }); }