Skip to content

Commit

Permalink
src: preload function for Environment
Browse files Browse the repository at this point in the history
This PR adds a |preload| arg to the node::LoadEnvironment to allow
embedders to set a preload function for the environment, which will run
after the environment is loaded and before the main script runs.

This is similiar to the --require CLI option, but runs a C++ function,
and can only be set by embedders.

The preload function can be used by embedders to inject scripts before
running the main script, for example:
1. In Electron it is used to initialize the ASAR virtual filesystem,
   inject custom process properties, etc.
2. In VS Code it can be used to reset the module search paths for
   extensions.

PR-URL: nodejs#51539
Reviewed-By: Joyee Cheung <[email protected]>
Reviewed-By: Yagiz Nizipli <[email protected]>
  • Loading branch information
zcbenz authored and rdw-msft committed Mar 26, 2024
1 parent f393919 commit 15b57b3
Show file tree
Hide file tree
Showing 10 changed files with 108 additions and 9 deletions.
7 changes: 7 additions & 0 deletions lib/internal/process/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ function setupUserModules(forceDefaultLoader = false) {
} = require('internal/modules/helpers');
assert(!hasStartedUserCJSExecution());
assert(!hasStartedUserESMExecution());
if (getEmbedderOptions().hasEmbedderPreload) {
runEmbedderPreload();
}
// Do not enable preload modules if custom loaders are disabled.
// For example, loader workers are responsible for doing this themselves.
// And preload modules are not supported in ShadowRealm as well.
Expand Down Expand Up @@ -761,6 +764,10 @@ function initializeFrozenIntrinsics() {
}
}

function runEmbedderPreload() {
internalBinding('mksnapshot').runEmbedderPreload(process, require);
}

function loadPreloadModules() {
// For user code, we preload modules if `-r` is passed
const preloadModules = getOptionValue('--require');
Expand Down
18 changes: 12 additions & 6 deletions src/api/environment.cc
Original file line number Diff line number Diff line change
Expand Up @@ -538,25 +538,31 @@ NODE_EXTERN std::unique_ptr<InspectorParentHandle> GetInspectorParentHandle(
#endif
}

MaybeLocal<Value> LoadEnvironment(
Environment* env,
StartExecutionCallback cb) {
MaybeLocal<Value> LoadEnvironment(Environment* env,
StartExecutionCallback cb,
EmbedderPreloadCallback preload) {
env->InitializeLibuv();
env->InitializeDiagnostics();
if (preload) {
env->set_embedder_preload(std::move(preload));
}

return StartExecution(env, cb);
}

MaybeLocal<Value> LoadEnvironment(Environment* env,
std::string_view main_script_source_utf8) {
std::string_view main_script_source_utf8,
EmbedderPreloadCallback preload) {
CHECK_NOT_NULL(main_script_source_utf8.data());
return LoadEnvironment(
env, [&](const StartExecutionCallbackInfo& info) -> MaybeLocal<Value> {
env,
[&](const StartExecutionCallbackInfo& info) -> MaybeLocal<Value> {
Local<Value> main_script =
ToV8Value(env->context(), main_script_source_utf8).ToLocalChecked();
return info.run_cjs->Call(
env->context(), Null(env->isolate()), 1, &main_script);
});
},
std::move(preload));
}

Environment* GetCurrentEnvironment(Local<Context> context) {
Expand Down
8 changes: 8 additions & 0 deletions src/env-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,14 @@ inline builtins::BuiltinLoader* Environment::builtin_loader() {
return &builtin_loader_;
}

inline const EmbedderPreloadCallback& Environment::embedder_preload() const {
return embedder_preload_;
}

inline void Environment::set_embedder_preload(EmbedderPreloadCallback fn) {
embedder_preload_ = std::move(fn);
}

inline double Environment::new_async_id() {
async_hooks()->async_id_fields()[AsyncHooks::kAsyncIdCounter] += 1;
return async_hooks()->async_id_fields()[AsyncHooks::kAsyncIdCounter];
Expand Down
4 changes: 4 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,9 @@ class Environment : public MemoryRetainer {

#endif // HAVE_INSPECTOR

inline const EmbedderPreloadCallback& embedder_preload() const;
inline void set_embedder_preload(EmbedderPreloadCallback fn);

inline void set_process_exit_handler(
std::function<void(Environment*, ExitCode)>&& handler);

Expand Down Expand Up @@ -1207,6 +1210,7 @@ class Environment : public MemoryRetainer {
std::unique_ptr<PrincipalRealm> principal_realm_ = nullptr;

builtins::BuiltinLoader builtin_loader_;
EmbedderPreloadCallback embedder_preload_;

// Used by allocate_managed_buffer() and release_managed_buffer() to keep
// track of the BackingStore for a given pointer.
Expand Down
25 changes: 23 additions & 2 deletions src/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -731,12 +731,33 @@ struct StartExecutionCallbackInfo {

using StartExecutionCallback =
std::function<v8::MaybeLocal<v8::Value>(const StartExecutionCallbackInfo&)>;
using EmbedderPreloadCallback =
std::function<void(Environment* env,
v8::Local<v8::Value> process,
v8::Local<v8::Value> require)>;

// Run initialization for the environment.
//
// The |preload| function, usually used by embedders to inject scripts,
// will be run by Node.js before Node.js executes the entry point.
// The function is guaranteed to run before the user land module loader running
// any user code, so it is safe to assume that at this point, no user code has
// been run yet.
// The function will be executed with preload(process, require), and the passed
// require function has access to internal Node.js modules. There is no
// stability guarantee about the internals exposed to the internal require
// function. Expect breakages when updating Node.js versions if the embedder
// imports internal modules with the internal require function.
// Worker threads created in the environment will also respect The |preload|
// function, so make sure the function is thread-safe.
NODE_EXTERN v8::MaybeLocal<v8::Value> LoadEnvironment(
Environment* env,
StartExecutionCallback cb);
StartExecutionCallback cb,
EmbedderPreloadCallback preload = nullptr);
NODE_EXTERN v8::MaybeLocal<v8::Value> LoadEnvironment(
Environment* env, std::string_view main_script_source_utf8);
Environment* env,
std::string_view main_script_source_utf8,
EmbedderPreloadCallback preload = nullptr);
NODE_EXTERN void FreeEnvironment(Environment* env);

// Set a callback that is called when process.exit() is called from JS,
Expand Down
6 changes: 6 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1319,6 +1319,12 @@ void GetEmbedderOptions(const FunctionCallbackInfo<Value>& args) {
.IsNothing())
return;

if (ret->Set(context,
FIXED_ONE_BYTE_STRING(env->isolate(), "hasEmbedderPreload"),
Boolean::New(isolate, env->embedder_preload() != nullptr))
.IsNothing())
return;

args.GetReturnValue().Set(ret);
}

Expand Down
13 changes: 13 additions & 0 deletions src/node_snapshotable.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,17 @@ void SerializeSnapshotableObjects(Realm* realm,
});
}

void RunEmbedderPreload(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(env->embedder_preload());
CHECK_EQ(args.Length(), 2);
Local<Value> process_obj = args[0];
Local<Value> require_fn = args[1];
CHECK(process_obj->IsObject());
CHECK(require_fn->IsFunction());
env->embedder_preload()(env, process_obj, require_fn);
}

void CompileSerializeMain(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsString());
Local<String> filename = args[0].As<String>();
Expand Down Expand Up @@ -1541,6 +1552,7 @@ void CreatePerContextProperties(Local<Object> target,
void CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
SetMethod(isolate, target, "runEmbedderPreload", RunEmbedderPreload);
SetMethod(isolate, target, "compileSerializeMain", CompileSerializeMain);
SetMethod(isolate, target, "setSerializeCallback", SetSerializeCallback);
SetMethod(isolate, target, "setDeserializeCallback", SetDeserializeCallback);
Expand All @@ -1553,6 +1565,7 @@ void CreatePerIsolateProperties(IsolateData* isolate_data,
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(RunEmbedderPreload);
registry->Register(CompileSerializeMain);
registry->Register(SetSerializeCallback);
registry->Register(SetDeserializeCallback);
Expand Down
7 changes: 6 additions & 1 deletion src/node_worker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Worker::Worker(Environment* env,
thread_id_(AllocateEnvironmentThreadId()),
name_(name),
env_vars_(env_vars),
embedder_preload_(env->embedder_preload()),
snapshot_data_(snapshot_data) {
Debug(this, "Creating new worker instance with thread id %llu",
thread_id_.id);
Expand Down Expand Up @@ -386,8 +387,12 @@ void Worker::Run() {
}

Debug(this, "Created message port for worker %llu", thread_id_.id);
if (LoadEnvironment(env_.get(), StartExecutionCallback{}).IsEmpty())
if (LoadEnvironment(env_.get(),
StartExecutionCallback{},
std::move(embedder_preload_))
.IsEmpty()) {
return;
}

Debug(this, "Loaded environment for worker %llu", thread_id_.id);
}
Expand Down
1 change: 1 addition & 0 deletions src/node_worker.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class Worker : public AsyncWrap {

std::unique_ptr<MessagePortData> child_port_data_;
std::shared_ptr<KVStore> env_vars_;
EmbedderPreloadCallback embedder_preload_;

// A raw flag that is used by creator and worker threads to
// sync up on pre-mature termination of worker - while in the
Expand Down
28 changes: 28 additions & 0 deletions test/cctest/test_environment.cc
Original file line number Diff line number Diff line change
Expand Up @@ -778,3 +778,31 @@ TEST_F(EnvironmentTest, RequestInterruptAtExit) {

context->Exit();
}

TEST_F(EnvironmentTest, EmbedderPreload) {
v8::HandleScope handle_scope(isolate_);
v8::Local<v8::Context> context = node::NewContext(isolate_);
v8::Context::Scope context_scope(context);

node::EmbedderPreloadCallback preload = [](node::Environment* env,
v8::Local<v8::Value> process,
v8::Local<v8::Value> require) {
CHECK(process->IsObject());
CHECK(require->IsFunction());
process.As<v8::Object>()
->Set(env->context(),
v8::String::NewFromUtf8Literal(env->isolate(), "prop"),
v8::String::NewFromUtf8Literal(env->isolate(), "preload"))
.Check();
};

std::unique_ptr<node::Environment, decltype(&node::FreeEnvironment)> env(
node::CreateEnvironment(isolate_data_, context, {}, {}),
node::FreeEnvironment);

v8::Local<v8::Value> main_ret =
node::LoadEnvironment(env.get(), "return process.prop;", preload)
.ToLocalChecked();
node::Utf8Value main_ret_str(isolate_, main_ret);
EXPECT_EQ(std::string(*main_ret_str), "preload");
}

0 comments on commit 15b57b3

Please sign in to comment.