diff --git a/LICENSE b/LICENSE index b2f25c4e4b8974..2bd3fc96d95a63 100644 --- a/LICENSE +++ b/LICENSE @@ -2186,3 +2186,28 @@ The externally maintained libraries used by Node.js are: NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ + +- synchronous-worker, located at lib/internal/vm/node_realm.js, is licensed as follows: + """ + The MIT License (MIT) + + Copyright (c) 2020 Anna Henningsen + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ diff --git a/doc/api/cli.md b/doc/api/cli.md index 8e26ae16ae6233..de1f3401a5db3d 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -530,6 +530,22 @@ changes: Specify the `module` of a custom experimental [ECMAScript module loader][]. `module` may be any string accepted as an [`import` specifier][]. +### `--experimental-node-realm` + + + +Enable experimental support for `vm.NodeRealm`. + +### `--no-experimental-node-realm` + + + +Disable experimental support for `vm.NodeRealm`. + ### `--experimental-network-imports` + +The `createImport()` function was passed a value that was neither +a string nor a `URL`. + ### `ERR_WORKER_UNSUPPORTED_EXTENSION` diff --git a/doc/api/vm.md b/doc/api/vm.md index 1b8bb71715250c..b440bd946014a0 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -1573,6 +1573,84 @@ inside a `vm.Context`, functions passed to them will be added to global queues, which are shared by all contexts. Therefore, callbacks passed to those functions are not controllable through the timeout either. +### Class: `NodeRealm` + +> Stability: 1 - Experimental. Use `--experimental-node-realm` CLI flag to +> enable this feature. + + + +* Extends: {EventEmitter} + +A `NodeRealm` is effectively a Node.js environment that runs within the +same thread. It similar to a [ShadowRealm][], but with a few main differences: + +* `NodeRealm` supports loading both CommonJS and ES modules. +* Full interoperability between the host realm and the `NodeRealm` instance + is allowed. +* There is a deliberate `stop()` function. + +```mjs +import { NodeRealm } from 'node:vm'; +const nodeRealm = new NodeRealm(); +const { myAsyncFunction } = await nodeRealm.createImport(import.meta.url)('my-module'); +console.log(await myAsyncFunction()); +``` + +#### `new NodeRealm()` + + + +#### `nodeRealm.stop()` + + + +* Returns: + +This will render the inner Node.js instance unusable. +and is generally comparable to running `process.exit()`. + +This method returns a promise that will be resolved when all resources +associated with this Node.js instance are released. This promise resolves on +the event loop of the _outer_ Node.js instance. + +#### `nodeRealm.createImport(specifier)` + + + +* `specifier` {string} A module specifier like './file.js' or 'my-package' + +Creates a function that can be used for loading +modules inside the inner Node.js instance. + +#### `nodeRealm.globalThis` + + + +* Type: {Object} + +Returns a reference to the global object of the inner Node.js instance. + +#### `nodeRealm.process` + + + +* Type: {Object} + +Returns a reference to the `process` object of the inner Node.js instance. + [Cyclic Module Record]: https://tc39.es/ecma262/#sec-cyclic-module-records [ECMAScript Module Loader]: esm.md#modules-ecmascript-modules [Evaluate() concrete method]: https://tc39.es/ecma262/#sec-moduleevaluation @@ -1599,3 +1677,4 @@ are not controllable through the timeout either. [global object]: https://es5.github.io/#x15.1 [indirect `eval()` call]: https://es5.github.io/#x10.4.2 [origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin +[ShadowRealm]: https://github.com/tc39/proposal-shadowrealm diff --git a/doc/node.1 b/doc/node.1 index 90d0e42e90deb4..face062ee14301 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -166,6 +166,9 @@ to use as a custom module loader. .It Fl -experimental-network-imports Enable experimental support for loading modules using `import` over `https:`. . +.It Fl -experimental-node-realm +Enable experimental support for vm.NodeRealm. +. .It Fl -experimental-permission Enable the experimental permission model. . diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 18f4a2c42c39b1..e12d6fc2e1382b 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1706,6 +1706,7 @@ E('ERR_VM_MODULE_LINK_FAILURE', function(message, cause) { E('ERR_VM_MODULE_NOT_MODULE', 'Provided module is not an instance of Module', Error); E('ERR_VM_MODULE_STATUS', 'Module status %s', Error); +E('ERR_VM_NODE_REALM_INVALID_PARENT', 'createImport() must be called with a string or URL; received "%s"', Error); E('ERR_WASI_ALREADY_STARTED', 'WASI instance has already started', Error); E('ERR_WEBASSEMBLY_RESPONSE', 'WebAssembly response %s', TypeError); E('ERR_WORKER_INIT_FAILED', 'Worker initialization failure: %s', Error); diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index fc6d9ccf4f517b..61fe05490693de 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -67,6 +67,7 @@ function prepareExecution(options) { setupInspectorHooks(); setupWarningHandler(); setupFetch(); + setupNodeRealm(); setupWebCrypto(); setupCustomEvent(); setupCodeCoverage(); @@ -267,6 +268,16 @@ function setupFetch() { }); } +function setupNodeRealm() { + // Patch the vm module when --experimental-node-realm is on. + // Please update the comments in vm.js when this block changes. + if (getOptionValue('--experimental-node-realm')) { + const NodeRealm = require('internal/vm/node_realm'); + const vm = require('vm'); + vm.NodeRealm = NodeRealm; + } +} + // TODO(aduh95): move this to internal/bootstrap/web/* when the CLI flag is // removed. function setupWebCrypto() { diff --git a/lib/internal/vm/node_realm.js b/lib/internal/vm/node_realm.js new file mode 100644 index 00000000000000..0fee2234e4af40 --- /dev/null +++ b/lib/internal/vm/node_realm.js @@ -0,0 +1,133 @@ +'use strict'; + +// NodeRealm was originally a separate module developed by +// Anna Henningsen and published separately on npm as the +// synchronous-worker module under the MIT license. It has been +// incorporated into Node.js with Anna's permission. +// See the LICENSE file for LICENSE and copyright attribution. + +const { + Promise, +} = primordials; + +const { + emitExperimentalWarning, +} = require('internal/util'); + +const { + ERR_VM_NODE_REALM_INVALID_PARENT, +} = require('internal/errors').codes; + +const { + NodeRealm: NodeRealmImpl, +} = internalBinding('contextify'); + +const { URL } = require('internal/url'); +const EventEmitter = require('events'); +const { setTimeout } = require('timers'); +const { pathToFileURL } = require('url'); + +let debug = require('internal/util/debuglog').debuglog('noderealm', (fn) => { + debug = fn; +}); + +class NodeRealm extends EventEmitter { + #handle = undefined; + #process = undefined; + #global = undefined; + #stoppedPromise = undefined; + #loader = undefined; + + constructor() { + super(); + emitExperimentalWarning('NodeRealm'); + this.#handle = new NodeRealmImpl(); + this.#handle.onexit = (code) => { + this.stop(); + this.emit('exit', code); + }; + try { + this.#handle.start(); + this.#handle.load((process, nativeRequire, globalThis) => { + this.#process = process; + this.#global = globalThis; + process.on('uncaughtException', (err) => { + if (process.listenerCount('uncaughtException') === 1) { + // If we are stopping, silence all errors + if (!this.#stoppedPromise) { + this.emit('error', err); + } + process.exit(1); + } + }); + }); + + const req = this.#handle.internalRequire(); + this.#loader = req('internal/process/esm_loader').esmLoader; + } catch (err) { + this.#handle.stop(); + throw err; + } + } + + /** + * @returns {Promise} + */ + async stop() { + // TODO(@mcollina): add support for AbortController, we want to abort this, + // or add a timeout. + return this.#stoppedPromise ??= new Promise((resolve) => { + const tryClosing = () => { + const closed = this.#handle.tryCloseAllHandles(); + debug('closed %d handles', closed); + if (closed > 0) { + // This is an active wait for the handles to close. + // We might want to change this in the future to use a callback, + // but at this point it seems like a premature optimization. + // We cannot unref() this because we need to shut this down properly. + // TODO(@mcollina): refactor to use a close callback + setTimeout(tryClosing, 100); + } else { + + this.#handle.stop(); + resolve(); + } + }; + + // We use setTimeout instead of setImmediate because it runs in a different + // phase of the event loop. This is important because the immediate queue + // would crash if the environment it refers to has been already closed. + // We cannot unref() this because we need to shut this down properly. + setTimeout(tryClosing, 100); + }); + } + + get process() { + return this.#process; + } + + get globalThis() { + return this.#global; + } + + /** + * @param {string|URL} parentURL + */ + createImport(parentURL) { + if (typeof parentURL === 'string') { + if (parentURL.indexOf('file://') === 0) { + parentURL = new URL(parentURL); + } else { + parentURL = pathToFileURL(parentURL); + } + } else if (!(parentURL instanceof URL)) { + throw new ERR_VM_NODE_REALM_INVALID_PARENT(parentURL); + } + + return (specifiers, importAssertions) => { + return this.#loader.import(specifiers, parentURL, importAssertions || {}); + }; + } +} + +module.exports = NodeRealm; diff --git a/lib/vm.js b/lib/vm.js index 1fdea8433495b2..798603ab1daee7 100644 --- a/lib/vm.js +++ b/lib/vm.js @@ -343,3 +343,5 @@ module.exports = { // The vm module is patched to include vm.Module, vm.SourceTextModule // and vm.SyntheticModule in the pre-execution phase when // --experimental-vm-modules is on. +// The vm module is also patched to include vm.NodeRealm in the +// pre-execution phase when --experimental-node-realm is on. diff --git a/src/env.cc b/src/env.cc index ff8d3952cf20a3..6e0429b86cc6c5 100644 --- a/src/env.cc +++ b/src/env.cc @@ -1019,21 +1019,40 @@ void Environment::CleanupHandles() { RunAndClearNativeImmediates(true /* skip unrefed SetImmediate()s */); - for (ReqWrapBase* request : req_wrap_queue_) + CleanupHandlesNoUvRun(); + + while (handle_cleanup_waiting_ != 0 || request_waiting_ != 0 || + !handle_wrap_queue_.IsEmpty()) { + uv_run(event_loop(), UV_RUN_ONCE); + } +} + +int Environment::CleanupHandlesNoUvRun() { + { + Mutex::ScopedLock lock(native_immediates_threadsafe_mutex_); + task_queues_async_initialized_ = false; + } + + int count = 0; + + for (ReqWrapBase* request : req_wrap_queue_) { + count++; request->Cancel(); + } - for (HandleWrap* handle : handle_wrap_queue_) + for (HandleWrap* handle : handle_wrap_queue_) { + count++; handle->Close(); + } - for (HandleCleanup& hc : handle_cleanup_queue_) + for (HandleCleanup& hc : handle_cleanup_queue_) { + count++; hc.cb_(this, hc.handle_, hc.arg_); + } + handle_cleanup_queue_.clear(); - while (handle_cleanup_waiting_ != 0 || - request_waiting_ != 0 || - !handle_wrap_queue_.IsEmpty()) { - uv_run(event_loop(), UV_RUN_ONCE); - } + return count; } void Environment::StartProfilerIdleNotifier() { diff --git a/src/env.h b/src/env.h index 5359436be31e76..413b831752c4b2 100644 --- a/src/env.h +++ b/src/env.h @@ -627,6 +627,7 @@ class Environment : public MemoryRetainer { void RegisterHandleCleanups(); void CleanupHandles(); + int CleanupHandlesNoUvRun(); void Exit(ExitCode code); void ExitEnv(StopFlags::Flags flags); @@ -857,6 +858,9 @@ class Environment : public MemoryRetainer { inline HandleWrapQueue* handle_wrap_queue() { return &handle_wrap_queue_; } inline ReqWrapQueue* req_wrap_queue() { return &req_wrap_queue_; } + inline int handle_cleanup_waiting() const { + return handle_cleanup_waiting_; + } // https://w3c.github.io/hr-time/#dfn-time-origin inline uint64_t time_origin() { diff --git a/src/env_properties.h b/src/env_properties.h index 320d08d22577c8..36c64d9b46e4a2 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -366,6 +366,7 @@ V(socketaddress_constructor_template, v8::FunctionTemplate) \ V(streambaseentry_ctor_template, v8::FunctionTemplate) \ V(streambaseoutputstream_constructor_template, v8::ObjectTemplate) \ + V(node_realm_constructor_template, v8::FunctionTemplate) \ V(streamentry_ctor_template, v8::FunctionTemplate) \ V(streamentry_opaque_ctor_template, v8::FunctionTemplate) \ V(qlogoutputstream_constructor_template, v8::ObjectTemplate) \ diff --git a/src/node_contextify.cc b/src/node_contextify.cc index 7f9f71ba74223a..0a612c7fc73ab8 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -21,6 +21,7 @@ #include "node_contextify.h" +#include "async_wrap-inl.h" #include "base_object-inl.h" #include "memory_tracker-inl.h" #include "module_wrap.h" @@ -41,13 +42,16 @@ using v8::Array; using v8::ArrayBufferView; using v8::Boolean; using v8::Context; +using v8::DeserializeInternalFieldsCallback; using v8::EscapableHandleScope; using v8::Function; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; +using v8::Global; using v8::HandleScope; using v8::IndexedPropertyHandlerConfiguration; using v8::Int32; +using v8::Integer; using v8::Isolate; using v8::Just; using v8::Local; @@ -1391,6 +1395,260 @@ void MicrotaskQueueWrap::RegisterExternalReferences( registry->Register(New); } +Local NodeRealm::GetConstructorTemplate( + IsolateData* isolate_data) { + Local tmpl = + isolate_data->node_realm_constructor_template(); + if (tmpl.IsEmpty()) { + Isolate* isolate = isolate_data->isolate(); + tmpl = NewFunctionTemplate(isolate, New); + tmpl->Inherit(AsyncWrap::GetConstructorTemplate(isolate_data)); + tmpl->InstanceTemplate()->SetInternalFieldCount(1); + + SetProtoMethod(isolate, tmpl, "start", Start); + SetProtoMethod(isolate, tmpl, "load", Load); + SetProtoMethod(isolate, tmpl, "stop", Stop); + SetProtoMethod(isolate, tmpl, "signalStop", SignalStop); + SetProtoMethod(isolate, tmpl, "tryCloseAllHandles", TryCloseAllHandles); + SetProtoMethod(isolate, tmpl, "internalRequire", InternalRequire); + + isolate_data->set_node_realm_constructor_template(tmpl); + } + return tmpl; +} + +void NodeRealm::CreatePerIsolateProperties( + IsolateData* isolate_data, v8::Local target) { + SetConstructorFunction(isolate_data->isolate(), + target, + "NodeRealm", + GetConstructorTemplate(isolate_data), + SetConstructorFunctionFlag::NONE); +} + +void NodeRealm::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(New); + registry->Register(Start); + registry->Register(Load); + registry->Register(SignalStop); + registry->Register(Stop); + registry->Register(TryCloseAllHandles); + registry->Register(InternalRequire); +} + +NodeRealm::NodeRealmScope::NodeRealmScope(NodeRealm* w) + : EscapableHandleScope(w->isolate_), + Scope(w->context()), + Isolate::SafeForTerminationScope(w->isolate_), + w_(w), + orig_can_be_terminated_(w->can_be_terminated_) { + w_->can_be_terminated_ = true; +} + +NodeRealm::NodeRealmScope::~NodeRealmScope() { + w_->can_be_terminated_ = orig_can_be_terminated_; +} + +Local NodeRealm::context() const { + return context_.Get(isolate_); +} + +NodeRealm::NodeRealm(Environment* env, Local object) + : isolate_(env->isolate()), wrap_(env->isolate(), object) { + AddEnvironmentCleanupHook(env->isolate(), CleanupHook, this); + object->SetAlignedPointerInInternalField(0, this); + + Local outer_context = env->context(); + outer_context_.Reset(env->isolate(), outer_context); +} + +NodeRealm* NodeRealm::Unwrap(const FunctionCallbackInfo& args) { + Local value = args.This(); + if (!value->IsObject() || value.As()->InternalFieldCount() < 1) { + THROW_ERR_INVALID_THIS(Environment::GetCurrent(args.GetIsolate())); + return nullptr; + } + return static_cast( + value.As()->GetAlignedPointerFromInternalField(0)); +} + +void NodeRealm::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + new NodeRealm(env, args.This()); +} + +void NodeRealm::Start(const FunctionCallbackInfo& args) { + NodeRealm* self = Unwrap(args); + if (self == nullptr) return; + self->Start(); +} + +void NodeRealm::TryCloseAllHandles(const FunctionCallbackInfo& args) { + auto count = 0; + NodeRealm* self = Unwrap(args); + if (self == nullptr) return; + self->env_->async_hooks()->clear_async_id_stack(); + count += self->env_->CleanupHandlesNoUvRun(); + args.GetReturnValue().Set(v8::Number::New(self->isolate_, count)); +} + +void NodeRealm::InternalRequire(const FunctionCallbackInfo& args) { + NodeRealm* self = Unwrap(args); + Local require = + Realm::GetCurrent(self->context())->builtin_module_require(); + args.GetReturnValue().Set(require); +} + +void NodeRealm::Stop(const FunctionCallbackInfo& args) { + NodeRealm* self = Unwrap(args); + if (self == nullptr) return; + self->Stop(true); +} + +void NodeRealm::SignalStop(const FunctionCallbackInfo& args) { + NodeRealm* self = Unwrap(args); + if (self == nullptr) return; + self->SignalStop(); + args.GetIsolate()->CancelTerminateExecution(); +} + +void NodeRealm::Load(const FunctionCallbackInfo& args) { + NodeRealm* self = Unwrap(args); + if (self == nullptr) return; + if (!args[0]->IsFunction()) { + return THROW_ERR_INVALID_ARG_TYPE( + Environment::GetCurrent(args), + "The load() argument must be a function."); + } + Local result; + if (self->Load(args[0].As()).ToLocal(&result)) { + args.GetReturnValue().Set(result); + } +} + +void NodeRealm::Start() { + signaled_stop_ = false; + Local outer_context = outer_context_.Get(isolate_); + Environment* outer_env = GetCurrentEnvironment(outer_context); + assert(outer_env != nullptr); + uv_loop_t* loop = GetCurrentEventLoop(isolate_); + assert(loop != nullptr); + + MicrotaskQueue* microtask_queue = + outer_context_.Get(isolate_)->GetMicrotaskQueue(); + + Local context = Context::New( + isolate_, + nullptr /* extensions */, + MaybeLocal() /* global_template */, + MaybeLocal() /* global_value */, + DeserializeInternalFieldsCallback() /* internal_fields_deserializer */, + microtask_queue); + context->SetSecurityToken(outer_context->GetSecurityToken()); + if (context.IsEmpty() || !InitializeContext(context).FromMaybe(false)) { + return; + } + + context_.Reset(isolate_, context); + Context::Scope context_scope(context); + isolate_data_ = CreateIsolateData( + isolate_, + loop, + GetMultiIsolatePlatform(outer_env), + GetArrayBufferAllocator(GetEnvironmentIsolateData(outer_env))); + assert(isolate_data_ != nullptr); + ThreadId thread_id = AllocateEnvironmentThreadId(); + auto inspector_parent_handle = + GetInspectorParentHandle(outer_env, thread_id, "file:///node_realm.js"); + env_ = CreateEnvironment(isolate_data_, + context, + {}, + {}, + static_cast( + EnvironmentFlags::kTrackUnmanagedFds), + thread_id, + std::move(inspector_parent_handle)); + assert(env_ != nullptr); + SetProcessExitHandler(env_, + [this](Environment* env, int code) { OnExit(code); }); +} + +void NodeRealm::OnExit(int code) { + HandleScope handle_scope(isolate_); + Local self = wrap_.Get(isolate_); + Local outer_context = outer_context_.Get(isolate_); + Context::Scope context_scope(outer_context); + Isolate::SafeForTerminationScope termination_scope(isolate_); + Local onexit_v; + if (!self->Get(outer_context, String::NewFromUtf8Literal(isolate_, "onexit")) + .ToLocal(&onexit_v) || + !onexit_v->IsFunction()) { + return; + } + Local args[] = {Integer::New(isolate_, code)}; + USE(onexit_v.As()->Call(outer_context, self, 1, args)); + SignalStop(); +} + +void NodeRealm::SignalStop() { + signaled_stop_ = true; + if (env_ != nullptr && can_be_terminated_) { + node::Stop(env_); + } +} + +void NodeRealm::Stop(bool may_throw) { + if (env_ != nullptr) { + if (!signaled_stop_) { + SignalStop(); + isolate_->CancelTerminateExecution(); + } + FreeEnvironment(env_); + env_ = nullptr; + } + if (isolate_data_ != nullptr) { + FreeIsolateData(isolate_data_); + isolate_data_ = nullptr; + } + + context_.Reset(); + outer_context_.Reset(); + microtask_queue_.reset(); + + RemoveEnvironmentCleanupHook(isolate_, CleanupHook, this); + if (!wrap_.IsEmpty()) { + HandleScope handle_scope(isolate_); + wrap_.Get(isolate_)->SetAlignedPointerInInternalField(0, nullptr); + } + wrap_.Reset(); + delete this; +} + +MaybeLocal NodeRealm::Load(Local callback) { + if (env_ == nullptr || signaled_stop_) { + Environment* env = Environment::GetCurrent(isolate_); + THROW_ERR_INVALID_STATE(env, "NodeRealm not initialized"); + return MaybeLocal(); + } + + NodeRealmScope worker_scope(this); + return worker_scope.EscapeMaybe( + LoadEnvironment(env_, [&](const StartExecutionCallbackInfo& info) { + Local argv[] = { + info.process_object, info.native_require, context()->Global()}; + return callback->Call(context(), Null(isolate_), 3, argv); + })); +} + +void NodeRealm::CleanupHook(void* arg) { + static_cast(arg)->Stop(false); +} + +void NodeRealm::MemoryInfo(MemoryTracker* tracker) const { + // TODO(@jasnell): Implement +} + void CreatePerIsolateProperties(IsolateData* isolate_data, Local ctor) { Isolate* isolate = isolate_data->isolate(); @@ -1398,6 +1656,7 @@ void CreatePerIsolateProperties(IsolateData* isolate_data, ContextifyContext::CreatePerIsolateProperties(isolate_data, target); ContextifyScript::CreatePerIsolateProperties(isolate_data, target); MicrotaskQueueWrap::CreatePerIsolateProperties(isolate_data, target); + NodeRealm::CreatePerIsolateProperties(isolate_data, target); SetMethod(isolate, target, "startSigintWatchdog", StartSigintWatchdog); SetMethod(isolate, target, "stopSigintWatchdog", StopSigintWatchdog); @@ -1458,6 +1717,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(StopSigintWatchdog); registry->Register(WatchdogHasPendingSigint); registry->Register(MeasureMemory); + + NodeRealm::RegisterExternalReferences(registry); } } // namespace contextify } // namespace node diff --git a/src/node_contextify.h b/src/node_contextify.h index 3160160521e0fe..ad3a43eb8a881f 100644 --- a/src/node_contextify.h +++ b/src/node_contextify.h @@ -4,6 +4,7 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #include "base_object-inl.h" +#include "memory_tracker-inl.h" #include "node_context_data.h" #include "node_errors.h" @@ -210,6 +211,69 @@ v8::Maybe StoreCodeCacheResult( bool produce_cached_data, std::unique_ptr new_cached_data); +class NodeRealm final : public node::MemoryRetainer { + public: + static v8::Local GetConstructorTemplate( + IsolateData* isolate_data); + static void CreatePerIsolateProperties(IsolateData* isolate_data, + v8::Local target); + static void RegisterExternalReferences( + node::ExternalReferenceRegistry* registry); + + NodeRealm(node::Environment* env, v8::Local obj); + + static void New(const v8::FunctionCallbackInfo& args); + static void Start(const v8::FunctionCallbackInfo& args); + static void Load(const v8::FunctionCallbackInfo& args); + static void SignalStop(const v8::FunctionCallbackInfo& args); + static void Stop(const v8::FunctionCallbackInfo& args); + static void InternalRequire(const v8::FunctionCallbackInfo& args); + static void TryCloseAllHandles( + const v8::FunctionCallbackInfo& args); + + struct NodeRealmScope : public v8::EscapableHandleScope, + public v8::Context::Scope, + public v8::Isolate::SafeForTerminationScope { + public: + explicit NodeRealmScope(NodeRealm* w); + ~NodeRealmScope(); + + private: + NodeRealm* w_; + bool orig_can_be_terminated_; + }; + + v8::Local context() const; + + void MemoryInfo(node::MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(NodeRealm) + SET_SELF_SIZE(NodeRealm) + + private: + static NodeRealm* Unwrap(const v8::FunctionCallbackInfo& arg); + static void CleanupHook(void* arg); + void OnExit(int code); + + void Start(); + v8::MaybeLocal Load(v8::Local callback); + v8::MaybeLocal RunInCallbackScope( + v8::Local callback); + void RunLoop(uv_run_mode mode); + void SignalStop(); + void Stop(bool may_throw); + + v8::Isolate* isolate_; + v8::Global wrap_; + + std::unique_ptr microtask_queue_; + v8::Global outer_context_; + v8::Global context_; + node::IsolateData* isolate_data_ = nullptr; + node::Environment* env_ = nullptr; + bool signaled_stop_ = false; + bool can_be_terminated_ = false; +}; + } // namespace contextify } // namespace node diff --git a/src/node_options.cc b/src/node_options.cc index 4e3633e5b5b8a6..277568b006d0cb 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -442,6 +442,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { &EnvironmentOptions::experimental_vm_modules, kAllowedInEnvvar); AddOption("--experimental-worker", "", NoOp{}, kAllowedInEnvvar); + AddOption("--experimental-node-realm", + "experimental NodeRealm support", + &EnvironmentOptions::experimental_node_realm, + kAllowedInEnvironment, + false); AddOption("--experimental-report", "", NoOp{}, kAllowedInEnvvar); AddOption( "--experimental-wasi-unstable-preview1", "", NoOp{}, kAllowedInEnvvar); diff --git a/src/node_options.h b/src/node_options.h index e0d338f203b0c4..15dbd5ab47b48f 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -128,6 +128,8 @@ class EnvironmentOptions : public Options { bool experimental_repl_await = true; bool experimental_vm_modules = false; bool expose_internals = false; + bool experimental_node_realm = false; + bool force_node_api_uncaught_exceptions_policy = false; bool frozen_intrinsics = false; int64_t heap_snapshot_near_heap_limit = 0; diff --git a/test/parallel/test-node-realm.js b/test/parallel/test-node-realm.js new file mode 100644 index 00000000000000..f57f359fa8535c --- /dev/null +++ b/test/parallel/test-node-realm.js @@ -0,0 +1,142 @@ +// Flags: --experimental-node-realm +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const { strictEqual } = assert; +const { pathToFileURL } = require('node:url'); + +const { + NodeRealm, +} = require('node:vm'); + +// Properly handles timers that are about to expire when FreeEnvironment() is called on +// a shared event loop +(async function() { + const w = new NodeRealm(); + + setImmediate(() => { + setTimeout(() => {}, 20); + const now = Date.now(); + while (Date.now() - now < 30); + }); + await w.stop(); +})().then(common.mustCall()); + +(async function() { + const w = new NodeRealm(); + + setImmediate(() => { + setImmediate(() => { + setImmediate(() => {}); + }); + }); + await w.stop(); +})().then(common.mustCall()); + +(async function() { + const w = new NodeRealm(); + + assert.throws( + () => { + w.createImport(undefined); + }, + { + name: 'Error', + message: 'createImport() must be called with a string or URL; received "undefined"', + code: 'ERR_VM_NODE_REALM_INVALID_PARENT', + }, + ); + + await w.stop(); +})().then(common.mustCall()); + +(async function() { + const w = new NodeRealm(); + const cb = common.mustCall(); + const _import = w.createImport(__filename); + const vm = await _import('vm'); + const fs = await _import('fs'); + + vm.runInThisContext(`({ fs, cb }) => { + const stream = fs.createReadStream('${__filename}'); + stream.on('open', () => { + cb() + }) + + setTimeout(() => {}, 200000); + }`)({ fs, cb }); + + await w.stop(); +})().then(common.mustCall()); + +(async function() { + const w = new NodeRealm(); + const cb = common.mustCall(); + const _import = await w.createImport(__filename); + const vm = await _import('vm'); + const fs = await _import('fs'); + + vm.runInThisContext(`({ fs, cb }) => { + const stream = fs.createReadStream('${__filename}'); + stream.on('open', () => { + cb() + }) + + setTimeout(() => {}, 200000); + }`)({ fs, cb }); + + await w.stop(); +})().then(common.mustCall()); + +// Globals are isolated +(async function() { + const w = new NodeRealm(); + const _import = w.createImport(__filename); + const vm = await _import('vm'); + + strictEqual(globalThis.foo, undefined); + vm.runInThisContext('globalThis.foo = 42'); + strictEqual(globalThis.foo, undefined); + strictEqual(w.globalThis.foo, 42); + + await w.stop(); +})().then(common.mustCall()); + +(async function() { + const w = new NodeRealm(); + const cb = common.mustCall(); + const _import = await w.createImport(pathToFileURL(__filename)); + const vm = await _import('vm'); + const fs = await _import('fs'); + + vm.runInThisContext(`({ fs, cb }) => { + const stream = fs.createReadStream('${__filename}'); + stream.on('open', () => { + cb() + }) + + setTimeout(() => {}, 200000); + }`)({ fs, cb }); + + await w.stop(); +})().then(common.mustCall()); + +(async function() { + const w = new NodeRealm(); + const cb = common.mustCall(); + const _import = await w.createImport(pathToFileURL(__filename).toString()); + const vm = await _import('vm'); + const fs = await _import('fs'); + + vm.runInThisContext(`({ fs, cb }) => { + const stream = fs.createReadStream('${__filename}'); + stream.on('open', () => { + cb() + }) + + setTimeout(() => {}, 200000); + }`)({ fs, cb }); + + await w.stop(); +})().then(common.mustCall()); diff --git a/tools/license-builder.sh b/tools/license-builder.sh index aaedfeed5c7b1b..cd2240f4388255 100755 --- a/tools/license-builder.sh +++ b/tools/license-builder.sh @@ -148,4 +148,7 @@ addlicense "node-fs-extra" "lib/internal/fs/cp" "$licenseText" addlicense "base64" "deps/base64/base64/" "$(cat "${rootdir}/deps/base64/base64/LICENSE" || true)" +licenseText="$(curl -sL https://raw.githubusercontent.com/addaleax/synchronous-worker/HEAD/LICENSE)" +addlicense "synchronous-worker" "lib/internal/vm/node_realm.js" "$licenseText" + mv "$tmplicense" "$licensefile"