mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
sea: mount vfsArchive at layer 0 and in workers
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>
This commit is contained in:
1 parent
2d908f48d9
commit
9f2fb935fd
15 files changed
+198
-22
No files matched your search
@@ -3955,6 +3955,10 @@ 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.
|
||||
|
||||
```console
|
||||
$ node --vfs-load=app.zip
|
||||
```
|
||||
|
||||
@@ -296,6 +296,27 @@ 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:
|
||||
|
||||
```cjs
|
||||
const { Worker, isMainThread } = require('node:worker_threads');
|
||||
|
||||
if (isMainThread) {
|
||||
new Worker(__filename);
|
||||
} else {
|
||||
// The worker runs the same script, from the same archive.
|
||||
console.log(require('./lib/math.js').add(2, 3));
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
#### Snapshot and code caching limitations
|
||||
|
||||
`"useVfs": true` cannot be used together with `"useSnapshot": true` or
|
||||
@@ -786,6 +807,7 @@ to help us document them.
|
||||
[Using native addons in the injected main script]: #using-native-addons-in-the-injected-main-script
|
||||
[VFS documentation]: vfs.md
|
||||
[Windows SDK]: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/
|
||||
[`--vfs-load`]: cli.md#--vfs-loadsource
|
||||
[`node:zlib`]: zlib.md
|
||||
[`process.execPath`]: process.md#processexecpath
|
||||
[`require()`]: modules.md#requireid
|
||||
|
||||
+1
-1
@@ -225,7 +225,7 @@ const fs = require('node:fs');
|
||||
const myVfs = vfs.create();
|
||||
myVfs.writeFileSync('/data.txt', 'Hello');
|
||||
const mountPoint = myVfs.mount();
|
||||
// e.g. '/dev/null/vfs/0'
|
||||
// e.g. '/dev/null/vfs/1'
|
||||
|
||||
fs.readFileSync(`${mountPoint}/data.txt`, 'utf8'); // 'Hello'
|
||||
```
|
||||
|
||||
@@ -2013,6 +2013,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.
|
||||
.Bd -literal
|
||||
$ node --vfs-load=app.zip
|
||||
.Ed
|
||||
|
||||
@@ -16,7 +16,6 @@ const {
|
||||
isExperimentalSeaWarningNeeded,
|
||||
isSea,
|
||||
isVfsEnabled,
|
||||
mainCodePath: seaMainCodePath,
|
||||
} = internalBinding('sea');
|
||||
const { emitExperimentalWarning } = require('internal/util');
|
||||
const { emitWarningSync } = require('internal/process/warning');
|
||||
@@ -134,13 +133,12 @@ function embedderRunESM(content, filename) {
|
||||
* @returns {string|null} The VFS path of the main script
|
||||
*/
|
||||
function setUpSeaVfs(content) {
|
||||
const mainName = path.basename(seaMainCodePath || process.execPath);
|
||||
const { initSeaVfs } = require('internal/vfs/sea');
|
||||
const seaVfs = initSeaVfs({ extraFiles: { [mainName]: content } });
|
||||
const { initSeaVfs, getSeaMainName } = require('internal/vfs/sea');
|
||||
const seaVfs = initSeaVfs(content);
|
||||
if (seaVfs === null) {
|
||||
return null;
|
||||
}
|
||||
return path.join(seaVfs.mountPoint, mainName);
|
||||
return path.join(seaVfs.mountPoint, getSeaMainName());
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
|
||||
@@ -152,6 +152,11 @@ port.on('message', (message) => {
|
||||
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)
|
||||
|
||||
+36
-9
@@ -9,8 +9,9 @@ const {
|
||||
isVfsEnabled,
|
||||
isVfsArchiveEnabled,
|
||||
getAsset,
|
||||
getMainCode,
|
||||
mainCodePath,
|
||||
} = internalBinding('sea');
|
||||
const { kEmptyObject } = require('internal/util');
|
||||
const {
|
||||
codes: {
|
||||
ERR_INVALID_STATE,
|
||||
@@ -29,14 +30,12 @@ let initialized = false;
|
||||
* SEA main script is placed at the mount point root so bundled code can
|
||||
* reach the assets through `__dirname`-relative paths and relative
|
||||
* `require()` calls.
|
||||
* @param {object} [options] Configuration options
|
||||
* @param {Record<string, string|Buffer>} [options.extraFiles] Additional
|
||||
* files to serve alongside the assets (used for the SEA main script)
|
||||
* @param {string|Buffer} mainCode The source of the SEA main script
|
||||
* @returns {VirtualFileSystem|null} The mounted VFS, or null if not running
|
||||
* as a SEA or VFS is not enabled in the SEA configuration
|
||||
* @throws {ERR_INVALID_STATE} If already initialized
|
||||
*/
|
||||
function initSeaVfs(options = kEmptyObject) {
|
||||
function initSeaVfs(mainCode) {
|
||||
if (initialized) {
|
||||
throw new ERR_INVALID_STATE('SEA VFS is already initialized');
|
||||
}
|
||||
@@ -46,25 +45,51 @@ function initSeaVfs(options = kEmptyObject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { VirtualFileSystem } = require('internal/vfs/file_system');
|
||||
const { VirtualFileSystem, kLoadLayer } = require('internal/vfs/file_system');
|
||||
|
||||
const extraFiles = { __proto__: null, [getSeaMainName()]: mainCode };
|
||||
const isArchive = isVfsArchiveEnabled();
|
||||
let provider;
|
||||
if (isVfsArchiveEnabled()) {
|
||||
provider = createZipProvider(options.extraFiles);
|
||||
if (isArchive) {
|
||||
provider = createZipProvider(extraFiles);
|
||||
} else {
|
||||
const { SEAProvider } = require('internal/vfs/providers/sea');
|
||||
provider = new SEAProvider({ extraFiles: options.extraFiles });
|
||||
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.
|
||||
const vfs = new VirtualFileSystem(provider, {
|
||||
__proto__: null,
|
||||
emitExperimentalWarning: false,
|
||||
[kLoadLayer]: isArchive,
|
||||
});
|
||||
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.
|
||||
*/
|
||||
function initSeaVfsInWorker() {
|
||||
if (isVfsArchiveEnabled()) {
|
||||
initSeaVfs(getMainCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The name under which the SEA main script is placed at the mount point root.
|
||||
* @returns {string}
|
||||
*/
|
||||
function getSeaMainName() {
|
||||
const { basename } = require('path');
|
||||
return basename(mainCodePath || process.execPath);
|
||||
}
|
||||
|
||||
// The reserved asset key under which --build-sea stores the ZIP archive
|
||||
// named by "vfsArchive". Must match kVfsArchiveAssetName in src/node_sea.cc.
|
||||
const kVfsArchiveAssetName = 'node:sea:vfs.zip';
|
||||
@@ -105,5 +130,7 @@ function createZipProvider(extraFiles) {
|
||||
/* c8 ignore stop */
|
||||
|
||||
module.exports = {
|
||||
getSeaMainName,
|
||||
initSeaVfs,
|
||||
initSeaVfsInWorker,
|
||||
};
|
||||
@@ -316,6 +316,17 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors,
|
||||
errors->push_back("either --check or --eval can be used, not both");
|
||||
}
|
||||
|
||||
#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) {
|
||||
errors->push_back(
|
||||
"--vfs-load cannot be used in a single executable application "
|
||||
"built with \"vfsArchive\"");
|
||||
}
|
||||
#endif // DISABLE_SINGLE_EXECUTABLE_APPLICATION
|
||||
|
||||
for (const std::string& pattern : permission::ParseEnvAllowList(allow_env)) {
|
||||
if (!permission::IsValidEnvAllowPattern(pattern)) {
|
||||
errors->push_back("--allow-env must be '*', a variable name, or a "
|
||||
@@ -816,6 +827,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("--experimental-quic",
|
||||
#ifndef OPENSSL_NO_QUIC
|
||||
"experimental QUIC support",
|
||||
|
||||
@@ -217,6 +217,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(webstorage) = HAVE_SQLITE;
|
||||
DEFINE_BOOL_FIELD(experimental_dtls) = EXPERIMENTALS_DEFAULT_VALUE;
|
||||
DEFINE_BOOL_FIELD(experimental_quic) = EXPERIMENTALS_DEFAULT_VALUE;
|
||||
|
||||
+31
-6
@@ -250,6 +250,10 @@ bool SeaResource::use_code_cache() const {
|
||||
return static_cast<bool>(flags & SeaFlags::kUseCodeCache);
|
||||
}
|
||||
|
||||
bool SeaResource::use_vfs_archive() const {
|
||||
return static_cast<bool>(flags & SeaFlags::kVfsArchive);
|
||||
}
|
||||
|
||||
const SeaResource& FindSingleExecutableResource() {
|
||||
static const SeaResource sea_resource = []() -> SeaResource {
|
||||
std::string_view blob = FindSingleExecutableBlob();
|
||||
@@ -277,12 +281,8 @@ void IsVfsEnabled(const FunctionCallbackInfo<Value>& args) {
|
||||
}
|
||||
|
||||
void IsVfsArchiveEnabled(const FunctionCallbackInfo<Value>& args) {
|
||||
bool enabled = false;
|
||||
if (IsSingleExecutable()) {
|
||||
const SeaResource& sea_resource = FindSingleExecutableResource();
|
||||
enabled = static_cast<bool>(sea_resource.flags & SeaFlags::kVfsArchive);
|
||||
}
|
||||
args.GetReturnValue().Set(enabled);
|
||||
args.GetReturnValue().Set(IsSingleExecutable() &&
|
||||
FindSingleExecutableResource().use_vfs_archive());
|
||||
}
|
||||
|
||||
void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo<Value>& args) {
|
||||
@@ -636,6 +636,15 @@ std::optional<SeaConfig> 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;
|
||||
}
|
||||
@@ -966,6 +975,20 @@ void GetAssetKeys(const FunctionCallbackInfo<Value>& args) {
|
||||
args.GetReturnValue().Set(result);
|
||||
}
|
||||
|
||||
// A worker has no main script handed to it, but places the SEA main script in
|
||||
// its virtual file system just as the main thread does.
|
||||
void GetMainCode(const FunctionCallbackInfo<Value>& args) {
|
||||
CHECK_EQ(args.Length(), 0);
|
||||
const SeaResource& sea_resource = FindSingleExecutableResource();
|
||||
CHECK(!sea_resource.use_snapshot());
|
||||
Local<Value> code;
|
||||
if (ToV8Value(args.GetIsolate()->GetCurrentContext(),
|
||||
sea_resource.main_code_or_snapshot)
|
||||
.ToLocal(&code)) {
|
||||
args.GetReturnValue().Set(code);
|
||||
}
|
||||
}
|
||||
|
||||
MaybeLocal<Value> LoadSingleExecutableApplication(
|
||||
const StartExecutionCallbackInfoWithModule& info) {
|
||||
// Here we are currently relying on the fact that in NodeMainInstance::Run(),
|
||||
@@ -1048,6 +1071,7 @@ void Initialize(Local<Object> target,
|
||||
IsExperimentalSeaWarningNeeded);
|
||||
SetMethod(context, target, "getAsset", GetAsset);
|
||||
SetMethod(context, target, "getAssetKeys", GetAssetKeys);
|
||||
SetMethod(context, target, "getMainCode", GetMainCode);
|
||||
}
|
||||
|
||||
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
||||
@@ -1057,6 +1081,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
||||
registry->Register(IsExperimentalSeaWarningNeeded);
|
||||
registry->Register(GetAsset);
|
||||
registry->Register(GetAssetKeys);
|
||||
registry->Register(GetMainCode);
|
||||
}
|
||||
|
||||
} // namespace sea
|
||||
|
||||
@@ -64,6 +64,7 @@ struct SeaResource {
|
||||
|
||||
bool use_snapshot() const;
|
||||
bool use_code_cache() const;
|
||||
bool use_vfs_archive() const;
|
||||
|
||||
static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags) +
|
||||
sizeof(SeaExecArgvExtension) +
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"main": "sea.js",
|
||||
"output": "sea-vfs-load-rejected",
|
||||
"useVfs": true,
|
||||
"vfsArchive": "assets.zip",
|
||||
"execArgvExtension": "cli"
|
||||
}
|
||||
Vendored
+30
-1
@@ -2,6 +2,19 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
const os = require('os');
|
||||
const { Worker, isMainThread, parentPort } = require('worker_threads');
|
||||
|
||||
if (!isMainThread) {
|
||||
// A worker mounts the archive too, at the same mount point, so the main
|
||||
// script and the code 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),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// With "useVfsZip", the bundled assets are stored as a single ZIP archive
|
||||
// and mounted through the ZipProvider. Everything is reachable through
|
||||
@@ -13,6 +26,9 @@ assert.strictEqual(path.basename(__filename), 'sea.js');
|
||||
assert.strictEqual(require.main, module);
|
||||
console.log('main script runs from', __filename);
|
||||
|
||||
// The archive is 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);
|
||||
|
||||
@@ -58,4 +74,17 @@ assert.notStrictEqual(a, b);
|
||||
a[0] = 0;
|
||||
assert.strictEqual(b.toString('utf8'), 'Hello from SEA VFS!');
|
||||
|
||||
console.log('All SEA VFS zip tests passed!');
|
||||
// The mount point is taken by the archive, so a worker cannot --vfs-load.
|
||||
assert.throws(() => new Worker(__filename, {
|
||||
execArgv: ['--experimental-vfs', `--vfs-load=${__dirname}`],
|
||||
}), { code: 'ERR_WORKER_INVALID_EXEC_ARGV' });
|
||||
|
||||
const worker = new Worker(__filename);
|
||||
worker.once('message', (message) => {
|
||||
assert.deepStrictEqual(message, {
|
||||
dirname: __dirname,
|
||||
greeting: 'Hello from SEA VFS!',
|
||||
sum: 5,
|
||||
});
|
||||
console.log('All SEA VFS zip tests passed!');
|
||||
});
|
||||
@@ -141,3 +141,24 @@ skipIfBuildSEAIsNotSupported();
|
||||
stderr: /"vfsArchive" field of .*vfs-archive-not-string\.json is not a string/,
|
||||
});
|
||||
}
|
||||
|
||||
// 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"/,
|
||||
});
|
||||
}
|
||||
@@ -57,4 +57,25 @@ 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.
|
||||
const rejectedFile = buildSEA(fixtures.path('sea', 'vfs-zip'), {
|
||||
configPath: 'sea-config-vfs-load-rejected.json',
|
||||
});
|
||||
spawnSyncAndAssert(
|
||||
rejectedFile,
|
||||
['--node-options=--experimental-vfs --vfs-load=assets.zip'],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
status: 9,
|
||||
stderr: /--vfs-load cannot be used in a single executable application built with "vfsArchive"/,
|
||||
},
|
||||
);
|
||||
}));
|
||||
Reference in new issue
Block a user