mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
A single executable application built with "vfsArchive" mounted its archive at the next free layer, like any file system a program mounts itself, and only in the main thread. A worker had no mount at all, so a path into the archive, such as the main script's __filename, could not be loaded there. Mount the archive at the layer reserved for --vfs-load instead, so it is at the same mount point in every thread, and mount it in each worker as well, with the main script placed at its root just as in the main thread. The worker reads the main script through a new getMainCode() binding, since no main script is handed to it. Unlike the --vfs-load source, the archive needs no provider that a preload could register, so its mount in a worker is not deferred past --import. Since the archive takes that mount point, reject --vfs-load alongside it: --build-sea refuses it in "execArgv", and the executable refuses it at startup when it comes from --node-options or from the execArgv of a worker. Also fix the example in vfs.md, which showed a file system a program mounts itself at layer 0, the layer reserved for these mounts. Signed-off-by: Philipp Dunkel <pip@pipobscure.com> PR-URL: https://github.com/nodejs/node/pull/66709 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
348 lines
11 KiB
JavaScript
348 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
// In worker threads, execute the script sent through the
|
|
// message port.
|
|
|
|
const {
|
|
ArrayPrototypeForEach,
|
|
ArrayPrototypePushApply,
|
|
ArrayPrototypeSplice,
|
|
AtomicsLoad,
|
|
ObjectDefineProperty,
|
|
PromisePrototypeThen,
|
|
RegExpPrototypeExec,
|
|
} = primordials;
|
|
|
|
const {
|
|
prepareWorkerThreadExecution,
|
|
initializeModuleLoaders,
|
|
finishVfsMounts,
|
|
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,
|
|
webWorkerData,
|
|
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 (webWorkerData !== undefined) {
|
|
require('internal/webworker')
|
|
.installDedicatedWorkerGlobalScope(webWorkerData.url, webWorkerData);
|
|
}
|
|
|
|
// 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 });
|
|
// Re-mount the inherited --vfs-load source so its reserved path (which a
|
|
// worker filename may point into) resolves in this thread too. With
|
|
// --import, mounting is deferred to after that loop in run_main, matching
|
|
// the main thread; finishVfsMounts() is idempotent so it runs once.
|
|
if (getOptionValue('--import').length === 0) {
|
|
finishVfsMounts();
|
|
}
|
|
// Unlike the --vfs-load source, a SEA archive needs no provider that a
|
|
// preload could register, so its mount is never deferred.
|
|
if (getOptionValue('[sea_vfs_archive]')) {
|
|
require('internal/vfs/sea').initSeaVfsInWorker();
|
|
}
|
|
}
|
|
|
|
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 (webWorkerData?.source !== undefined) {
|
|
// The source of a web Worker script loaded from a blob: or data:
|
|
// URL.
|
|
runWebWorkerScript(webWorkerData.source, webWorkerData);
|
|
break;
|
|
} else 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('--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: {
|
|
if (webWorkerData !== undefined) {
|
|
// A web Worker script loaded from a file: URL. Its type decides
|
|
// how the source is run, regardless of the file extension.
|
|
const source = require('fs').readFileSync(filename, 'utf8');
|
|
runWebWorkerScript(source, webWorkerData);
|
|
break;
|
|
}
|
|
// 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();
|
|
}
|
|
|
|
function runWebWorkerScript(source, webWorkerData) {
|
|
const webworker = require('internal/webworker');
|
|
if (webWorkerData.type === 'module') {
|
|
PromisePrototypeThen(
|
|
webworker.runModuleScriptSource(source, webWorkerData.url),
|
|
undefined,
|
|
(error) => workerOnGlobalUncaughtException(error, true),
|
|
);
|
|
} else {
|
|
// Unlike a worker eval, "run a classic script" evaluates the source in
|
|
// the worker's global scope.
|
|
webworker.runClassicScriptSource(source, webWorkerData.url);
|
|
}
|
|
}
|
|
|
|
// 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();
|