diff --git a/doc/api/cli.md b/doc/api/cli.md index 415b65ac5efa..12caa0185d8c 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -3996,9 +3996,9 @@ comes from anywhere else, such as the real file system, needs nothing added. the command line's decision, and the environment must not be able to redirect it. -A [single executable application][] built with `"vfsArchive"` mounts that -archive at the mount point reserved for `--vfs-load`, so `--vfs-load` cannot be -used in it. +`--vfs-load` cannot be used in a [single executable application][]: its entry +point is the `"main"` it was built with, and with `"useVfs"` its own virtual +file system is mounted at the mount point reserved for `--vfs-load`. ```console $ node --vfs-load=app.zip diff --git a/doc/api/errors.md b/doc/api/errors.md index 8be520b7e521..5661b529be3c 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -3598,6 +3598,17 @@ entry types are found. A `--vfs-load` source does not exist, is neither a regular file nor a directory, or is a source no provider claims. + + +### `ERR_VFS_NOT_MOUNTED` + +A path into a virtual file system was used in a thread in which that virtual +file system is not mounted. Each thread has its own mounts: a virtual file +system mounted with [`vfs.mount()`][] is not mounted in the [`Worker`][] +threads, so a worker cannot be started from a script in it. The source +mounted with [`--vfs-load`][] and the virtual file system of a +[single executable application][] are mounted in every thread. + ### `ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING` @@ -4787,6 +4798,7 @@ An error occurred trying to allocate memory. This should never happen. [`--force-fips`]: cli.md#--force-fips [`--no-addons`]: cli.md#--no-addons [`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode +[`--vfs-load`]: cli.md#--vfs-loadsource [`ABORT_ERR`]: #abort_err [`BoundSocket`]: net.md#class-netboundsocket [`Class: DOMException`]: globals.md#class-domexception @@ -4805,6 +4817,7 @@ An error occurred trying to allocate memory. This should never happen. [`REPL`]: repl.md [`ServerResponse`]: http.md#class-httpserverresponse [`Temporal`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal +[`Worker`]: worker_threads.md#class-worker [`Writable`]: stream.md#class-streamwritable [`ZipBuffer`]: zlib.md#class-zlibzipbuffer [`ZipFile`]: zlib.md#class-zlibzipfile @@ -4868,6 +4881,7 @@ An error occurred trying to allocate memory. This should never happen. [`util.inspect()`]: util.md#utilinspectobject-options [`util.parseArgs()`]: util.md#utilparseargsconfig [`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data +[`vfs.mount()`]: vfs.md#vfsmount [`zlib`]: zlib.md [crypto digest algorithm]: crypto.md#cryptogethashes [debugger]: debugger.md @@ -4878,6 +4892,7 @@ An error occurred trying to allocate memory. This should never happen. [package map]: packages.md#package-maps [relative URL]: https://url.spec.whatwg.org/#relative-url-string [self-reference a package using its name]: packages.md#self-referencing-a-package-using-its-name +[single executable application]: single-executable-applications.md#virtual-file-system-vfs-for-assets [special scheme]: https://url.spec.whatwg.org/#special-scheme [stream-based]: stream.md [syscall]: https://man7.org/linux/man-pages/man2/syscalls.2.html diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index dcef69a7ee95..72700121aa57 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -223,6 +223,13 @@ The VFS supports the `node:fs` operations for reading files and directories. Since the SEA VFS is read-only, write operations fail with `EROFS`. See the [VFS documentation][] for the full list of supported operations. +`useVfs` does for a single executable application what [`--vfs-load`][] does +for `node`: the entry point is its `"main"`, and modules resolve against its +own files. `--vfs-load` therefore cannot be used in a single executable +application, whether or not it uses `useVfs`: `--build-sea` rejects it in +`"execArgv"`, and the executable rejects it at startup when it comes from +`--node-options` or from the `execArgv` of a worker. + #### Loading modules from the VFS in a SEA When `useVfs` is enabled, the main script is executed from inside the @@ -264,6 +271,26 @@ Module format detection works the same way as on the real file system: name bundled ES modules with the `.mjs` extension (or provide the relevant `package.json` files as assets) so they are interpreted as ESM. +#### Worker threads + +The virtual file system is mounted at the mount point reserved for +[`--vfs-load`][], which is the same in every thread, and worker threads mount +it as well. A path into it, such as the main script's +`__filename` or a worker script bundled as an asset, can therefore be loaded +in a [`Worker`][], which reaches the other assets the same way the main +script does: + +```cjs +const { Worker } = require('node:worker_threads'); +const path = require('node:path'); + +// worker.js is bundled as an asset, at the root of the mount. +new Worker(path.join(__dirname, 'worker.js')); +``` + +A virtual file system mounted with [`vfs.mount()`][] in one thread is not +mounted in the others, so a worker cannot load a script from it. + #### Serving the assets from a ZIP archive with `"vfsArchive"` Instead of listing individual `"assets"`, the configuration can point @@ -299,10 +326,9 @@ executable only embeds the archive; read the files through the file system APIs instead. `"vfsArchive"` requires `"useVfs": true` and cannot be combined with `"assets"`. -The archive is mounted at the mount point reserved for [`--vfs-load`][], which -is the same in every thread, and worker threads mount it as well. A path into -the archive, such as the main script's `__filename`, can therefore be loaded -in a worker: +The archive is mounted at the same mount point as the assets would be, and +in [worker threads](#worker-threads) as well. A path into the archive, such +as the main script's `__filename`, can therefore be loaded in a worker: ```cjs const { Worker, isMainThread } = require('node:worker_threads'); @@ -315,11 +341,6 @@ if (isMainThread) { } ``` -Since that mount point is taken by the archive, `--vfs-load` cannot be used -with `"vfsArchive"`: `--build-sea` rejects it in `"execArgv"`, and the -executable rejects it at startup when it comes from `--node-options` or from -the `execArgv` of a worker. - #### Code cache for the modules in the VFS When `"useCodeCache": true` is combined with `"useVfs": true`, the code cache @@ -827,6 +848,7 @@ to help us document them. [VFS documentation]: vfs.md [Windows SDK]: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/ [`--vfs-load`]: cli.md#--vfs-loadsource +[`Worker`]: worker_threads.md#class-worker [`node:zlib`]: zlib.md [`process.execPath`]: process.md#processexecpath [`require()`]: modules.md#requireid @@ -837,6 +859,7 @@ to help us document them. [`sea.getRawAsset()`]: #seagetrawassetkey [`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data [`v8.startupSnapshot` API]: v8.md#startup-snapshot-api +[`vfs.mount()`]: vfs.md#vfsmount [documentation about startup snapshot support in Node.js]: cli.md#--build-snapshot [fuse]: https://www.electronjs.org/docs/latest/tutorial/fuses [module loader integration]: vfs.md#module-loader-integration diff --git a/doc/node.1 b/doc/node.1 index 7419eb9a34d2..c90a58b6d45a 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -2040,9 +2040,9 @@ comes from anywhere else, such as the real file system, needs nothing added. \fB--vfs-load\fR is not permitted in \fBNODE_OPTIONS\fR: which entry point runs is the command line's decision, and the environment must not be able to redirect it. -A single executable application built with \fB"vfsArchive"\fR mounts that -archive at the mount point reserved for \fB--vfs-load\fR, so \fB--vfs-load\fR cannot be -used in it. +\fB--vfs-load\fR cannot be used in a single executable application: its entry +point is the \fB"main"\fR it was built with, and with \fB"useVfs"\fR its own virtual +file system is mounted at the mount point reserved for \fB--vfs-load\fR. .Bd -literal $ node --vfs-load=app.zip .Ed diff --git a/lib/internal/errors.js b/lib/internal/errors.js index e2ee2cdd340d..786fc7e275eb 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1964,6 +1964,8 @@ E('ERR_VALID_PERFORMANCE_ENTRY_TYPE', 'At least one valid performance entry type is required', Error); E('ERR_VFS_INVALID_TARGET', '%s is not a valid --vfs-load source: must be an existing file or directory', Error); +E('ERR_VFS_NOT_MOUNTED', + '%s is in a virtual file system that is not mounted in this thread', Error); E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING', 'A dynamic import callback was not specified.', TypeError); E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG', diff --git a/lib/internal/main/worker_thread.js b/lib/internal/main/worker_thread.js index 1f5fa8f8212b..ed12eb946789 100644 --- a/lib/internal/main/worker_thread.js +++ b/lib/internal/main/worker_thread.js @@ -11,6 +11,7 @@ const { ObjectDefineProperty, PromisePrototypeThen, RegExpPrototypeExec, + StringPrototypeIncludes, } = primordials; const { @@ -153,9 +154,9 @@ port.on('message', (message) => { if (getOptionValue('--import').length === 0) { finishVfsMounts(); } - // Unlike the --vfs-load source, a SEA archive needs no provider that a + // Unlike the --vfs-load source, the SEA VFS needs no provider that a // preload could register, so its mount is never deferred. - if (getOptionValue('[sea_vfs_archive]')) { + if (getOptionValue('[sea_vfs]')) { require('internal/vfs/sea').initSeaVfsInWorker(); } } @@ -247,6 +248,13 @@ port.on('message', (message) => { runWebWorkerScript(source, webWorkerData); break; } + // A worker does not share the VFS mounts of its parent. Report a path + // into one that is not mounted here instead of letting the real file + // system answer for it. Every such path contains the reserved `vfs` + // directory, which spares other workers from loading the VFS. + if (StringPrototypeIncludes(filename, 'vfs')) { + require('internal/vfs/setup').assertVFSPathMounted(filename); + } // script filename // runMain here might be monkey-patched by users in --require. // XXX: the monkey-patchability here should probably be deprecated. diff --git a/lib/internal/vfs/file_system.js b/lib/internal/vfs/file_system.js index 743ba6d81e16..4982542239ea 100644 --- a/lib/internal/vfs/file_system.js +++ b/lib/internal/vfs/file_system.js @@ -55,10 +55,11 @@ const kLayerId = Symbol('kLayerId'); const kReservedRoot = Symbol('kReservedRoot'); const kLoadLayer = Symbol('kLoadLayer'); -// Layer 0 is reserved for the file system --vfs-load mounts, so that source is -// at the same mount point in every thread whatever else a thread mounts, and a -// path into it stays valid in a worker. Everything else is numbered from 1, in -// the order it is mounted. +// Layer 0 is reserved for the file system the process boots with: the source +// --vfs-load mounts, or the virtual file system of a single executable +// application, which cannot use --vfs-load. It is then at the same mount point +// in every thread whatever else a thread mounts, and a path into it stays valid +// in a worker. Everything else is numbered from 1, in the order it is mounted. const kLoadLayerId = 0; let nextLayerId = kLoadLayerId + 1; diff --git a/lib/internal/vfs/sea.js b/lib/internal/vfs/sea.js index c6aa19314533..9599717f1415 100644 --- a/lib/internal/vfs/sea.js +++ b/lib/internal/vfs/sea.js @@ -48,9 +48,8 @@ function initSeaVfs(mainCode) { const { VirtualFileSystem, kLoadLayer } = require('internal/vfs/file_system'); const extraFiles = { __proto__: null, [getSeaMainName()]: mainCode }; - const isArchive = isVfsArchiveEnabled(); let provider; - if (isArchive) { + if (isVfsArchiveEnabled()) { provider = createZipProvider(extraFiles); } else { const { SEAProvider } = require('internal/vfs/providers/sea'); @@ -58,12 +57,13 @@ function initSeaVfs(mainCode) { } // The SEA warning already covers the feature; don't emit the // VirtualFileSystem experimental warning for the implicit SEA mount. - // An archive is mounted where --vfs-load mounts its source, which is the - // same mount point in every thread, so a worker can reach the code in it. + // "useVfs" is the --vfs-load of a SEA: whichever provider serves it, the + // VFS is mounted where --vfs-load mounts its source, which is the same mount + // point in every thread, so a worker can reach the code in it. const vfs = new VirtualFileSystem(provider, { __proto__: null, emitExperimentalWarning: false, - [kLoadLayer]: isArchive, + [kLoadLayer]: true, }); vfs.mount(); @@ -71,14 +71,12 @@ function initSeaVfs(mainCode) { } /** - * Mounts the SEA virtual file system in a worker, when it is backed by a - * `"vfsArchive"`, so that a path into the mount in the main thread, such as - * the main script's `__filename`, is also valid in the worker. + * Mounts the SEA virtual file system in a worker, so that a path into the + * mount in the main thread, such as the main script's `__filename`, is also + * valid in the worker. */ function initSeaVfsInWorker() { - if (isVfsArchiveEnabled()) { - initSeaVfs(getMainCode()); - } + initSeaVfs(getMainCode()); } /** diff --git a/lib/internal/vfs/sea_compile_cache.js b/lib/internal/vfs/sea_compile_cache.js index 1d77e54f2855..5e6e1b95a3e3 100644 --- a/lib/internal/vfs/sea_compile_cache.js +++ b/lib/internal/vfs/sea_compile_cache.js @@ -45,7 +45,7 @@ const kTypeScriptExtensions = ['.ts', '.cts', '.mts']; /** * Mounts the assets of the SEA configuration the way the executable mounts * them: the main script at the mount point root, at the layer reserved for - * --vfs-load when they come from an archive (see lib/internal/vfs/sea.js). + * --vfs-load (see lib/internal/vfs/sea.js). * @param {object} options The SEA configuration, see buildSeaCompileCache() * @param {string} mainName The name of the main script at the mount root * @returns {string} The mount point @@ -70,7 +70,7 @@ function mountAssets(options, mainName) { const vfs = new VirtualFileSystem(provider, { __proto__: null, emitExperimentalWarning: false, - [kLoadLayer]: isArchive, + [kLoadLayer]: true, }); if (!isArchive) { diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index 0827100c2d82..1ed74f5d566c 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -22,6 +22,7 @@ const { ERR_INVALID_ARG_TYPE, ERR_INVALID_STATE, ERR_MODULE_NOT_FOUND, + ERR_VFS_NOT_MOUNTED, }, } = require('internal/errors'); const { createENOENT, createEROFS, createEXDEV } = require('internal/vfs/errors'); @@ -176,6 +177,27 @@ function findVFSOrRoot(inputPath) { return { vfs, normalized }; } +/** + * Throws if `filename` is under the reserved VFS root but no layer mounted in + * this thread owns it. Each thread has its own mounts, so this is the case of + * a path a worker is handed into a VFS its parent mounted. Left to the native + * loader, such a path fails confusingly: on Windows, where the root sits under + * `\\.\nul`, the NUL device is found as the file and as an empty, invalid + * package.json above it. + * @param {string} filename + */ +function assertVFSPathMounted(filename) { + const normalized = normalizeMountedPath(filename); + const root = getNormalizedVfsRoot(); + if (normalized !== root && !StringPrototypeStartsWith(normalized, root + sep)) { + return; + } + const layerId = getLayerIdFromPath(normalized); + if (layerId === -1 || !activeVFSLayers.has(layerId)) { + throw new ERR_VFS_NOT_MOUNTED(filename); + } +} + /** * Resolves a path string to the VFS that serves it for the fs functions, * or null for a path outside the reserved VFS root. Unlike the loader, the @@ -1164,6 +1186,7 @@ function uninstallHooks() { } module.exports = { + assertVFSPathMounted, registerVFS, deregisterVFS, }; diff --git a/src/node_options.cc b/src/node_options.cc index e141fab9e823..2aee993404ee 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -320,13 +320,11 @@ void EnvironmentOptions::CheckOptions(std::vector* errors, } #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION - sea_vfs_archive = sea::IsSingleExecutable() && - sea::FindSingleExecutableResource().use_vfs_archive(); - // The archive takes the mount point reserved for the --vfs-load source. - if (!vfs_load_source.empty() && sea_vfs_archive) { + sea_vfs = sea::IsSingleExecutable() && + sea::FindSingleExecutableResource().use_vfs(); + if (!vfs_load_source.empty() && sea::IsSingleExecutable()) { errors->push_back( - "--vfs-load cannot be used in a single executable application " - "built with \"vfsArchive\""); + "--vfs-load cannot be used in a single executable application"); } #endif // DISABLE_SINGLE_EXECUTABLE_APPLICATION @@ -830,7 +828,7 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { kDisallowedInEnvvar); AddOption("[vfs_load_set]", "", BOOL_FIELD(vfs_load)); Implies("--vfs-load", "[vfs_load_set]"); - AddOption("[sea_vfs_archive]", "", BOOL_FIELD(sea_vfs_archive)); + AddOption("[sea_vfs]", "", BOOL_FIELD(sea_vfs)); AddOption("--experimental-quic", #ifndef OPENSSL_NO_QUIC "experimental QUIC support", diff --git a/src/node_options.h b/src/node_options.h index d28560c5a539..14f0cc024ef8 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -225,7 +225,7 @@ class EnvironmentOptions : public Options { DEFINE_BOOL_FIELD(experimental_stream_iter) = EXPERIMENTALS_DEFAULT_VALUE; DEFINE_BOOL_FIELD(experimental_vfs) = true; DEFINE_BOOL_FIELD(vfs_load) = false; - DEFINE_BOOL_FIELD(sea_vfs_archive) = false; + DEFINE_BOOL_FIELD(sea_vfs) = false; DEFINE_BOOL_FIELD(webstorage) = HAVE_SQLITE; DEFINE_BOOL_FIELD(experimental_dtls) = EXPERIMENTALS_DEFAULT_VALUE; DEFINE_BOOL_FIELD(experimental_quic) = EXPERIMENTALS_DEFAULT_VALUE; diff --git a/src/node_sea.cc b/src/node_sea.cc index b25d0fcb93cc..09e1599e9506 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -257,6 +257,10 @@ bool SeaResource::use_code_cache() const { return static_cast(flags & SeaFlags::kUseCodeCache); } +bool SeaResource::use_vfs() const { + return static_cast(flags & SeaFlags::kEnableVfs); +} + bool SeaResource::use_vfs_archive() const { return static_cast(flags & SeaFlags::kVfsArchive); } @@ -622,6 +626,17 @@ std::optional ParseSingleExecutableConfig( } } + // A SEA rejects --vfs-load at startup (see EnvironmentOptions::CheckOptions), + // so do not build one that passes it in "execArgv". + for (const std::string& arg : result.exec_argv) { + if (arg == "--vfs-load" || arg.starts_with("--vfs-load=")) { + FPrintF(stderr, + "--vfs-load cannot be used in \"execArgv\" of a single " + "executable application\n"); + return std::nullopt; + } + } + if (static_cast(result.flags & SeaFlags::kVfsArchive)) { if (!static_cast(result.flags & SeaFlags::kEnableVfs)) { FPrintF(stderr, "\"vfsArchive\" requires \"useVfs\" to be true\n"); @@ -638,15 +653,6 @@ std::optional ParseSingleExecutableConfig( config_path); return std::nullopt; } - // The archive takes the mount point reserved for the --vfs-load source. - for (const std::string& arg : result.exec_argv) { - if (arg == "--vfs-load" || arg.starts_with("--vfs-load=")) { - FPrintF(stderr, - "\"vfsArchive\" cannot be used together with --vfs-load in " - "\"execArgv\"\n"); - return std::nullopt; - } - } // The archive is embedded as a single reserved asset. result.flags |= SeaFlags::kIncludeAssets; } diff --git a/src/node_sea.h b/src/node_sea.h index b4c852eff4eb..c88a5b5daa62 100644 --- a/src/node_sea.h +++ b/src/node_sea.h @@ -64,6 +64,7 @@ struct SeaResource { bool use_snapshot() const; bool use_code_cache() const; + bool use_vfs() const; bool use_vfs_archive() const; static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags) + diff --git a/test/fixtures/sea/vfs-zip/sea.js b/test/fixtures/sea/vfs-zip/sea.js index 439728c78e1a..0ce049831433 100644 --- a/test/fixtures/sea/vfs-zip/sea.js +++ b/test/fixtures/sea/vfs-zip/sea.js @@ -74,7 +74,7 @@ assert.notStrictEqual(a, b); a[0] = 0; assert.strictEqual(b.toString('utf8'), 'Hello from SEA VFS!'); -// The mount point is taken by the archive, so a worker cannot --vfs-load. +// --vfs-load cannot be used in a SEA, so neither can a worker. assert.throws(() => new Worker(__filename, { execArgv: ['--experimental-vfs', `--vfs-load=${__dirname}`], }), { code: 'ERR_WORKER_INVALID_EXEC_ARGV' }); diff --git a/test/fixtures/sea/vfs/sea-config-code-cache.json b/test/fixtures/sea/vfs/sea-config-code-cache.json index 7627849eb9de..6ff862cad4a7 100644 --- a/test/fixtures/sea/vfs/sea-config-code-cache.json +++ b/test/fixtures/sea/vfs/sea-config-code-cache.json @@ -5,6 +5,7 @@ "useCodeCache": true, "assets": { "config.json": "config.json", + "worker.js": "worker.js", "data/greeting.txt": "greeting.txt", "modules/math.js": "math.js", "modules/calculator.js": "calculator.js", diff --git a/test/fixtures/sea/vfs/sea-config-vfs-load-rejected.json b/test/fixtures/sea/vfs/sea-config-vfs-load-rejected.json new file mode 100644 index 000000000000..fc450de44fec --- /dev/null +++ b/test/fixtures/sea/vfs/sea-config-vfs-load-rejected.json @@ -0,0 +1,9 @@ +{ + "main": "sea.js", + "output": "sea-vfs-load-rejected", + "useVfs": true, + "assets": { + "config.json": "config.json" + }, + "execArgvExtension": "cli" +} diff --git a/test/fixtures/sea/vfs/sea-config.json b/test/fixtures/sea/vfs/sea-config.json index 767e778ea26e..307ff3653ee5 100644 --- a/test/fixtures/sea/vfs/sea-config.json +++ b/test/fixtures/sea/vfs/sea-config.json @@ -4,6 +4,7 @@ "useVfs": true, "assets": { "config.json": "config.json", + "worker.js": "worker.js", "data/greeting.txt": "greeting.txt", "modules/math.js": "math.js", "modules/calculator.js": "calculator.js", diff --git a/test/fixtures/sea/vfs/sea.js b/test/fixtures/sea/vfs/sea.js index 59989390c7d1..bff660148fc3 100644 --- a/test/fixtures/sea/vfs/sea.js +++ b/test/fixtures/sea/vfs/sea.js @@ -2,6 +2,7 @@ const fs = require('fs'); const path = require('path'); const assert = require('assert'); +const os = require('os'); // The SEA VFS never shadows the real file system: the assets are mounted at // a reserved mount point and the main script runs from inside the mount, so @@ -14,6 +15,10 @@ assert.strictEqual(path.basename(__filename), 'sea.js'); assert.strictEqual(require.main, module); console.log('main script runs from', __filename); +// Like an archive, the assets are mounted at the mount point reserved for +// --vfs-load. +assert.strictEqual(__dirname, path.join(os.devNull, 'vfs', '0')); + // The main script itself is readable through fs. assert.strictEqual(fs.existsSync(__filename), true); @@ -103,4 +108,22 @@ const exportsPkg = require('test-exports-pkg'); assert.strictEqual(exportsPkg.fromExports, true, 'exports-only package should resolve'); console.log('exports-only package lookup test passed'); -console.log('All SEA VFS tests passed!'); +// A worker started from a script among the assets can load it, and the +// assets next to it, from the same mount point as the main thread. +const { Worker } = require('worker_threads'); + +// --vfs-load cannot be used in a SEA, so neither can a worker. +assert.throws(() => new Worker(path.join(__dirname, 'worker.js'), { + execArgv: ['--experimental-vfs', `--vfs-load=${__dirname}`], +}), { code: 'ERR_WORKER_INVALID_EXEC_ARGV' }); + +const worker = new Worker(path.join(__dirname, 'worker.js')); +worker.once('message', (message) => { + assert.deepStrictEqual(message, { + dirname: __dirname, + greeting: 'Hello from SEA VFS!', + sum: 5, + }); + console.log('worker test passed'); + console.log('All SEA VFS tests passed!'); +}); diff --git a/test/fixtures/sea/vfs/worker.js b/test/fixtures/sea/vfs/worker.js new file mode 100644 index 000000000000..47fc8046d1c9 --- /dev/null +++ b/test/fixtures/sea/vfs/worker.js @@ -0,0 +1,12 @@ +'use strict'; +const fs = require('fs'); +const path = require('path'); +const { parentPort } = require('worker_threads'); + +// A worker mounts the assets too, at the same mount point, so a worker script +// among them, and the code and data next to it, can be loaded from there. +parentPort.postMessage({ + dirname: __dirname, + greeting: fs.readFileSync(path.join(__dirname, 'data', 'greeting.txt'), 'utf8'), + sum: require('./modules/math.js').add(2, 3), +}); diff --git a/test/parallel/test-vfs-worker-not-mounted.js b/test/parallel/test-vfs-worker-not-mounted.js new file mode 100644 index 000000000000..46f1ed1fccd6 --- /dev/null +++ b/test/parallel/test-vfs-worker-not-mounted.js @@ -0,0 +1,45 @@ +// Flags: --disable-warning=ExperimentalWarning +'use strict'; + +// A worker does not share the VFS mounts of the thread that starts it, so a +// worker script in a VFS mounted with vfs.mount() cannot be loaded. The worker +// reports that, instead of failing on the real file system (on Windows, as an +// invalid package.json, because paths under the VFS root open the NUL device). + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); +const { Worker } = require('worker_threads'); + +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/worker.js', "require('worker_threads').parentPort.postMessage('ran');\n"); + const mountPoint = myVfs.mount(); + const filename = path.join(mountPoint, 'worker.js'); + + const worker = new Worker(filename); + worker.on('message', common.mustNotCall()); + worker.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_VFS_NOT_MOUNTED'); + assert.strictEqual( + err.message, + `${filename} is in a virtual file system that is not mounted in this thread`); + myVfs.unmount(); + })); +} + +// A real worker script whose path merely mentions "vfs" runs as usual. +{ + tmpdir.refresh(); + const filename = tmpdir.resolve('vfs-worker.js'); + fs.writeFileSync(filename, "require('worker_threads').parentPort.postMessage('ran');\n"); + + const worker = new Worker(filename); + worker.on('message', common.mustCall((message) => { + assert.strictEqual(message, 'ran'); + })); + worker.on('error', common.mustNotCall()); +} diff --git a/test/sea/test-build-sea-vfs-incompatible-options.js b/test/sea/test-build-sea-vfs-incompatible-options.js index e2ef11a01458..92666cbf6a5c 100644 --- a/test/sea/test-build-sea-vfs-incompatible-options.js +++ b/test/sea/test-build-sea-vfs-incompatible-options.js @@ -120,23 +120,29 @@ skipIfBuildSEAIsNotSupported(); }); } -// Test: "vfsArchive" combined with --vfs-load in "execArgv" -for (const arg of ['--vfs-load=assets.zip', '--vfs-load']) { - tmpdir.refresh(); - const config = tmpdir.resolve('vfs-archive-vfs-load.json'); - writeFileSync(config, JSON.stringify({ - main: 'bundle.js', - output: 'sea', - useVfs: true, - vfsArchive: 'assets.zip', - execArgv: ['--experimental-vfs', arg], - }), 'utf8'); - spawnSyncAndAssert( - process.execPath, - ['--build-sea', config], { - cwd: tmpdir.path, - }, { - status: 1, - stderr: /"vfsArchive" cannot be used together with --vfs-load in "execArgv"/, - }); +// Test: --vfs-load in "execArgv", which a SEA rejects at startup whether or +// not it uses a VFS, and whatever serves the VFS. +for (const vfsConfig of [ + {}, + { useVfs: true }, + { useVfs: true, vfsArchive: 'assets.zip' }, +]) { + for (const arg of ['--vfs-load=assets.zip', '--vfs-load']) { + tmpdir.refresh(); + const config = tmpdir.resolve('vfs-load.json'); + writeFileSync(config, JSON.stringify({ + main: 'bundle.js', + output: 'sea', + ...vfsConfig, + execArgv: ['--experimental-vfs', arg], + }), 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /--vfs-load cannot be used in "execArgv" of a single executable application/, + }); + } } diff --git a/test/sea/test-single-executable-application-vfs-code-cache.js b/test/sea/test-single-executable-application-vfs-code-cache.js index a1666473ce8e..788936879613 100644 --- a/test/sea/test-single-executable-application-vfs-code-cache.js +++ b/test/sea/test-single-executable-application-vfs-code-cache.js @@ -60,9 +60,10 @@ async function generateBlob(fixtureDir, prepare) { } async function main() { - // CommonJS: sea.js, modules/math.js, modules/calculator.js and the two - // packages under node_modules; config.json and greeting.txt are data. - assert.strictEqual(await generateBlob(fixtures.path('sea', 'vfs')), 5); + // CommonJS: sea.js, worker.js, modules/math.js, modules/calculator.js and + // the two packages under node_modules; config.json and greeting.txt are + // data. + assert.strictEqual(await generateBlob(fixtures.path('sea', 'vfs')), 6); // ESM: main.mjs, modules/math.mjs, modules/calculator.cjs and the package. assert.strictEqual(await generateBlob(fixtures.path('sea', 'vfs-esm')), 4); @@ -101,7 +102,7 @@ async function main() { stderr: /Wrote single executable preparation blob/, }); assert.strictEqual( - countCompileCacheEntries(tmpdir.resolve('sea-prep.blob')), 5); + countCompileCacheEntries(tmpdir.resolve('sea-prep.blob')), 6); // The executable runs with the cache embedded, and the reserved assets // are not visible as files of the virtual file system. diff --git a/test/sea/test-single-executable-application-vfs-zip.js b/test/sea/test-single-executable-application-vfs-zip.js index 6165d567de5b..dd30c6f7cf1e 100644 --- a/test/sea/test-single-executable-application-vfs-zip.js +++ b/test/sea/test-single-executable-application-vfs-zip.js @@ -58,9 +58,8 @@ main().then(common.mustCall(() => { }, ); - // The archive is mounted where --vfs-load would mount its source, so the - // executable rejects --vfs-load at startup. --build-sea already refuses it - // in "execArgv", so pass it through --node-options instead. + // A SEA rejects --vfs-load at startup. --build-sea already refuses it in + // "execArgv", so pass it through --node-options instead. const rejectedFile = buildSEA(fixtures.path('sea', 'vfs-zip'), { configPath: 'sea-config-vfs-load-rejected.json', }); @@ -75,7 +74,7 @@ main().then(common.mustCall(() => { }, { status: 9, - stderr: /--vfs-load cannot be used in a single executable application built with "vfsArchive"/, + stderr: /--vfs-load cannot be used in a single executable application/, }, ); })); diff --git a/test/sea/test-single-executable-application-vfs.js b/test/sea/test-single-executable-application-vfs.js index 46a50cd79f86..3b92eab033ff 100644 --- a/test/sea/test-single-executable-application-vfs.js +++ b/test/sea/test-single-executable-application-vfs.js @@ -36,3 +36,24 @@ spawnSyncAndAssert( }, }, ); + +// A SEA rejects --vfs-load at startup, so it cannot take the mount point of +// the assets. --build-sea already refuses it in "execArgv", so pass it through +// --node-options instead. +const rejectedFile = buildSEA(fixtures.path('sea', 'vfs'), { + configPath: 'sea-config-vfs-load-rejected.json', +}); +spawnSyncAndAssert( + rejectedFile, + ['--node-options=--experimental-vfs --vfs-load=.'], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: undefined, + }, + }, + { + status: 9, + stderr: /--vfs-load cannot be used in a single executable application/, + }, +);