node/lib/internal/main/worker_thread.js
Joyee Cheung b19525a33c
module: refactor and clarify async loader hook customizations
- This updates the comments that assume loader hooks must be async
- Differentiate the sync/async loader hook paths in naming
  `#customizations` is now `#asyncLoaderHooks` to make it clear
  it's from the async APIs.
- Differentiate the paths running on the loader hook thread
  (affects the loading of async other loader hooks and are async)
  v.s. paths on the main thread calling out to code on the loader
  hook thread (do not handle loading of other async loader hooks, and
  can be sync by blocking).
  - `Hooks` is now `AsyncLoaderHooksOnLoaderHookWorker`
  - `CustomizedModuleLoader` is now
    `AsyncLoaderHooksProxiedToLoaderHookWorker` and moved into
    `lib/internal/modules/esm/hooks.js` as it implements the same
    interface as `AsyncLoaderHooksOnLoaderHookWorker`
  - `HooksProxy` is now `AsyncLoaderHookWorker`
  - Adjust the JSDoc accordingly
- Clarify the "loader worker" as the "async loader hook worker"
  i.e. when there's no _async_ loader hook registered, there won't
  be this worker, to avoid the misconception that this worker
  is spawned unconditionally.
- The code run on the loader hook worker to process
  `--experimental-loader` is moved into
  `lib/internal/modules/esm/worker.js` for clarity.
- The initialization configuration `forceDefaultLoader` is split
  into `shouldSpawnLoaderHookWorker` and `shouldPreloadModules`
  as those can be separate.
- `--experimental-vm-modules` is now processed during pre-execution
  and no longer part of the initialization of the built-in ESM
  loader, as it only exposes the vm APIs of ESM, and is unrelated
  to built-in ESM loading.

PR-URL: https://github.com/nodejs/node/pull/60278
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
2025-10-23 13:42:23 +00:00

307 lines
8.9 KiB
JavaScript

'use strict';
// In worker threads, execute the script sent through the
// message port.
const {
ArrayPrototypeForEach,
ArrayPrototypePushApply,
ArrayPrototypeSplice,
AtomicsLoad,
ObjectDefineProperty,
PromisePrototypeThen,
RegExpPrototypeExec,
globalThis: {
SharedArrayBuffer,
},
} = primordials;
const {
prepareWorkerThreadExecution,
initializeModuleLoaders,
markBootstrapComplete,
} = require('internal/process/pre_execution');
const {
threadId,
getEnvMessagePort,
} = internalBinding('worker');
const workerIo = require('internal/worker/io');
const {
messageTypes: {
// Messages that may be received by workers
LOAD_SCRIPT,
// Messages that may be posted from workers
UP_AND_RUNNING,
ERROR_MESSAGE,
COULD_NOT_SERIALIZE_ERROR,
// Messages that may be either received or posted
STDIO_PAYLOAD,
STDIO_WANTS_MORE_DATA,
},
kStdioWantsMoreDataCallback,
} = workerIo;
const { setupMainThreadPort } = require('internal/worker/messaging');
const {
onGlobalUncaughtException,
evalScript,
evalTypeScript,
evalModuleEntryPoint,
parseAndEvalCommonjsTypeScript,
parseAndEvalModuleTypeScript,
} = require('internal/process/execution');
let debug = require('internal/util/debuglog').debuglog('worker', (fn) => {
debug = fn;
});
const assert = require('internal/assert');
const { getOptionValue } = require('internal/options');
const { exitCodes: { kGenericUserError } } = internalBinding('errors');
prepareWorkerThreadExecution();
debug(`[${threadId}] is setting up worker child environment`);
// Set up the message port and start listening
const port = getEnvMessagePort();
// If the main thread is spawned with env NODE_CHANNEL_FD, it's probably
// spawned by our child_process module. In the work threads, mark the
// related IPC properties as unavailable.
if (process.env.NODE_CHANNEL_FD) {
const workerThreadSetup = require('internal/process/worker_thread_only');
ObjectDefineProperty(process, 'channel', {
__proto__: null,
enumerable: false,
get: workerThreadSetup.unavailable('process.channel'),
});
ObjectDefineProperty(process, 'connected', {
__proto__: null,
enumerable: false,
get: workerThreadSetup.unavailable('process.connected'),
});
process.send = workerThreadSetup.unavailable('process.send()');
process.disconnect =
workerThreadSetup.unavailable('process.disconnect()');
}
port.on('message', (message) => {
if (message.type === LOAD_SCRIPT) {
port.unref();
const {
argv,
cwdCounter,
doEval,
environmentData,
filename,
hasStdin,
publicPort,
workerData,
mainThreadPort,
} = message;
if (doEval !== 'internal') {
if (argv !== undefined) {
ArrayPrototypePushApply(process.argv, argv);
}
const publicWorker = require('worker_threads');
publicWorker.parentPort = publicPort;
publicWorker.workerData = workerData;
}
require('internal/worker').assignEnvironmentData(environmentData);
setupMainThreadPort(mainThreadPort);
if (SharedArrayBuffer !== undefined) {
// The counter is only passed to the workers created by the main thread,
// not to workers created by other workers.
let cachedCwd = '';
let lastCounter = -1;
const originalCwd = process.cwd;
process.cwd = function() {
const currentCounter = AtomicsLoad(cwdCounter, 0);
if (currentCounter === lastCounter)
return cachedCwd;
lastCounter = currentCounter;
cachedCwd = originalCwd();
return cachedCwd;
};
workerIo.sharedCwdCounter = cwdCounter;
}
const isLoaderHookWorker = (filename === 'internal/modules/esm/worker' && doEval === 'internal');
if (!isLoaderHookWorker) {
// If we are in the loader hook worker, delay the module loader initializations until
// initializeAsyncLoaderHooksOnLoaderHookWorker() which needs to run preloads
// after the asynchronous loader hooks are registered.
initializeModuleLoaders({ shouldSpawnLoaderHookWorker: true, shouldPreloadModules: true });
}
if (!hasStdin)
process.stdin.push(null);
debug(`[${threadId}] starts worker script ${filename} ` +
`(eval = ${doEval}) at cwd = ${process.cwd()}`);
port.postMessage({ type: UP_AND_RUNNING });
switch (doEval) {
case 'internal': {
// Currently the only user of internal eval is the async loader hook thread.
assert(isLoaderHookWorker, `Unexpected internal eval ${filename}`);
const setupModuleWorker = require('internal/modules/esm/worker');
setupModuleWorker(workerData, publicPort);
break;
}
case 'classic': if (getOptionValue('--input-type') !== 'module') {
const name = '[worker eval]';
// This is necessary for CJS module compilation.
// TODO: pass this with something really internal.
ObjectDefineProperty(process, '_eval', {
__proto__: null,
configurable: true,
enumerable: true,
value: filename,
});
ArrayPrototypeSplice(process.argv, 1, 0, name);
const tsEnabled = getOptionValue('--experimental-strip-types');
const inputType = getOptionValue('--input-type');
if (inputType === 'module-typescript' && tsEnabled) {
// This is a special case where we want to parse and eval the
// TypeScript code as a module
parseAndEvalModuleTypeScript(filename, false);
break;
}
let evalFunction;
if (inputType === 'commonjs') {
evalFunction = evalScript;
} else if (inputType === 'commonjs-typescript' && tsEnabled) {
evalFunction = parseAndEvalCommonjsTypeScript;
} else if (tsEnabled) {
evalFunction = evalTypeScript;
} else {
// Default to commonjs.
evalFunction = evalScript;
}
evalFunction(name, filename);
break;
}
// eslint-disable-next-line no-fallthrough
case 'module': {
PromisePrototypeThen(evalModuleEntryPoint(filename), undefined, (e) => {
workerOnGlobalUncaughtException(e, true);
});
break;
}
case 'data-url': {
const { runEntryPointWithESMLoader } = require('internal/modules/run_main');
RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs.
const promise = runEntryPointWithESMLoader((cascadedLoader) => {
return cascadedLoader.import(filename, undefined, { __proto__: null }, undefined, true);
});
PromisePrototypeThen(promise, undefined, (e) => {
workerOnGlobalUncaughtException(e, true);
});
break;
}
default: {
// script filename
// runMain here might be monkey-patched by users in --require.
// XXX: the monkey-patchability here should probably be deprecated.
ArrayPrototypeSplice(process.argv, 1, 0, filename);
const CJSLoader = require('internal/modules/cjs/loader');
CJSLoader.Module.runMain(filename);
break;
}
}
} else if (message.type === STDIO_PAYLOAD) {
const { stream, chunks } = message;
ArrayPrototypeForEach(chunks, ({ chunk, encoding }) => {
process[stream].push(chunk, encoding);
});
} else {
assert(
message.type === STDIO_WANTS_MORE_DATA,
`Unknown worker message type ${message.type}`,
);
const { stream } = message;
process[stream][kStdioWantsMoreDataCallback]();
}
});
function workerOnGlobalUncaughtException(error, fromPromise) {
debug(`[${threadId}] gets uncaught exception`);
let handled = false;
let handlerThrew = false;
try {
handled = onGlobalUncaughtException(error, fromPromise);
} catch (e) {
error = e;
handlerThrew = true;
}
debug(`[${threadId}] uncaught exception handled = ${handled}`);
if (handled) {
return true;
}
if (!process._exiting) {
try {
process._exiting = true;
process.exitCode = kGenericUserError;
if (!handlerThrew) {
process.emit('exit', process.exitCode);
}
} catch {
// Continue regardless of error.
}
}
let serialized;
try {
const { serializeError } = require('internal/error_serdes');
serialized = serializeError(error);
} catch {
// Continue regardless of error.
}
debug(`[${threadId}] uncaught exception serialized = ${!!serialized}`);
if (serialized)
port.postMessage({
type: ERROR_MESSAGE,
error: serialized,
});
else
port.postMessage({ type: COULD_NOT_SERIALIZE_ERROR });
const { clearAsyncIdStack } = require('internal/async_hooks');
clearAsyncIdStack();
process.exit();
}
// Patch the global uncaught exception handler so it gets picked up by
// node::errors::TriggerUncaughtException().
process._fatalException = workerOnGlobalUncaughtException;
markBootstrapComplete();
// Necessary to reset RegExp statics before user code runs.
RegExpPrototypeExec(/^/, '');
port.start();