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::CreateEnvironment 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.
  • Loading branch information
zcbenz committed Jan 23, 2024
1 parent f820efe commit 84d5f00
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 7 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 @@ -186,6 +186,9 @@ function setupUserModules(forceDefaultLoader = false) {
initializeESMLoader(forceDefaultLoader);
const CJSLoader = require('internal/modules/cjs/loader');
assert(!CJSLoader.hasLoadedAnyUserCJSModule);
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 @@ -754,6 +757,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
6 changes: 4 additions & 2 deletions src/api/environment.cc
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,8 @@ Environment* CreateEnvironment(
const std::vector<std::string>& exec_args,
EnvironmentFlags::Flags flags,
ThreadId thread_id,
std::unique_ptr<InspectorParentHandle> inspector_parent_handle) {
std::unique_ptr<InspectorParentHandle> inspector_parent_handle,
EmbedderPreloadCallback preload) {
Isolate* isolate = isolate_data->isolate();

Isolate::Scope isolate_scope(isolate);
Expand All @@ -452,7 +453,8 @@ Environment* CreateEnvironment(
exec_args,
env_snapshot_info,
flags,
thread_id);
thread_id,
std::move(preload));
CHECK_NOT_NULL(env);

if (use_snapshot) {
Expand Down
4 changes: 4 additions & 0 deletions src/env-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,10 @@ inline void Environment::set_embedder_entry_point(StartExecutionCallback&& fn) {
embedder_entry_point_ = std::move(fn);
}

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

inline double Environment::new_async_id() {
async_hooks()->async_id_fields()[AsyncHooks::kAsyncIdCounter] += 1;
return async_hooks()->async_id_fields()[AsyncHooks::kAsyncIdCounter];
Expand Down
6 changes: 4 additions & 2 deletions src/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,8 @@ Environment::Environment(IsolateData* isolate_data,
const std::vector<std::string>& exec_args,
const EnvSerializeInfo* env_info,
EnvironmentFlags::Flags flags,
ThreadId thread_id)
ThreadId thread_id,
EmbedderPreloadCallback preload)
: isolate_(isolate),
isolate_data_(isolate_data),
async_hooks_(isolate, MAYBE_FIELD_PTR(env_info, async_hooks)),
Expand All @@ -802,7 +803,8 @@ Environment::Environment(IsolateData* isolate_data,
flags_(flags),
thread_id_(thread_id.id == static_cast<uint64_t>(-1)
? AllocateEnvironmentThreadId().id
: thread_id.id) {
: thread_id.id),
embedder_preload_(std::move(preload)) {
constexpr bool is_shared_ro_heap =
#ifdef NODE_V8_SHARED_RO_HEAP
true;
Expand Down
6 changes: 5 additions & 1 deletion src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,8 @@ class Environment : public MemoryRetainer {
const std::vector<std::string>& exec_args,
const EnvSerializeInfo* env_info,
EnvironmentFlags::Flags flags,
ThreadId thread_id);
ThreadId thread_id,
EmbedderPreloadCallback preload);
void InitializeMainContext(v8::Local<v8::Context> context,
const EnvSerializeInfo* env_info);
~Environment() override;
Expand Down Expand Up @@ -1002,6 +1003,8 @@ class Environment : public MemoryRetainer {
inline const StartExecutionCallback& embedder_entry_point() const;
inline void set_embedder_entry_point(StartExecutionCallback&& fn);

inline const EmbedderPreloadCallback& embedder_preload() const;

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

Expand Down Expand Up @@ -1208,6 +1211,7 @@ class Environment : public MemoryRetainer {

builtins::BuiltinLoader builtin_loader_;
StartExecutionCallback embedder_entry_point_;
EmbedderPreloadCallback embedder_preload_;

// Used by allocate_managed_buffer() and release_managed_buffer() to keep
// track of the BackingStore for a given pointer.
Expand Down
15 changes: 14 additions & 1 deletion src/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -691,19 +691,32 @@ struct InspectorParentHandle {
virtual ~InspectorParentHandle() = default;
};

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

// TODO(addaleax): Maybe move per-Environment options parsing here.
// Returns nullptr when the Environment cannot be created e.g. there are
// pending JavaScript exceptions.
// `context` may be empty if an `EmbedderSnapshotData` instance was provided
// to `NewIsolate()` and `CreateIsolateData()`.
//
// The |preload| function will run before executing the entry point, which
// is usually used by embedders to inject scripts. The function is executed
// with preload(process, require), and the passed require function has access
// to internal Node.js modules. The |preload| function is inherited by worker
// threads and thus will run in work threads, so make sure the function is
// thread-safe.
NODE_EXTERN Environment* CreateEnvironment(
IsolateData* isolate_data,
v8::Local<v8::Context> context,
const std::vector<std::string>& args,
const std::vector<std::string>& exec_args,
EnvironmentFlags::Flags flags = EnvironmentFlags::kDefaultFlags,
ThreadId thread_id = {} /* allocates a thread id automatically */,
std::unique_ptr<InspectorParentHandle> inspector_parent_handle = {});
std::unique_ptr<InspectorParentHandle> inspector_parent_handle = {},
EmbedderPreloadCallback preload = nullptr);

// Returns a handle that can be passed to `LoadEnvironment()`, making the
// child Environment accessible to the inspector as if it were a Node.js Worker.
Expand Down
6 changes: 6 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1304,6 +1304,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
9 changes: 9 additions & 0 deletions src/node_snapshotable.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1453,6 +1453,13 @@ static void RunEmbedderEntryPoint(const FunctionCallbackInfo<Value>& args) {
}
}

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

void CompileSerializeMain(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsString());
Local<String> filename = args[0].As<String>();
Expand Down Expand Up @@ -1577,6 +1584,7 @@ void CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
SetMethod(isolate, target, "runEmbedderEntryPoint", RunEmbedderEntryPoint);
SetMethod(isolate, target, "runEmbedderPreload", RunEmbedderPreload);
SetMethod(isolate, target, "compileSerializeMain", CompileSerializeMain);
SetMethod(isolate, target, "setSerializeCallback", SetSerializeCallback);
SetMethod(isolate, target, "setDeserializeCallback", SetDeserializeCallback);
Expand All @@ -1590,6 +1598,7 @@ void CreatePerIsolateProperties(IsolateData* isolate_data,

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(RunEmbedderEntryPoint);
registry->Register(RunEmbedderPreload);
registry->Register(CompileSerializeMain);
registry->Register(SetSerializeCallback);
registry->Register(SetDeserializeCallback);
Expand Down
4 changes: 3 additions & 1 deletion src/node_worker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,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 @@ -366,7 +367,8 @@ void Worker::Run() {
std::move(exec_argv_),
static_cast<EnvironmentFlags::Flags>(environment_flags_),
thread_id_,
std::move(inspector_parent_handle_)));
std::move(inspector_parent_handle_),
std::move(embedder_preload_)));
if (is_stopped()) return;
CHECK_NOT_NULL(env_);
env_->set_env_vars(std::move(env_vars_));
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::EnvironmentFlags::kDefaultFlags, {}, {},
preload),
node::FreeEnvironment);

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

0 comments on commit 84d5f00

Please sign in to comment.