Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a id="ERR_VFS_NOT_MOUNTED"></a>

### `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.

<a id="ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING"></a>

### `ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
41 changes: 32 additions & 9 deletions doc/api/single-executable-applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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');
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
12 changes: 10 additions & 2 deletions lib/internal/main/worker_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
ObjectDefineProperty,
PromisePrototypeThen,
RegExpPrototypeExec,
StringPrototypeIncludes,
} = primordials;

const {
Expand Down Expand Up @@ -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();
}
}
Expand Down Expand Up @@ -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.
Expand Down
9 changes: 5 additions & 4 deletions lib/internal/vfs/file_system.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
20 changes: 9 additions & 11 deletions lib/internal/vfs/sea.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,37 +48,35 @@ 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');
provider = new SEAProvider({ extraFiles });
}
// 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();

return vfs;
}

/**
* 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());
}

/**
Expand Down
4 changes: 2 additions & 2 deletions lib/internal/vfs/sea_compile_cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -70,7 +70,7 @@ function mountAssets(options, mainName) {
const vfs = new VirtualFileSystem(provider, {
__proto__: null,
emitExperimentalWarning: false,
[kLoadLayer]: isArchive,
[kLoadLayer]: true,
});

if (!isArchive) {
Expand Down
23 changes: 23 additions & 0 deletions lib/internal/vfs/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1164,6 +1186,7 @@ function uninstallHooks() {
}

module.exports = {
assertVFSPathMounted,
registerVFS,
deregisterVFS,
};
12 changes: 5 additions & 7 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -320,13 +320,11 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* 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

Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading