diff --git a/lib/internal/main/embedding.js b/lib/internal/main/embedding.js index 63676385b14e8c..a1076ccb82a46b 100644 --- a/lib/internal/main/embedding.js +++ b/lib/internal/main/embedding.js @@ -6,7 +6,7 @@ const { const { isExperimentalSeaWarningNeeded } = internalBinding('sea'); const { emitExperimentalWarning } = require('internal/util'); const { embedderRequire, embedderRunCjs } = require('internal/util/embedding'); -const { getEmbedderEntryFunction } = internalBinding('mksnapshot'); +const { runEmbedderEntryPoint } = internalBinding('mksnapshot'); prepareMainThreadExecution(false, true); markBootstrapComplete(); @@ -15,4 +15,4 @@ if (isExperimentalSeaWarningNeeded()) { emitExperimentalWarning('Single executable application'); } -return getEmbedderEntryFunction()(embedderRequire, embedderRunCjs); +return runEmbedderEntryPoint(process, embedderRequire, embedderRunCjs); diff --git a/lib/internal/main/mksnapshot.js b/lib/internal/main/mksnapshot.js index 2a9b5d9851a2b1..2207d9253d7ec4 100644 --- a/lib/internal/main/mksnapshot.js +++ b/lib/internal/main/mksnapshot.js @@ -9,20 +9,30 @@ const { SafeSet, } = primordials; -const binding = internalBinding('mksnapshot'); const { BuiltinModule: { normalizeRequirableId } } = require('internal/bootstrap/realm'); const { - getEmbedderEntryFunction, + runEmbedderEntryPoint, compileSerializeMain, -} = binding; + anonymousMainPath, +} = internalBinding('mksnapshot'); const { getOptionValue, } = require('internal/options'); const { - readFileSync, -} = require('fs'); + initializeCallbacks, + namespace: { + addSerializeCallback, + addDeserializeCallback, + }, +} = require('internal/v8/startup_snapshot'); + +const { + prepareMainThreadExecution, +} = require('internal/process/pre_execution'); + +const path = require('path'); const supportedModules = new SafeSet(new SafeArrayIterator([ // '_http_agent', @@ -117,42 +127,7 @@ function requireForUserSnapshot(id) { } function main() { - const { - prepareMainThreadExecution, - } = require('internal/process/pre_execution'); - const path = require('path'); - - let serializeMainFunction = getEmbedderEntryFunction(); - const serializeMainArgs = [requireForUserSnapshot]; - - if (serializeMainFunction) { // embedded case - prepareMainThreadExecution(false, false); - // TODO(addaleax): Make this `embedderRunCjs` once require('module') - // is supported in snapshots. - const filename = process.execPath; - const dirname = path.dirname(filename); - function minimalRunCjs(source) { - const fn = compileSerializeMain(filename, source); - return fn(requireForUserSnapshot, filename, dirname); - } - serializeMainArgs.push(minimalRunCjs); - } else { - prepareMainThreadExecution(true, false); - const file = process.argv[1]; - const filename = path.resolve(file); - const dirname = path.dirname(filename); - const source = readFileSync(file, 'utf-8'); - serializeMainFunction = compileSerializeMain(filename, source); - serializeMainArgs.push(filename, dirname); - } - - const { - initializeCallbacks, - namespace: { - addSerializeCallback, - addDeserializeCallback, - }, - } = require('internal/v8/startup_snapshot'); + prepareMainThreadExecution(true, false); initializeCallbacks(); let stackTraceLimitDesc; @@ -161,14 +136,6 @@ function main() { ObjectDefineProperty(Error, 'stackTraceLimit', stackTraceLimitDesc); } }); - - if (getOptionValue('--inspect-brk')) { - internalBinding('inspector').callAndPauseOnStart( - serializeMainFunction, undefined, ...serializeMainArgs); - } else { - serializeMainFunction(...serializeMainArgs); - } - addSerializeCallback(() => { stackTraceLimitDesc = ObjectGetOwnPropertyDescriptor(Error, 'stackTraceLimit'); @@ -181,6 +148,31 @@ function main() { delete Error.stackTraceLimit; } }); + + // TODO(addaleax): Make this `embedderRunCjs` once require('module') + // is supported in snapshots. + function minimalRunCjs(source) { + let filename; + let dirname; + if (process.argv[1] === anonymousMainPath) { + filename = dirname = process.argv[1]; + } else { + filename = path.resolve(process.argv[1]); + dirname = path.dirname(filename); + } + + const fn = compileSerializeMain(filename, source); + return fn(requireForUserSnapshot, filename, dirname); + } + + const serializeMainArgs = [process, requireForUserSnapshot, minimalRunCjs]; + + if (getOptionValue('--inspect-brk')) { + internalBinding('inspector').callAndPauseOnStart( + runEmbedderEntryPoint, undefined, ...serializeMainArgs); + } else { + runEmbedderEntryPoint(...serializeMainArgs); + } } main(); diff --git a/src/node.cc b/src/node.cc index b29dc57d6011b5..7881742ac91b59 100644 --- a/src/node.cc +++ b/src/node.cc @@ -290,6 +290,8 @@ MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { return scope.EscapeMaybe(StartExecution(env, entry)); } + CHECK(!env->isolate_data()->is_building_snapshot()); + // TODO(joyeecheung): move these conditions into JS land and let the // deserialize main function take precedence. For workers, we need to // move the pre-execution part into a different file that can be @@ -311,15 +313,10 @@ MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { return StartExecution(env, "internal/main/inspect"); } - if (env->isolate_data()->is_building_snapshot()) { - return StartExecution(env, "internal/main/mksnapshot"); - } - if (per_process::cli_options->print_help) { return StartExecution(env, "internal/main/print_help"); } - if (env->options()->prof_process) { return StartExecution(env, "internal/main/prof_process"); } @@ -1119,7 +1116,8 @@ ExitCode GenerateAndWriteSnapshotData(const SnapshotData** snapshot_data_ptr, // node:embedded_snapshot_main indicates that we are using the // embedded snapshot and we are not supposed to clean it up. - if (result->args()[1] == "node:embedded_snapshot_main") { + const std::string& main_script = result->args()[1]; + if (main_script == "node:embedded_snapshot_main") { *snapshot_data_ptr = SnapshotBuilder::GetEmbeddedSnapshotData(); if (*snapshot_data_ptr == nullptr) { // The Node.js binary is built without embedded snapshot @@ -1134,8 +1132,21 @@ ExitCode GenerateAndWriteSnapshotData(const SnapshotData** snapshot_data_ptr, // Otherwise, load and run the specified main script. std::unique_ptr generated_data = std::make_unique(); - exit_code = node::SnapshotBuilder::Generate( - generated_data.get(), result->args(), result->exec_args()); + std::string main_script_content; + int r = ReadFileSync(&main_script_content, main_script.c_str()); + if (r != 0) { + FPrintF(stderr, + "Cannot read main script %s for building snapshot. %s: %s", + main_script, + uv_err_name(r), + uv_strerror(r)); + return ExitCode::kGenericUserError; + } + + exit_code = node::SnapshotBuilder::Generate(generated_data.get(), + result->args(), + result->exec_args(), + main_script_content); if (exit_code == ExitCode::kNoFailure) { *snapshot_data_ptr = generated_data.release(); } else { diff --git a/src/node.h b/src/node.h index cee4ebe2a432d1..846ec413f8e1fc 100644 --- a/src/node.h +++ b/src/node.h @@ -816,6 +816,8 @@ NODE_EXTERN struct uv_loop_s* GetCurrentEventLoop(v8::Isolate* isolate); // This function only works if `env` has an associated `MultiIsolatePlatform`. NODE_EXTERN v8::Maybe SpinEventLoop(Environment* env); +NODE_EXTERN std::string GetAnonymousMainPath(); + class NODE_EXTERN CommonEnvironmentSetup { public: ~CommonEnvironmentSetup(); @@ -848,6 +850,13 @@ class NODE_EXTERN CommonEnvironmentSetup { // no support for native/host objects other than Node.js builtins // in the snapshot. // + // If the embedder wants to use LoadEnvironment() later to run a snapshot + // builder script they should make sure args[1] contains the path of the + // snapshot script, which will be used to create __filename and __dirname + // in the context where the builder script is run. If they do not want to + // include the build-time paths into the snapshot, use the string returned + // by GetAnonymousMainPath() as args[1] to anonymize the script. + // // Snapshots are an *experimental* feature. In particular, the embedder API // exposed through this class is subject to change or removal between Node.js // versions, including possible API and ABI breakage. @@ -909,6 +918,7 @@ std::unique_ptr CommonEnvironmentSetup::Create( if (!errors->empty()) ret.reset(); return ret; } + // Implementation for ::CreateFromSnapshot -- the ::Create() method // could call this with a nullptr snapshot_data in a major version. template diff --git a/src/node_snapshot_builder.h b/src/node_snapshot_builder.h index f8cd900b2bdaa4..f433bc52c28864 100644 --- a/src/node_snapshot_builder.h +++ b/src/node_snapshot_builder.h @@ -5,6 +5,8 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #include +#include +#include #include "node_exit_code.h" #include "node_mutex.h" #include "v8.h" @@ -17,13 +19,15 @@ struct SnapshotData; class NODE_EXTERN_PRIVATE SnapshotBuilder { public: static ExitCode Generate(std::ostream& out, - const std::vector args, - const std::vector exec_args); + const std::vector& args, + const std::vector& exec_args, + std::optional main_script); // Generate the snapshot into out. static ExitCode Generate(SnapshotData* out, - const std::vector args, - const std::vector exec_args); + const std::vector& args, + const std::vector& exec_args, + std::optional main_script); // If nullptr is returned, the binary is not built with embedded // snapshot. diff --git a/src/node_snapshotable.cc b/src/node_snapshotable.cc index 55933e3bbef2f3..5724142de8e55c 100644 --- a/src/node_snapshotable.cc +++ b/src/node_snapshotable.cc @@ -897,9 +897,20 @@ void SnapshotBuilder::InitializeIsolateParams(const SnapshotData* data, const_cast(&(data->v8_snapshot_blob_data)); } -ExitCode SnapshotBuilder::Generate(SnapshotData* out, - const std::vector args, - const std::vector exec_args) { +ExitCode SnapshotBuilder::Generate( + SnapshotData* out, + const std::vector& args, + const std::vector& exec_args, + std::optional main_script) { + // The default snapshot is meant to be runtime-independent and has more + // restrictions. We do not enable the inspector and do not run the event + // loop when building the default snapshot to avoid inconsistencies, but + // we do for the fully customized one, and they are expected to fixup the + // inconsistencies using v8.startupSnapshot callbacks. + SnapshotMetadata::Type snapshot_type = + main_script.has_value() ? SnapshotMetadata::Type::kFullyCustomized + : SnapshotMetadata::Type::kDefault; + std::vector errors; auto setup = CommonEnvironmentSetup::CreateForSnapshotting( per_process::v8_platform.Platform(), &errors, args, exec_args); @@ -910,12 +921,6 @@ ExitCode SnapshotBuilder::Generate(SnapshotData* out, } Isolate* isolate = setup->isolate(); - // It's only possible to be kDefault in node_mksnapshot. - SnapshotMetadata::Type snapshot_type = - per_process::cli_options->per_isolate->build_snapshot - ? SnapshotMetadata::Type::kFullyCustomized - : SnapshotMetadata::Type::kDefault; - { HandleScope scope(isolate); TryCatch bootstrapCatch(isolate); @@ -927,32 +932,25 @@ ExitCode SnapshotBuilder::Generate(SnapshotData* out, } }); - // Initialize the main instance context. - { + // Run the custom main script for fully customized snapshots. + if (snapshot_type == SnapshotMetadata::Type::kFullyCustomized) { Context::Scope context_scope(setup->context()); Environment* env = setup->env(); - - // If --build-snapshot is true, lib/internal/main/mksnapshot.js would be - // loaded via LoadEnvironment() to execute process.argv[1] as the entry - // point (we currently only support this kind of entry point, but we - // could also explore snapshotting other kinds of execution modes - // in the future). - if (snapshot_type == SnapshotMetadata::Type::kFullyCustomized) { #if HAVE_INSPECTOR env->InitializeInspector({}); #endif - if (LoadEnvironment(env, StartExecutionCallback{}).IsEmpty()) { + if (LoadEnvironment(env, main_script.value()).IsEmpty()) { return ExitCode::kGenericUserError; } + // FIXME(joyeecheung): right now running the loop in the snapshot - // builder seems to introduces inconsistencies in JS land that need to + // builder might introduce inconsistencies in JS land that need to // be synchronized again after snapshot restoration. ExitCode exit_code = SpinEventLoopInternal(env).FromMaybe(ExitCode::kGenericUserError); if (exit_code != ExitCode::kNoFailure) { return exit_code; } - } } } @@ -1071,11 +1069,13 @@ ExitCode SnapshotBuilder::CreateSnapshot(SnapshotData* out, return ExitCode::kNoFailure; } -ExitCode SnapshotBuilder::Generate(std::ostream& out, - const std::vector args, - const std::vector exec_args) { +ExitCode SnapshotBuilder::Generate( + std::ostream& out, + const std::vector& args, + const std::vector& exec_args, + std::optional main_script) { SnapshotData data; - ExitCode exit_code = Generate(&data, args, exec_args); + ExitCode exit_code = Generate(&data, args, exec_args, main_script); if (exit_code != ExitCode::kNoFailure) { return exit_code; } @@ -1229,27 +1229,23 @@ void SerializeSnapshotableObjects(Realm* realm, }); } -// NB: This is also used by the regular embedding codepath. -void GetEmbedderEntryFunction(const FunctionCallbackInfo& args) { +static void RunEmbedderEntryPoint(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - Isolate* isolate = env->isolate(); - if (!env->embedder_entry_point()) return; - MaybeLocal jsfn = - Function::New(isolate->GetCurrentContext(), - [](const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - Local require_fn = args[0]; - Local runcjs_fn = args[1]; - CHECK(require_fn->IsFunction()); - CHECK(runcjs_fn->IsFunction()); - MaybeLocal retval = env->embedder_entry_point()( - {env->process_object(), - require_fn.As(), - runcjs_fn.As()}); - if (!retval.IsEmpty()) - args.GetReturnValue().Set(retval.ToLocalChecked()); - }); - if (!jsfn.IsEmpty()) args.GetReturnValue().Set(jsfn.ToLocalChecked()); + Local process_obj = args[0]; + Local require_fn = args[1]; + Local runcjs_fn = args[2]; + CHECK(process_obj->IsObject()); + CHECK(require_fn->IsFunction()); + CHECK(runcjs_fn->IsFunction()); + + const node::StartExecutionCallback& callback = env->embedder_entry_point(); + node::StartExecutionCallbackInfo info{process_obj.As(), + require_fn.As(), + runcjs_fn.As()}; + MaybeLocal retval = callback(info); + if (!retval.IsEmpty()) { + args.GetReturnValue().Set(retval.ToLocalChecked()); + } } void CompileSerializeMain(const FunctionCallbackInfo& args) { @@ -1301,6 +1297,12 @@ void SetDeserializeMainFunction(const FunctionCallbackInfo& args) { env->set_snapshot_deserialize_main(args[0].As()); } +constexpr const char* kAnonymousMainPath = "__node_anonymous_main"; + +std::string GetAnonymousMainPath() { + return kAnonymousMainPath; +} + namespace mksnapshot { BindingData::BindingData(Realm* realm, @@ -1377,8 +1379,7 @@ void CreatePerContextProperties(Local target, void CreatePerIsolateProperties(IsolateData* isolate_data, Local target) { Isolate* isolate = isolate_data->isolate(); - SetMethod( - isolate, target, "getEmbedderEntryFunction", GetEmbedderEntryFunction); + SetMethod(isolate, target, "runEmbedderEntryPoint", RunEmbedderEntryPoint); SetMethod(isolate, target, "compileSerializeMain", CompileSerializeMain); SetMethod(isolate, target, "setSerializeCallback", SetSerializeCallback); SetMethod(isolate, target, "setDeserializeCallback", SetDeserializeCallback); @@ -1386,10 +1387,12 @@ void CreatePerIsolateProperties(IsolateData* isolate_data, target, "setDeserializeMainFunction", SetDeserializeMainFunction); + target->Set(FIXED_ONE_BYTE_STRING(isolate, "anonymousMainPath"), + OneByteString(isolate, kAnonymousMainPath)); } void RegisterExternalReferences(ExternalReferenceRegistry* registry) { - registry->Register(GetEmbedderEntryFunction); + registry->Register(RunEmbedderEntryPoint); registry->Register(CompileSerializeMain); registry->Register(SetSerializeCallback); registry->Register(SetDeserializeCallback); diff --git a/test/embedding/embedtest.cc b/test/embedding/embedtest.cc index dbb420f34682d9..d08ba0f4fb942b 100644 --- a/test/embedding/embedtest.cc +++ b/test/embedding/embedtest.cc @@ -62,23 +62,49 @@ int RunNodeInstance(MultiIsolatePlatform* platform, const std::vector& exec_args) { int exit_code = 0; + // Format of the arguments of this binary: + // Building snapshot: + // embedtest js_code_to_eval arg1 arg2... \ + // --embedder-snapshot-blob blob-path \ + // --embedder-snapshot-create + // [--embedder-snapshot-as-file] + // Running snapshot: + // embedtest --embedder-snapshot-blob blob-path \ + // [--embedder-snapshot-as-file] + // arg1 arg2... + // No snapshot: + // embedtest arg1 arg2... node::EmbedderSnapshotData::Pointer snapshot; - auto snapshot_build_mode_it = - std::find(args.begin(), args.end(), "--embedder-snapshot-create"); - auto snapshot_arg_it = - std::find(args.begin(), args.end(), "--embedder-snapshot-blob"); - auto snapshot_as_file_it = - std::find(args.begin(), args.end(), "--embedder-snapshot-as-file"); - if (snapshot_arg_it < args.end() - 1 && - snapshot_build_mode_it == args.end()) { - const char* filename = (snapshot_arg_it + 1)->c_str(); - FILE* fp = fopen(filename, "r"); + + std::string binary_path = args[0]; + std::vector filtered_args; + bool is_building_snapshot = false; + bool snapshot_as_file = false; + std::string snapshot_blob_path; + for (size_t i = 0; i < args.size(); ++i) { + const std::string& arg = args[i]; + if (arg == "--embedder-snapshot-create") { + is_building_snapshot = true; + } else if (arg == "--embedder-snapshot-as-file") { + snapshot_as_file = true; + } else if (arg == "--embedder-snapshot-blob") { + assert(i + 1 < args.size()); + snapshot_blob_path = args[i + i]; + i++; + } else { + filtered_args.push_back(arg); + } + } + + if (!snapshot_blob_path.empty() && !is_building_snapshot) { + FILE* fp = fopen(snapshot_blob_path.c_str(), "r"); assert(fp != nullptr); - if (snapshot_as_file_it != args.end()) { + if (snapshot_as_file) { snapshot = node::EmbedderSnapshotData::FromFile(fp); } else { uv_fs_t req = uv_fs_t(); - int statret = uv_fs_stat(nullptr, &req, filename, nullptr); + int statret = + uv_fs_stat(nullptr, &req, snapshot_blob_path.c_str(), nullptr); assert(statret == 0); size_t filesize = req.statbuf.st_size; uv_fs_req_cleanup(&req); @@ -93,17 +119,27 @@ int RunNodeInstance(MultiIsolatePlatform* platform, assert(ret == 0); } + if (is_building_snapshot) { + // It contains at least the binary path, the code to snapshot, + // and --embedder-snapshot-create. Insert an anonymous filename + // as process.argv[1]. + assert(filtered_args.size() >= 3); + filtered_args.insert(filtered_args.begin() + 1, + node::GetAnonymousMainPath()); + } + std::vector errors; std::unique_ptr setup = - snapshot ? CommonEnvironmentSetup::CreateFromSnapshot( - platform, &errors, snapshot.get(), args, exec_args) - : snapshot_build_mode_it != args.end() - ? CommonEnvironmentSetup::CreateForSnapshotting( - platform, &errors, args, exec_args) - : CommonEnvironmentSetup::Create(platform, &errors, args, exec_args); + snapshot + ? CommonEnvironmentSetup::CreateFromSnapshot( + platform, &errors, snapshot.get(), filtered_args, exec_args) + : is_building_snapshot ? CommonEnvironmentSetup::CreateForSnapshotting( + platform, &errors, filtered_args, exec_args) + : CommonEnvironmentSetup::Create( + platform, &errors, filtered_args, exec_args); if (!setup) { for (const std::string& err : errors) - fprintf(stderr, "%s: %s\n", args[0].c_str(), err.c_str()); + fprintf(stderr, "%s: %s\n", binary_path.c_str(), err.c_str()); return 1; } @@ -129,7 +165,7 @@ int RunNodeInstance(MultiIsolatePlatform* platform, " globalThis.require = publicRequire;" "} else globalThis.require = require;" "globalThis.embedVars = { nön_ascıı: '🏳️‍🌈' };" - "require('vm').runInThisContext(process.argv[1]);"); + "require('vm').runInThisContext(process.argv[2]);"); } if (loadenv_ret.IsEmpty()) // There has been a JS exception. @@ -138,14 +174,13 @@ int RunNodeInstance(MultiIsolatePlatform* platform, exit_code = node::SpinEventLoop(env).FromMaybe(1); } - if (snapshot_arg_it < args.end() - 1 && - snapshot_build_mode_it != args.end()) { + if (!snapshot_blob_path.empty() && is_building_snapshot) { snapshot = setup->CreateSnapshot(); assert(snapshot); - FILE* fp = fopen((snapshot_arg_it + 1)->c_str(), "w"); + FILE* fp = fopen(snapshot_blob_path.c_str(), "w"); assert(fp != nullptr); - if (snapshot_as_file_it != args.end()) { + if (snapshot_as_file) { snapshot->ToFile(fp); } else { const std::vector vec = snapshot->ToBlob(); diff --git a/test/embedding/test-embedding.js b/test/embedding/test-embedding.js index 9dfaad6c2ab27f..a0ac4834b566eb 100644 --- a/test/embedding/test-embedding.js +++ b/test/embedding/test-embedding.js @@ -74,11 +74,16 @@ for (const extraSnapshotArgs of [[], ['--embedder-snapshot-as-file']]) { ]; fs.rmSync(blobPath, { force: true }); - assert.strictEqual(child_process.spawnSync(binary, [ + const child = child_process.spawnSync(binary, [ '--', ...buildSnapshotArgs, ], { cwd: tmpdir.path, - }).status, 0); + }); + if (child.status !== 0) { + console.log(child.stderr.toString()); + console.log(child.stdout.toString()); + } + assert.strictEqual(child.status, 0); const spawnResult = child_process.spawnSync(binary, ['--', ...runEmbeddedArgs]); assert.deepStrictEqual(JSON.parse(spawnResult.stdout), { originalArgv: [binary, ...buildSnapshotArgs], diff --git a/tools/snapshot/node_mksnapshot.cc b/tools/snapshot/node_mksnapshot.cc index d6d92ab156da62..ecc295acdbea32 100644 --- a/tools/snapshot/node_mksnapshot.cc +++ b/tools/snapshot/node_mksnapshot.cc @@ -87,7 +87,7 @@ int BuildSnapshot(int argc, char* argv[]) { node::ExitCode exit_code = node::ExitCode::kNoFailure; { exit_code = node::SnapshotBuilder::Generate( - out, result->args(), result->exec_args()); + out, result->args(), result->exec_args(), std::nullopt); if (exit_code == node::ExitCode::kNoFailure) { if (!out) { std::cerr << "Failed to write " << out_path << "\n";