From f0ded20b58eeb9537fba6ce1f3e5c12dc602c7d7 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 17 May 2018 23:46:19 +0200 Subject: [PATCH 01/26] src: cleanup per-isolate state on platform on isolate unregister Clean up once all references to an `Isolate*` are gone from the `NodePlatform`, rather than waiting for the `PerIsolatePlatformData` struct to be deleted since there may be cyclic references between that struct and the individual tasks. --- src/node_platform.cc | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/node_platform.cc b/src/node_platform.cc index 2885c72ed71213..6fc83950d3083e 100644 --- a/src/node_platform.cc +++ b/src/node_platform.cc @@ -82,12 +82,14 @@ void PerIsolatePlatformData::PostIdleTask(std::unique_ptr task) { } void PerIsolatePlatformData::PostTask(std::unique_ptr task) { + CHECK_NE(flush_tasks_, nullptr); foreground_tasks_.Push(std::move(task)); uv_async_send(flush_tasks_); } void PerIsolatePlatformData::PostDelayedTask( std::unique_ptr task, double delay_in_seconds) { + CHECK_NE(flush_tasks_, nullptr); std::unique_ptr delayed(new DelayedTask()); delayed->task = std::move(task); delayed->platform_data = shared_from_this(); @@ -97,6 +99,13 @@ void PerIsolatePlatformData::PostDelayedTask( } PerIsolatePlatformData::~PerIsolatePlatformData() { + Shutdown(); +} + +void PerIsolatePlatformData::Shutdown() { + if (flush_tasks_ == nullptr) + return; + while (FlushForegroundTasksInternal()) {} CancelPendingDelayedTasks(); @@ -104,6 +113,7 @@ PerIsolatePlatformData::~PerIsolatePlatformData() { [](uv_handle_t* handle) { delete reinterpret_cast(handle); }); + flush_tasks_ = nullptr; } void PerIsolatePlatformData::ref() { @@ -144,6 +154,7 @@ void NodePlatform::UnregisterIsolate(IsolateData* isolate_data) { std::shared_ptr existing = per_isolate_[isolate]; CHECK(existing); if (existing->unref() == 0) { + existing->Shutdown(); per_isolate_.erase(isolate); } } From 2bb055b1c993e3f84d01622e634400762199d360 Mon Sep 17 00:00:00 2001 From: Daniel Bevenius Date: Wed, 23 May 2018 13:10:53 +0200 Subject: [PATCH 02/26] src: remove unused fields isolate_ Currently the following compiler warnings are generated: In file included from ../src/node_platform.cc:1: ../src/node_platform.h:83:16: warning: private field 'isolate_' is not used [-Wunused-private-field] v8::Isolate* isolate_; ^ 1 warning generated. This commit removes these unused private member. --- src/node_platform.cc | 2 +- src/node_platform.h | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/node_platform.cc b/src/node_platform.cc index 6fc83950d3083e..f2407f5be2d3c8 100644 --- a/src/node_platform.cc +++ b/src/node_platform.cc @@ -65,7 +65,7 @@ size_t BackgroundTaskRunner::NumberOfAvailableBackgroundThreads() const { PerIsolatePlatformData::PerIsolatePlatformData( v8::Isolate* isolate, uv_loop_t* loop) - : isolate_(isolate), loop_(loop) { + : loop_(loop) { flush_tasks_ = new uv_async_t(); CHECK_EQ(0, uv_async_init(loop, flush_tasks_, FlushTasks)); flush_tasks_->data = static_cast(this); diff --git a/src/node_platform.h b/src/node_platform.h index 8f6ff89f491fe3..cf0809ad1f673b 100644 --- a/src/node_platform.h +++ b/src/node_platform.h @@ -80,7 +80,6 @@ class PerIsolatePlatformData : static void RunForegroundTask(uv_timer_t* timer); int ref_count_ = 1; - v8::Isolate* isolate_; uv_loop_t* const loop_; uv_async_t* flush_tasks_ = nullptr; TaskQueue foreground_tasks_; From e7695ccc099c3e4803bb5779624ade90a249a627 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sun, 10 Sep 2017 05:05:48 +0200 Subject: [PATCH 03/26] src: simplify handle closing Remove one extra closing state and use a smart pointer for deleting `HandleWrap`s. --- src/handle_wrap.cc | 16 +++++++--------- src/handle_wrap.h | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/handle_wrap.cc b/src/handle_wrap.cc index 20356b94a5775a..bbc124ff69dc89 100644 --- a/src/handle_wrap.cc +++ b/src/handle_wrap.cc @@ -67,7 +67,7 @@ void HandleWrap::Close(const FunctionCallbackInfo& args) { wrap->Close(args[0]); } -void HandleWrap::Close(v8::Local close_callback) { +void HandleWrap::Close(Local close_callback) { if (state_ != kInitialized) return; @@ -77,8 +77,7 @@ void HandleWrap::Close(v8::Local close_callback) { if (!close_callback.IsEmpty() && close_callback->IsFunction()) { object()->Set(env()->context(), env()->onclose_string(), close_callback) - .FromJust(); - state_ = kClosingWithCallback; + .FromMaybe(false); } } @@ -109,24 +108,23 @@ HandleWrap::HandleWrap(Environment* env, void HandleWrap::OnClose(uv_handle_t* handle) { - HandleWrap* wrap = static_cast(handle->data); + std::unique_ptr wrap { static_cast(handle->data) }; Environment* env = wrap->env(); HandleScope scope(env->isolate()); Context::Scope context_scope(env->context()); // The wrap object should still be there. CHECK_EQ(wrap->persistent().IsEmpty(), false); - CHECK(wrap->state_ >= kClosing && wrap->state_ <= kClosingWithCallback); + CHECK_EQ(wrap->state_, kClosing); - const bool have_close_callback = (wrap->state_ == kClosingWithCallback); wrap->state_ = kClosed; wrap->OnClose(); - if (have_close_callback) + if (wrap->object()->Has(env->context(), env->onclose_string()) + .FromMaybe(false)) { wrap->MakeCallback(env->onclose_string(), 0, nullptr); - - delete wrap; + } } diff --git a/src/handle_wrap.h b/src/handle_wrap.h index b2b09f5010d1f7..4e177d249f28b5 100644 --- a/src/handle_wrap.h +++ b/src/handle_wrap.h @@ -95,7 +95,7 @@ class HandleWrap : public AsyncWrap { // refer to `doc/guides/node-postmortem-support.md` friend int GenDebugSymbols(); ListNode handle_wrap_queue_; - enum { kInitialized, kClosing, kClosingWithCallback, kClosed } state_; + enum { kInitialized, kClosing, kClosed } state_; uv_handle_t* const handle_; }; From 6d4931ab0b2f1bb1b05e49e1e060a5780a63933b Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sat, 2 Jun 2018 13:13:59 +0200 Subject: [PATCH 04/26] src: make handle onclose property a Symbol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes the property “more” hidden when exposing a `HandleWrap` as public API, e.g. for upcoming `MessagePort`s. --- src/async_wrap-inl.h | 16 ++++++++++++++++ src/async_wrap.h | 8 ++++++++ src/bootstrapper.cc | 20 +++++++++++++++++++- src/env-inl.h | 6 ++++++ src/env.cc | 13 +++++++++++++ src/env.h | 13 ++++++++++++- src/handle_wrap.cc | 8 +++++--- src/node_internals.h | 1 + 8 files changed, 80 insertions(+), 5 deletions(-) diff --git a/src/async_wrap-inl.h b/src/async_wrap-inl.h index c9f12333243092..5763b17aa08bc4 100644 --- a/src/async_wrap-inl.h +++ b/src/async_wrap-inl.h @@ -65,6 +65,22 @@ inline v8::MaybeLocal AsyncWrap::MakeCallback( const v8::Local symbol, int argc, v8::Local* argv) { + return MakeCallback(symbol.As(), argc, argv); +} + + +inline v8::MaybeLocal AsyncWrap::MakeCallback( + const v8::Local symbol, + int argc, + v8::Local* argv) { + return MakeCallback(symbol.As(), argc, argv); +} + + +inline v8::MaybeLocal AsyncWrap::MakeCallback( + const v8::Local symbol, + int argc, + v8::Local* argv) { v8::Local cb_v = object()->Get(symbol); CHECK(cb_v->IsFunction()); return MakeCallback(cb_v.As(), argc, argv); diff --git a/src/async_wrap.h b/src/async_wrap.h index 451bcfe12e6717..377702a8d6ef9c 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -158,10 +158,18 @@ class AsyncWrap : public BaseObject { v8::MaybeLocal MakeCallback(const v8::Local cb, int argc, v8::Local* argv); + inline v8::MaybeLocal MakeCallback( + const v8::Local symbol, + int argc, + v8::Local* argv); inline v8::MaybeLocal MakeCallback( const v8::Local symbol, int argc, v8::Local* argv); + inline v8::MaybeLocal MakeCallback( + const v8::Local symbol, + int argc, + v8::Local* argv); inline v8::MaybeLocal MakeCallback(uint32_t index, int argc, v8::Local* argv); diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc index 6c7c1af3e31cf6..35c7c4dc696ebd 100644 --- a/src/bootstrapper.cc +++ b/src/bootstrapper.cc @@ -17,6 +17,7 @@ using v8::Object; using v8::Promise; using v8::PromiseRejectEvent; using v8::PromiseRejectMessage; +using v8::String; using v8::Value; void SetupProcessObject(const FunctionCallbackInfo& args) { @@ -121,7 +122,7 @@ void SetupBootstrapObject(Environment* env, BOOTSTRAP_METHOD(_setgroups, SetGroups); #endif // __POSIX__ && !defined(__ANDROID__) && !defined(__CloudABI__) - auto should_abort_on_uncaught_toggle = + Local should_abort_on_uncaught_toggle = FIXED_ONE_BYTE_STRING(env->isolate(), "_shouldAbortOnUncaughtToggle"); CHECK(bootstrapper->Set(env->context(), should_abort_on_uncaught_toggle, @@ -130,4 +131,21 @@ void SetupBootstrapObject(Environment* env, } #undef BOOTSTRAP_METHOD +namespace symbols { + +void Initialize(Local target, + Local unused, + Local context) { + Environment* env = Environment::GetCurrent(context); +#define V(PropertyName, StringValue) \ + target->Set(env->context(), \ + env->PropertyName()->Name(), \ + env->PropertyName()).FromJust(); + PER_ISOLATE_SYMBOL_PROPERTIES(V) +#undef V +} + +} // namespace symbols } // namespace node + +NODE_MODULE_CONTEXT_AWARE_INTERNAL(symbols, node::symbols::Initialize) diff --git a/src/env-inl.h b/src/env-inl.h index cb8d0c4efe0405..aadb81271cf5d4 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -706,6 +706,7 @@ bool Environment::CleanupHookCallback::Equal::operator()( } #define VP(PropertyName, StringValue) V(v8::Private, PropertyName) +#define VY(PropertyName, StringValue) V(v8::Symbol, PropertyName) #define VS(PropertyName, StringValue) V(v8::String, PropertyName) #define V(TypeName, PropertyName) \ inline \ @@ -714,21 +715,26 @@ bool Environment::CleanupHookCallback::Equal::operator()( return const_cast(this)->PropertyName ## _.Get(isolate); \ } PER_ISOLATE_PRIVATE_SYMBOL_PROPERTIES(VP) + PER_ISOLATE_SYMBOL_PROPERTIES(VY) PER_ISOLATE_STRING_PROPERTIES(VS) #undef V #undef VS +#undef VY #undef VP #define VP(PropertyName, StringValue) V(v8::Private, PropertyName) +#define VY(PropertyName, StringValue) V(v8::Symbol, PropertyName) #define VS(PropertyName, StringValue) V(v8::String, PropertyName) #define V(TypeName, PropertyName) \ inline v8::Local Environment::PropertyName() const { \ return isolate_data()->PropertyName(isolate()); \ } PER_ISOLATE_PRIVATE_SYMBOL_PROPERTIES(VP) + PER_ISOLATE_SYMBOL_PROPERTIES(VY) PER_ISOLATE_STRING_PROPERTIES(VS) #undef V #undef VS +#undef VY #undef VP #define V(PropertyName, TypeName) \ diff --git a/src/env.cc b/src/env.cc index 7865ba95404df5..cb514828d2cdc4 100644 --- a/src/env.cc +++ b/src/env.cc @@ -23,6 +23,7 @@ using v8::Private; using v8::StackFrame; using v8::StackTrace; using v8::String; +using v8::Symbol; using v8::Value; IsolateData::IsolateData(Isolate* isolate, @@ -59,6 +60,18 @@ IsolateData::IsolateData(Isolate* isolate, sizeof(StringValue) - 1).ToLocalChecked())); PER_ISOLATE_PRIVATE_SYMBOL_PROPERTIES(V) #undef V +#define V(PropertyName, StringValue) \ + PropertyName ## _.Set( \ + isolate, \ + Symbol::New( \ + isolate, \ + String::NewFromOneByte( \ + isolate, \ + reinterpret_cast(StringValue), \ + v8::NewStringType::kInternalized, \ + sizeof(StringValue) - 1).ToLocalChecked())); + PER_ISOLATE_SYMBOL_PROPERTIES(V) +#undef V #define V(PropertyName, StringValue) \ PropertyName ## _.Set( \ isolate, \ diff --git a/src/env.h b/src/env.h index 96252fa12a1397..7a432eaa3d4ff0 100644 --- a/src/env.h +++ b/src/env.h @@ -107,6 +107,11 @@ struct PackageConfig { V(napi_env, "node:napi:env") \ V(napi_wrapper, "node:napi:wrapper") \ +// Symbols are per-isolate primitives but Environment proxies them +// for the sake of convenience. +#define PER_ISOLATE_SYMBOL_PROPERTIES(V) \ + V(handle_onclose_symbol, "handle_onclose") \ + // Strings are per-isolate primitives but Environment proxies them // for the sake of convenience. Strings should be ASCII-only. #define PER_ISOLATE_STRING_PROPERTIES(V) \ @@ -127,7 +132,6 @@ struct PackageConfig { V(chunks_sent_since_last_write_string, "chunksSentSinceLastWrite") \ V(constants_string, "constants") \ V(oncertcb_string, "oncertcb") \ - V(onclose_string, "_onclose") \ V(code_string, "code") \ V(cwd_string, "cwd") \ V(dest_string, "dest") \ @@ -356,10 +360,12 @@ class IsolateData { inline MultiIsolatePlatform* platform() const; #define VP(PropertyName, StringValue) V(v8::Private, PropertyName) +#define VY(PropertyName, StringValue) V(v8::Symbol, PropertyName) #define VS(PropertyName, StringValue) V(v8::String, PropertyName) #define V(TypeName, PropertyName) \ inline v8::Local PropertyName(v8::Isolate* isolate) const; PER_ISOLATE_PRIVATE_SYMBOL_PROPERTIES(VP) + PER_ISOLATE_SYMBOL_PROPERTIES(VY) PER_ISOLATE_STRING_PROPERTIES(VS) #undef V #undef VS @@ -370,10 +376,12 @@ class IsolateData { private: #define VP(PropertyName, StringValue) V(v8::Private, PropertyName) +#define VY(PropertyName, StringValue) V(v8::Symbol, PropertyName) #define VS(PropertyName, StringValue) V(v8::String, PropertyName) #define V(TypeName, PropertyName) \ v8::Eternal PropertyName ## _; PER_ISOLATE_PRIVATE_SYMBOL_PROPERTIES(VP) + PER_ISOLATE_SYMBOL_PROPERTIES(VY) PER_ISOLATE_STRING_PROPERTIES(VS) #undef V #undef VS @@ -737,13 +745,16 @@ class Environment { // Strings and private symbols are shared across shared contexts // The getters simply proxy to the per-isolate primitive. #define VP(PropertyName, StringValue) V(v8::Private, PropertyName) +#define VY(PropertyName, StringValue) V(v8::Symbol, PropertyName) #define VS(PropertyName, StringValue) V(v8::String, PropertyName) #define V(TypeName, PropertyName) \ inline v8::Local PropertyName() const; PER_ISOLATE_PRIVATE_SYMBOL_PROPERTIES(VP) + PER_ISOLATE_SYMBOL_PROPERTIES(VY) PER_ISOLATE_STRING_PROPERTIES(VS) #undef V #undef VS +#undef VY #undef VP #define V(PropertyName, TypeName) \ diff --git a/src/handle_wrap.cc b/src/handle_wrap.cc index bbc124ff69dc89..4c2a33aa84459d 100644 --- a/src/handle_wrap.cc +++ b/src/handle_wrap.cc @@ -76,7 +76,9 @@ void HandleWrap::Close(Local close_callback) { state_ = kClosing; if (!close_callback.IsEmpty() && close_callback->IsFunction()) { - object()->Set(env()->context(), env()->onclose_string(), close_callback) + object()->Set(env()->context(), + env()->handle_onclose_symbol(), + close_callback) .FromMaybe(false); } } @@ -121,9 +123,9 @@ void HandleWrap::OnClose(uv_handle_t* handle) { wrap->OnClose(); - if (wrap->object()->Has(env->context(), env->onclose_string()) + if (wrap->object()->Has(env->context(), env->handle_onclose_symbol()) .FromMaybe(false)) { - wrap->MakeCallback(env->onclose_string(), 0, nullptr); + wrap->MakeCallback(env->handle_onclose_symbol(), 0, nullptr); } } diff --git a/src/node_internals.h b/src/node_internals.h index 3014a0e5f7a442..7bf4eaf1ecf86a 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -125,6 +125,7 @@ struct sockaddr; V(stream_pipe) \ V(stream_wrap) \ V(string_decoder) \ + V(symbols) \ V(tcp_wrap) \ V(timer_wrap) \ V(trace_events) \ From 315efb5e8b0673a264a9e2b4182a4f671d894a4f Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 5 Sep 2017 22:38:32 +0200 Subject: [PATCH 05/26] worker: implement `MessagePort` and `MessageChannel` Implement `MessagePort` and `MessageChannel` along the lines of the DOM classes of the same names. `MessagePort`s initially support transferring only `ArrayBuffer`s. Thanks to Stephen Belanger for reviewing this change in its original form, to Benjamin Gruenbaum for reviewing the added tests in their original form, and to Olivia Hugger for reviewing the documentation in its original form. Refs: https://github.com/ayojs/ayo/pull/98 --- doc/api/_toc.md | 1 + doc/api/all.md | 1 + doc/api/errors.md | 16 + doc/api/worker.md | 146 +++++ lib/internal/bootstrap/loaders.js | 3 +- lib/internal/modules/cjs/helpers.js | 5 + lib/internal/worker.js | 103 ++++ lib/worker.js | 5 + node.gyp | 4 + src/async_wrap.h | 1 + src/env.h | 5 + src/node.cc | 9 + src/node_config.cc | 3 + src/node_errors.h | 6 + src/node_internals.h | 6 + src/node_messaging.cc | 548 ++++++++++++++++++ src/node_messaging.h | 167 ++++++ src/util.h | 3 + test/parallel/test-message-channel.js | 26 + .../parallel/test-message-port-arraybuffer.js | 20 + test/parallel/test-message-port.js | 56 ++ test/sequential/test-async-wrap-getasyncid.js | 2 + tools/doc/type-parser.js | 4 +- 23 files changed, 1138 insertions(+), 2 deletions(-) create mode 100644 doc/api/worker.md create mode 100644 lib/internal/worker.js create mode 100644 lib/worker.js create mode 100644 src/node_messaging.cc create mode 100644 src/node_messaging.h create mode 100644 test/parallel/test-message-channel.js create mode 100644 test/parallel/test-message-port-arraybuffer.js create mode 100644 test/parallel/test-message-port.js diff --git a/doc/api/_toc.md b/doc/api/_toc.md index 9b487b50a55031..1b2fdea26e46eb 100644 --- a/doc/api/_toc.md +++ b/doc/api/_toc.md @@ -53,6 +53,7 @@ * [Utilities](util.html) * [V8](v8.html) * [VM](vm.html) +* [Worker](worker.html) * [ZLIB](zlib.html)
diff --git a/doc/api/all.md b/doc/api/all.md index d013f07bd328fc..6f0a21dd092105 100644 --- a/doc/api/all.md +++ b/doc/api/all.md @@ -46,4 +46,5 @@ @include util @include v8 @include vm +@include worker @include zlib diff --git a/doc/api/errors.md b/doc/api/errors.md index fc1bcd3e6e994b..f3e5939f258576 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -650,12 +650,23 @@ Used when a child process is being forked without specifying an IPC channel. Used when the main process is trying to read data from the child process's STDERR / STDOUT, and the data's length is longer than the `maxBuffer` option. + +### ERR_CLOSED_MESSAGE_PORT + +There was an attempt to use a `MessagePort` instance in a closed +state, usually after `.close()` has been called. + ### ERR_CONSOLE_WRITABLE_STREAM `Console` was instantiated without `stdout` stream, or `Console` has a non-writable `stdout` or `stderr` stream. + +### ERR_CONSTRUCT_CALL_REQUIRED + +A constructor for a class was called without `new`. + ### ERR_CPU_USAGE @@ -1203,6 +1214,11 @@ urlSearchParams.has.call(buf, 'foo'); // Throws a TypeError with code 'ERR_INVALID_THIS' ``` + +### ERR_INVALID_TRANSFER_OBJECT + +An invalid transfer object was passed to `postMessage()`. + ### ERR_INVALID_TUPLE diff --git a/doc/api/worker.md b/doc/api/worker.md new file mode 100644 index 00000000000000..4724714cd62f26 --- /dev/null +++ b/doc/api/worker.md @@ -0,0 +1,146 @@ +# Worker + + + +> Stability: 1 - Experimental + +## Class: MessageChannel + + +Instances of the `worker.MessageChannel` class represent an asynchronous, +two-way communications channel. +The `MessageChannel` has no methods of its own. `new MessageChannel()` +yields an object with `port1` and `port2` properties, which refer to linked +[`MessagePort`][] instances. + +```js +const { MessageChannel } = require('worker'); + +const { port1, port2 } = new MessageChannel(); +port1.on('message', (message) => console.log('received', message)); +port2.postMessage({ foo: 'bar' }); +// prints: received { foo: 'bar' } +``` + +## Class: MessagePort + + +* Extends: {EventEmitter} + +Instances of the `worker.MessagePort` class represent one end of an +asynchronous, two-way communications channel. It can be used to transfer +structured data, memory regions and other `MessagePort`s between different +[`Worker`][]s. + +With the exception of `MessagePort`s being [`EventEmitter`][]s rather +than `EventTarget`s, this implementation matches [browser `MessagePort`][]s. + +### Event: 'close' + + +The `'close'` event is emitted once either side of the channel has been +disconnected. + +### Event: 'message' + + +* `value` {any} The transmitted value + +The `'message'` event is emitted for any incoming message, containing the cloned +input of [`port.postMessage()`][]. + +Listeners on this event will receive a clone of the `value` parameter as passed +to `postMessage()` and no further arguments. + +### port.close() + + +Disables further sending of messages on either side of the connection. +This method can be called once you know that no further communication +will happen over this `MessagePort`. + +### port.postMessage(value[, transferList]) + + +* `value` {any} +* `transferList` {Object[]} + +Sends a JavaScript value to the receiving side of this channel. +`value` will be transferred in a way which is compatible with +the [HTML structured clone algorithm][]. In particular, it may contain circular +references and objects like typed arrays that the `JSON` API is not able +to stringify. + +`transferList` may be a list of `ArrayBuffer` objects. +After transferring, they will not be usable on the sending side of the channel +anymore (even if they are not contained in `value`). + +`value` may still contain `ArrayBuffer` instances that are not in +`transferList`; in that case, the underlying memory is copied rather than moved. + +For more information on the serialization and deserialization mechanisms +behind this API, see the [serialization API of the `v8` module][v8.serdes]. + +Because the object cloning uses the structured clone algorithm, +non-enumerable properties, property accessors, and object prototypes are +not preserved. In particular, [`Buffer`][] objects will be read as +plain [`Uint8Array`][]s on the receiving side. + +The message object will be cloned immediately, and can be modified after +posting without having side effects. + +### port.ref() + + +Opposite of `unref()`. Calling `ref()` on a previously `unref()`ed port will +*not* let the program exit if it's the only active handle left (the default +behavior). If the port is `ref()`ed, calling `ref()` again will have no effect. + +If listeners are attached or removed using `.on('message')`, the port will +be `ref()`ed and `unref()`ed automatically depending on whether +listeners for the event exist. + +### port.start() + + +Starts receiving messages on this `MessagePort`. When using this port +as an event emitter, this will be called automatically once `'message'` +listeners are attached. + +### port.unref() + + +Calling `unref()` on a port will allow the thread to exit if this is the only +active handle in the event system. If the port is already `unref()`ed calling +`unref()` again will have no effect. + +If listeners are attached or removed using `.on('message')`, the port will +be `ref()`ed and `unref()`ed automatically depending on whether +listeners for the event exist. + +[`Buffer`]: buffer.html +[`EventEmitter`]: events.html +[`MessagePort`]: #worker_class_messageport +[`port.postMessage()`]: #worker_port_postmessage_value_transferlist +[v8.serdes]: v8.html#v8_serialization_api +[`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array +[browser `MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort +[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm diff --git a/lib/internal/bootstrap/loaders.js b/lib/internal/bootstrap/loaders.js index ff809a91291bee..417e8594e14aab 100644 --- a/lib/internal/bootstrap/loaders.js +++ b/lib/internal/bootstrap/loaders.js @@ -194,7 +194,8 @@ }; NativeModule.isInternal = function(id) { - return id.startsWith('internal/'); + return id.startsWith('internal/') || + (id === 'worker' && !process.binding('config').experimentalWorker); }; } diff --git a/lib/internal/modules/cjs/helpers.js b/lib/internal/modules/cjs/helpers.js index 60346c5841c7df..55eaed7d376506 100644 --- a/lib/internal/modules/cjs/helpers.js +++ b/lib/internal/modules/cjs/helpers.js @@ -105,6 +105,11 @@ const builtinLibs = [ 'v8', 'vm', 'zlib' ]; +if (process.binding('config').experimentalWorker) { + builtinLibs.push('worker'); + builtinLibs.sort(); +} + if (typeof process.binding('inspector').open === 'function') { builtinLibs.push('inspector'); builtinLibs.sort(); diff --git a/lib/internal/worker.js b/lib/internal/worker.js new file mode 100644 index 00000000000000..03caa07a4b1eeb --- /dev/null +++ b/lib/internal/worker.js @@ -0,0 +1,103 @@ +'use strict'; + +const EventEmitter = require('events'); +const util = require('util'); + +const { internalBinding } = require('internal/bootstrap/loaders'); +const { MessagePort, MessageChannel } = internalBinding('messaging'); +util.inherits(MessagePort, EventEmitter); + +const kOnMessageListener = Symbol('kOnMessageListener'); + +const debug = util.debuglog('worker'); + +// A MessagePort consists of a handle (that wraps around an +// uv_async_t) which can receive information from other threads and emits +// .onmessage events, and a function used for sending data to a MessagePort +// in some other thread. +MessagePort.prototype[kOnMessageListener] = function onmessage(payload) { + debug('received message', payload); + // Emit the deserialized object to userland. + this.emit('message', payload); +}; + +// This is for compatibility with the Web's MessagePort API. It makes sense to +// provide it as an `EventEmitter` in Node.js, but if somebody overrides +// `onmessage`, we'll switch over to the Web API model. +Object.defineProperty(MessagePort.prototype, 'onmessage', { + enumerable: true, + configurable: true, + get() { + return this[kOnMessageListener]; + }, + set(value) { + this[kOnMessageListener] = value; + if (typeof value === 'function') { + this.ref(); + this.start(); + } else { + this.unref(); + this.stop(); + } + } +}); + +// This is called from inside the `MessagePort` constructor. +function oninit() { + setupPortReferencing(this, this, 'message'); +} + +Object.defineProperty(MessagePort.prototype, 'oninit', { + enumerable: true, + writable: false, + value: oninit +}); + +// This is called after the underlying `uv_async_t` has been closed. +function onclose() { + if (typeof this.onclose === 'function') { + // Not part of the Web standard yet, but there aren't many reasonable + // alternatives in a non-EventEmitter usage setting. + // Refs: https://github.com/whatwg/html/issues/1766 + this.onclose(); + } + this.emit('close'); +} + +Object.defineProperty(MessagePort.prototype, '_onclose', { + enumerable: true, + writable: false, + value: onclose +}); + +const originalClose = MessagePort.prototype.close; +MessagePort.prototype.close = function(cb) { + if (typeof cb === 'function') + this.once('close', cb); + originalClose.call(this); +}; + +function setupPortReferencing(port, eventEmitter, eventName) { + // Keep track of whether there are any workerMessage listeners: + // If there are some, ref() the channel so it keeps the event loop alive. + // If there are none or all are removed, unref() the channel so the worker + // can shutdown gracefully. + port.unref(); + eventEmitter.on('newListener', (name) => { + if (name === eventName && eventEmitter.listenerCount(eventName) === 0) { + port.ref(); + port.start(); + } + }); + eventEmitter.on('removeListener', (name) => { + if (name === eventName && eventEmitter.listenerCount(eventName) === 0) { + port.stop(); + port.unref(); + } + }); +} + +module.exports = { + MessagePort, + MessageChannel +}; diff --git a/lib/worker.js b/lib/worker.js new file mode 100644 index 00000000000000..d67fb4efe40a33 --- /dev/null +++ b/lib/worker.js @@ -0,0 +1,5 @@ +'use strict'; + +const { MessagePort, MessageChannel } = require('internal/worker'); + +module.exports = { MessagePort, MessageChannel }; diff --git a/node.gyp b/node.gyp index 4b94c1dd6b2ad9..9a8dbf00cd9f15 100644 --- a/node.gyp +++ b/node.gyp @@ -78,6 +78,7 @@ 'lib/util.js', 'lib/v8.js', 'lib/vm.js', + 'lib/worker.js', 'lib/zlib.js', 'lib/internal/assert.js', 'lib/internal/async_hooks.js', @@ -156,6 +157,7 @@ 'lib/internal/validators.js', 'lib/internal/stream_base_commons.js', 'lib/internal/vm/module.js', + 'lib/internal/worker.js', 'lib/internal/streams/lazy_transform.js', 'lib/internal/streams/async_iterator.js', 'lib/internal/streams/buffer_list.js', @@ -334,6 +336,7 @@ 'src/node_file.cc', 'src/node_http2.cc', 'src/node_http_parser.cc', + 'src/node_messaging.cc', 'src/node_os.cc', 'src/node_platform.cc', 'src/node_perf.cc', @@ -391,6 +394,7 @@ 'src/node_http2_state.h', 'src/node_internals.h', 'src/node_javascript.h', + 'src/node_messaging.h', 'src/node_mutex.h', 'src/node_perf.h', 'src/node_perf_common.h', diff --git a/src/async_wrap.h b/src/async_wrap.h index 377702a8d6ef9c..cf269a4c1f5e1e 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -49,6 +49,7 @@ namespace node { V(HTTP2SETTINGS) \ V(HTTPPARSER) \ V(JSSTREAM) \ + V(MESSAGEPORT) \ V(PIPECONNECTWRAP) \ V(PIPESERVERWRAP) \ V(PIPEWRAP) \ diff --git a/src/env.h b/src/env.h index 7a432eaa3d4ff0..d87c39c5186bd7 100644 --- a/src/env.h +++ b/src/env.h @@ -193,6 +193,7 @@ struct PackageConfig { V(main_string, "main") \ V(max_buffer_string, "maxBuffer") \ V(message_string, "message") \ + V(message_port_constructor_string, "MessagePort") \ V(minttl_string, "minttl") \ V(modulus_string, "modulus") \ V(name_string, "name") \ @@ -212,6 +213,7 @@ struct PackageConfig { V(onhandshakedone_string, "onhandshakedone") \ V(onhandshakestart_string, "onhandshakestart") \ V(onheaders_string, "onheaders") \ + V(oninit_string, "oninit") \ V(onmessage_string, "onmessage") \ V(onnewsession_string, "onnewsession") \ V(onocspresponse_string, "onocspresponse") \ @@ -242,6 +244,8 @@ struct PackageConfig { V(pipe_target_string, "pipeTarget") \ V(pipe_source_string, "pipeSource") \ V(port_string, "port") \ + V(port1_string, "port1") \ + V(port2_string, "port2") \ V(preference_string, "preference") \ V(priority_string, "priority") \ V(promise_string, "promise") \ @@ -323,6 +327,7 @@ struct PackageConfig { V(http2stream_constructor_template, v8::ObjectTemplate) \ V(immediate_callback_function, v8::Function) \ V(inspector_console_api_object, v8::Object) \ + V(message_port_constructor_template, v8::FunctionTemplate) \ V(pbkdf2_constructor_template, v8::ObjectTemplate) \ V(pipe_constructor_template, v8::FunctionTemplate) \ V(performance_entry_callback, v8::Function) \ diff --git a/src/node.cc b/src/node.cc index bf3aae2d35f773..baa97281b064a7 100644 --- a/src/node.cc +++ b/src/node.cc @@ -253,6 +253,11 @@ bool config_experimental_modules = false; // that is used by lib/vm.js bool config_experimental_vm_modules = false; +// Set in node.cc by ParseArgs when --experimental-worker is used. +// Used in node_config.cc to set a constant on process.binding('config') +// that is used by lib/worker.js +bool config_experimental_worker = false; + // Set in node.cc by ParseArgs when --experimental-repl-await is used. // Used in node_config.cc to set a constant on process.binding('config') // that is used by lib/repl.js. @@ -3094,6 +3099,7 @@ static void PrintHelp() { " --experimental-vm-modules experimental ES Module support\n" " in vm module\n" #endif // defined(NODE_HAVE_I18N_SUPPORT) + " --experimental-worker experimental threaded Worker support\n" #if HAVE_OPENSSL && NODE_FIPS_MODE " --force-fips force FIPS crypto (cannot be disabled)\n" #endif // HAVE_OPENSSL && NODE_FIPS_MODE @@ -3257,6 +3263,7 @@ static void CheckIfAllowedInEnv(const char* exe, bool is_env, "--experimental-modules", "--experimental-repl-await", "--experimental-vm-modules", + "--experimental-worker", "--force-fips", "--icu-data-dir", "--inspect", @@ -3454,6 +3461,8 @@ static void ParseArgs(int* argc, new_v8_argc += 1; } else if (strcmp(arg, "--experimental-vm-modules") == 0) { config_experimental_vm_modules = true; + } else if (strcmp(arg, "--experimental-worker") == 0) { + config_experimental_worker = true; } else if (strcmp(arg, "--experimental-repl-await") == 0) { config_experimental_repl_await = true; } else if (strcmp(arg, "--loader") == 0) { diff --git a/src/node_config.cc b/src/node_config.cc index 603d55491a259b..dd5ee666486874 100644 --- a/src/node_config.cc +++ b/src/node_config.cc @@ -91,6 +91,9 @@ static void Initialize(Local target, if (config_experimental_vm_modules) READONLY_BOOLEAN_PROPERTY("experimentalVMModules"); + if (config_experimental_worker) + READONLY_BOOLEAN_PROPERTY("experimentalWorker"); + if (config_experimental_repl_await) READONLY_BOOLEAN_PROPERTY("experimentalREPLAwait"); diff --git a/src/node_errors.h b/src/node_errors.h index b2f2b256c4c120..81169d241bc226 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -23,9 +23,12 @@ namespace node { #define ERRORS_WITH_CODE(V) \ V(ERR_BUFFER_OUT_OF_BOUNDS, RangeError) \ V(ERR_BUFFER_TOO_LARGE, Error) \ + V(ERR_CLOSED_MESSAGE_PORT, Error) \ + V(ERR_CONSTRUCT_CALL_REQUIRED, Error) \ V(ERR_INDEX_OUT_OF_RANGE, RangeError) \ V(ERR_INVALID_ARG_VALUE, TypeError) \ V(ERR_INVALID_ARG_TYPE, TypeError) \ + V(ERR_INVALID_TRANSFER_OBJECT, TypeError) \ V(ERR_MEMORY_ALLOCATION_FAILED, Error) \ V(ERR_MISSING_ARGS, TypeError) \ V(ERR_MISSING_MODULE, Error) \ @@ -54,7 +57,10 @@ namespace node { // Errors with predefined static messages #define PREDEFINED_ERROR_MESSAGES(V) \ + V(ERR_CLOSED_MESSAGE_PORT, "Cannot send data on closed MessagePort") \ + V(ERR_CONSTRUCT_CALL_REQUIRED, "Cannot call constructor without `new`") \ V(ERR_INDEX_OUT_OF_RANGE, "Index out of range") \ + V(ERR_INVALID_TRANSFER_OBJECT, "Found invalid object in transferList") \ V(ERR_MEMORY_ALLOCATION_FAILED, "Failed to allocate memory") \ V(ERR_SCRIPT_EXECUTION_INTERRUPTED, \ "Script execution was interrupted by `SIGINT`") diff --git a/src/node_internals.h b/src/node_internals.h index 7bf4eaf1ecf86a..a5d8ed0e5d3ad7 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -114,6 +114,7 @@ struct sockaddr; V(http_parser) \ V(inspector) \ V(js_stream) \ + V(messaging) \ V(module_wrap) \ V(os) \ V(performance) \ @@ -189,6 +190,11 @@ extern bool config_experimental_modules; // that is used by lib/vm.js extern bool config_experimental_vm_modules; +// Set in node.cc by ParseArgs when --experimental-vm-modules is used. +// Used in node_config.cc to set a constant on process.binding('config') +// that is used by lib/vm.js +extern bool config_experimental_worker; + // Set in node.cc by ParseArgs when --experimental-repl-await is used. // Used in node_config.cc to set a constant on process.binding('config') // that is used by lib/repl.js. diff --git a/src/node_messaging.cc b/src/node_messaging.cc new file mode 100644 index 00000000000000..c6e701c7d94426 --- /dev/null +++ b/src/node_messaging.cc @@ -0,0 +1,548 @@ +#include "node_messaging.h" +#include "node_internals.h" +#include "node_buffer.h" +#include "node_errors.h" +#include "util.h" +#include "util-inl.h" +#include "async_wrap.h" +#include "async_wrap-inl.h" + +using v8::Array; +using v8::ArrayBuffer; +using v8::ArrayBufferCreationMode; +using v8::Context; +using v8::EscapableHandleScope; +using v8::Exception; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Isolate; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Nothing; +using v8::Object; +using v8::String; +using v8::Value; +using v8::ValueDeserializer; +using v8::ValueSerializer; + +namespace node { +namespace worker { + +Message::Message(MallocedBuffer&& buffer) + : main_message_buf_(std::move(buffer)) {} + +namespace { + +// This is used to tell V8 how to read transferred host objects, like other +// `MessagePort`s and `SharedArrayBuffer`s, and make new JS objects out of them. +class DeserializerDelegate : public ValueDeserializer::Delegate { + public: + DeserializerDelegate(Message* m, Environment* env) + : env_(env), msg_(m) {} + + ValueDeserializer* deserializer = nullptr; + + private: + Environment* env_; + Message* msg_; +}; + +} // anonymous namespace + +MaybeLocal Message::Deserialize(Environment* env, + Local context) { + EscapableHandleScope handle_scope(env->isolate()); + Context::Scope context_scope(context); + + DeserializerDelegate delegate(this, env); + ValueDeserializer deserializer( + env->isolate(), + reinterpret_cast(main_message_buf_.data), + main_message_buf_.size, + &delegate); + delegate.deserializer = &deserializer; + + // Attach all transfered ArrayBuffers to their new Isolate. + for (uint32_t i = 0; i < array_buffer_contents_.size(); ++i) { + Local ab = + ArrayBuffer::New(env->isolate(), + array_buffer_contents_[i].release(), + array_buffer_contents_[i].size, + ArrayBufferCreationMode::kInternalized); + deserializer.TransferArrayBuffer(i, ab); + } + array_buffer_contents_.clear(); + + if (deserializer.ReadHeader(context).IsNothing()) + return MaybeLocal(); + return handle_scope.Escape( + deserializer.ReadValue(context).FromMaybe(Local())); +} + +namespace { + +// This tells V8 how to serialize objects that it does not understand +// (e.g. C++ objects) into the output buffer, in a way that our own +// DeserializerDelegate understands how to unpack. +class SerializerDelegate : public ValueSerializer::Delegate { + public: + SerializerDelegate(Environment* env, Local context, Message* m) + : env_(env), context_(context), msg_(m) {} + + void ThrowDataCloneError(Local message) override { + env_->isolate()->ThrowException(Exception::Error(message)); + } + + ValueSerializer* serializer = nullptr; + + private: + Environment* env_; + Local context_; + Message* msg_; + + friend class worker::Message; +}; + +} // anynomous namespace + +Maybe Message::Serialize(Environment* env, + Local context, + Local input, + Local transfer_list_v) { + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(context); + + // Verify that we're not silently overwriting an existing message. + CHECK(main_message_buf_.is_empty()); + + SerializerDelegate delegate(env, context, this); + ValueSerializer serializer(env->isolate(), &delegate); + delegate.serializer = &serializer; + + std::vector> array_buffers; + if (transfer_list_v->IsArray()) { + Local transfer_list = transfer_list_v.As(); + uint32_t length = transfer_list->Length(); + for (uint32_t i = 0; i < length; ++i) { + Local entry; + if (!transfer_list->Get(context, i).ToLocal(&entry)) + return Nothing(); + // Currently, we support ArrayBuffers. + if (entry->IsArrayBuffer()) { + Local ab = entry.As(); + // If we cannot render the ArrayBuffer unusable in this Isolate and + // take ownership of its memory, copying the buffer will have to do. + if (!ab->IsNeuterable() || ab->IsExternal()) + continue; + // We simply use the array index in the `array_buffers` list as the + // ID that we write into the serialized buffer. + uint32_t id = array_buffers.size(); + array_buffers.push_back(ab); + serializer.TransferArrayBuffer(id, ab); + continue; + } + + THROW_ERR_INVALID_TRANSFER_OBJECT(env); + return Nothing(); + } + } + + serializer.WriteHeader(); + if (serializer.WriteValue(context, input).IsNothing()) { + return Nothing(); + } + + for (Local ab : array_buffers) { + // If serialization succeeded, we want to take ownership of + // (a.k.a. externalize) the underlying memory region and render + // it inaccessible in this Isolate. + ArrayBuffer::Contents contents = ab->Externalize(); + ab->Neuter(); + array_buffer_contents_.push_back( + MallocedBuffer { static_cast(contents.Data()), + contents.ByteLength() }); + } + + // The serializer gave us a buffer allocated using `malloc()`. + std::pair data = serializer.Release(); + main_message_buf_ = + MallocedBuffer(reinterpret_cast(data.first), data.second); + return Just(true); +} + +MessagePortData::MessagePortData(MessagePort* owner) : owner_(owner) { } + +MessagePortData::~MessagePortData() { + CHECK_EQ(owner_, nullptr); + Disentangle(); +} + +void MessagePortData::AddToIncomingQueue(Message&& message) { + // This function will be called by other threads. + Mutex::ScopedLock lock(mutex_); + incoming_messages_.emplace_back(std::move(message)); + + if (owner_ != nullptr) + owner_->TriggerAsync(); +} + +bool MessagePortData::IsSiblingClosed() const { + Mutex::ScopedLock lock(*sibling_mutex_); + return sibling_ == nullptr; +} + +void MessagePortData::Entangle(MessagePortData* a, MessagePortData* b) { + CHECK_EQ(a->sibling_, nullptr); + CHECK_EQ(b->sibling_, nullptr); + a->sibling_ = b; + b->sibling_ = a; + a->sibling_mutex_ = b->sibling_mutex_; +} + +void MessagePortData::PingOwnerAfterDisentanglement() { + Mutex::ScopedLock lock(mutex_); + if (owner_ != nullptr) + owner_->TriggerAsync(); +} + +void MessagePortData::Disentangle() { + // Grab a copy of the sibling mutex, then replace it so that each sibling + // has its own sibling_mutex_ now. + std::shared_ptr sibling_mutex = sibling_mutex_; + Mutex::ScopedLock sibling_lock(*sibling_mutex); + sibling_mutex_ = std::make_shared(); + + MessagePortData* sibling = sibling_; + if (sibling_ != nullptr) { + sibling_->sibling_ = nullptr; + sibling_ = nullptr; + } + + // We close MessagePorts after disentanglement, so we trigger the + // corresponding uv_async_t to let them know that this happened. + PingOwnerAfterDisentanglement(); + if (sibling != nullptr) { + sibling->PingOwnerAfterDisentanglement(); + } +} + +MessagePort::~MessagePort() { + if (data_) + data_->owner_ = nullptr; +} + +MessagePort::MessagePort(Environment* env, + Local context, + Local wrap) + : HandleWrap(env, + wrap, + reinterpret_cast(new uv_async_t()), + AsyncWrap::PROVIDER_MESSAGEPORT), + data_(new MessagePortData(this)) { + auto onmessage = [](uv_async_t* handle) { + // Called when data has been put into the queue. + MessagePort* channel = static_cast(handle->data); + channel->OnMessage(); + }; + CHECK_EQ(uv_async_init(env->event_loop(), + async(), + onmessage), 0); + async()->data = static_cast(this); + + Local fn; + if (!wrap->Get(context, env->oninit_string()).ToLocal(&fn)) + return; + + if (fn->IsFunction()) { + Local init = fn.As(); + USE(init->Call(context, wrap, 0, nullptr)); + } +} + +void MessagePort::AddToIncomingQueue(Message&& message) { + data_->AddToIncomingQueue(std::move(message)); +} + +uv_async_t* MessagePort::async() { + return reinterpret_cast(GetHandle()); +} + +void MessagePort::TriggerAsync() { + CHECK_EQ(uv_async_send(async()), 0); +} + +void MessagePort::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + if (!args.IsConstructCall()) { + THROW_ERR_CONSTRUCT_CALL_REQUIRED(env); + return; + } + + Local context = args.This()->CreationContext(); + Context::Scope context_scope(context); + + new MessagePort(env, context, args.This()); +} + +MessagePort* MessagePort::New( + Environment* env, + Local context, + std::unique_ptr data) { + Context::Scope context_scope(context); + Local ctor; + if (!GetMessagePortConstructor(env, context).ToLocal(&ctor)) + return nullptr; + MessagePort* port = nullptr; + + // Construct a new instance, then assign the listener instance and possibly + // the MessagePortData to it. + Local instance; + if (!ctor->NewInstance(context).ToLocal(&instance)) + return nullptr; + ASSIGN_OR_RETURN_UNWRAP(&port, instance, nullptr); + if (data) { + port->Detach(); + port->data_ = std::move(data); + port->data_->owner_ = port; + // If the existing MessagePortData object had pending messages, this is + // the easiest way to run that queue. + port->TriggerAsync(); + } + return port; +} + +void MessagePort::OnMessage() { + HandleScope handle_scope(env()->isolate()); + Local context = object()->CreationContext(); + + // data_ can only ever be modified by the owner thread, so no need to lock. + // However, the message port may be transferred while it is processing + // messages, so we need to check that this handle still owns its `data_` field + // on every iteration. + while (data_) { + Message received; + { + // Get the head of the message queue. + Mutex::ScopedLock lock(data_->mutex_); + if (!data_->receiving_messages_) + break; + if (data_->incoming_messages_.empty()) + break; + received = std::move(data_->incoming_messages_.front()); + data_->incoming_messages_.pop_front(); + } + + if (!env()->can_call_into_js()) { + // In this case there is nothing to do but to drain the current queue. + continue; + } + + { + // Call the JS .onmessage() callback. + HandleScope handle_scope(env()->isolate()); + Context::Scope context_scope(context); + Local args[] = { + received.Deserialize(env(), context).FromMaybe(Local()) + }; + + if (args[0].IsEmpty() || + !object()->Has(context, env()->onmessage_string()).FromMaybe(false) || + MakeCallback(env()->onmessage_string(), 1, args).IsEmpty()) { + // Re-schedule OnMessage() execution in case of failure. + if (data_) + TriggerAsync(); + return; + } + } + } + + if (data_ && data_->IsSiblingClosed()) { + Close(); + } +} + +bool MessagePort::IsSiblingClosed() const { + CHECK(data_); + return data_->IsSiblingClosed(); +} + +void MessagePort::OnClose() { + if (data_) { + data_->owner_ = nullptr; + data_->Disentangle(); + } + data_.reset(); + delete async(); +} + +std::unique_ptr MessagePort::Detach() { + Mutex::ScopedLock lock(data_->mutex_); + data_->owner_ = nullptr; + return std::move(data_); +} + + +void MessagePort::Send(Message&& message) { + Mutex::ScopedLock lock(*data_->sibling_mutex_); + if (data_->sibling_ == nullptr) + return; + data_->sibling_->AddToIncomingQueue(std::move(message)); +} + +void MessagePort::Send(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Message msg; + if (msg.Serialize(env, object()->CreationContext(), args[0], args[1]) + .IsNothing()) { + return; + } + Send(std::move(msg)); +} + +void MessagePort::PostMessage(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + MessagePort* port; + ASSIGN_OR_RETURN_UNWRAP(&port, args.This()); + if (!port->data_) { + return THROW_ERR_CLOSED_MESSAGE_PORT(env); + } + if (args.Length() == 0) { + return THROW_ERR_MISSING_ARGS(env, "Not enough arguments to " + "MessagePort.postMessage"); + } + port->Send(args); +} + +void MessagePort::Start() { + Mutex::ScopedLock lock(data_->mutex_); + data_->receiving_messages_ = true; + if (!data_->incoming_messages_.empty()) + TriggerAsync(); +} + +void MessagePort::Stop() { + Mutex::ScopedLock lock(data_->mutex_); + data_->receiving_messages_ = false; +} + +void MessagePort::Start(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + MessagePort* port; + ASSIGN_OR_RETURN_UNWRAP(&port, args.This()); + if (!port->data_) { + THROW_ERR_CLOSED_MESSAGE_PORT(env); + return; + } + port->Start(); +} + +void MessagePort::Stop(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + MessagePort* port; + ASSIGN_OR_RETURN_UNWRAP(&port, args.This()); + if (!port->data_) { + THROW_ERR_CLOSED_MESSAGE_PORT(env); + return; + } + port->Stop(); +} + +size_t MessagePort::self_size() const { + Mutex::ScopedLock lock(data_->mutex_); + size_t sz = sizeof(*this) + sizeof(*data_); + for (const Message& msg : data_->incoming_messages_) + sz += sizeof(msg) + msg.main_message_buf_.size; + return sz; +} + +void MessagePort::Entangle(MessagePort* a, MessagePort* b) { + Entangle(a, b->data_.get()); +} + +void MessagePort::Entangle(MessagePort* a, MessagePortData* b) { + MessagePortData::Entangle(a->data_.get(), b); +} + +MaybeLocal GetMessagePortConstructor( + Environment* env, Local context) { + // Factor generating the MessagePort JS constructor into its own piece + // of code, because it is needed early on in the child environment setup. + Local templ = env->message_port_constructor_template(); + if (!templ.IsEmpty()) + return templ->GetFunction(context); + + { + Local m = env->NewFunctionTemplate(MessagePort::New); + m->SetClassName(env->message_port_constructor_string()); + m->InstanceTemplate()->SetInternalFieldCount(1); + + AsyncWrap::AddWrapMethods(env, m); + + env->SetProtoMethod(m, "postMessage", MessagePort::PostMessage); + env->SetProtoMethod(m, "start", MessagePort::Start); + env->SetProtoMethod(m, "stop", MessagePort::Stop); + env->SetProtoMethod(m, "close", HandleWrap::Close); + env->SetProtoMethod(m, "unref", HandleWrap::Unref); + env->SetProtoMethod(m, "ref", HandleWrap::Ref); + env->SetProtoMethod(m, "hasRef", HandleWrap::HasRef); + + env->set_message_port_constructor_template(m); + } + + return GetMessagePortConstructor(env, context); +} + +namespace { + +static void MessageChannel(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + if (!args.IsConstructCall()) { + THROW_ERR_CONSTRUCT_CALL_REQUIRED(env); + return; + } + + Local context = args.This()->CreationContext(); + Context::Scope context_scope(context); + + MessagePort* port1 = MessagePort::New(env, context); + MessagePort* port2 = MessagePort::New(env, context); + MessagePort::Entangle(port1, port2); + + args.This()->Set(env->context(), env->port1_string(), port1->object()) + .FromJust(); + args.This()->Set(env->context(), env->port2_string(), port2->object()) + .FromJust(); +} + +static void InitMessaging(Local target, + Local unused, + Local context, + void* priv) { + Environment* env = Environment::GetCurrent(context); + + { + Local message_channel_string = + FIXED_ONE_BYTE_STRING(env->isolate(), "MessageChannel"); + Local templ = env->NewFunctionTemplate(MessageChannel); + templ->SetClassName(message_channel_string); + target->Set(env->context(), + message_channel_string, + templ->GetFunction(context).ToLocalChecked()).FromJust(); + } + + target->Set(context, + env->message_port_constructor_string(), + GetMessagePortConstructor(env, context).ToLocalChecked()) + .FromJust(); +} + +} // anonymous namespace + +} // namespace worker +} // namespace node + +NODE_MODULE_CONTEXT_AWARE_INTERNAL(messaging, node::worker::InitMessaging) diff --git a/src/node_messaging.h b/src/node_messaging.h new file mode 100644 index 00000000000000..7bd60163ea167c --- /dev/null +++ b/src/node_messaging.h @@ -0,0 +1,167 @@ +#ifndef SRC_NODE_MESSAGING_H_ +#define SRC_NODE_MESSAGING_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "env.h" +#include "node_mutex.h" +#include +#include + +namespace node { +namespace worker { + +class MessagePortData; +class MessagePort; + +// Represents a single communication message. +class Message { + public: + explicit Message(MallocedBuffer&& payload = MallocedBuffer()); + + Message(Message&& other) = default; + Message& operator=(Message&& other) = default; + Message& operator=(const Message&) = delete; + Message(const Message&) = delete; + + // Deserialize the contained JS value. May only be called once, and only + // after Serialize() has been called (e.g. by another thread). + v8::MaybeLocal Deserialize(Environment* env, + v8::Local context); + + // Serialize a JS value, and optionally transfer objects, into this message. + // The Message object retains ownership of all transferred objects until + // deserialization. + v8::Maybe Serialize(Environment* env, + v8::Local context, + v8::Local input, + v8::Local transfer_list); + + private: + MallocedBuffer main_message_buf_; + std::vector> array_buffer_contents_; + + friend class MessagePort; +}; + +// This contains all data for a `MessagePort` instance that is not tied to +// a specific Environment/Isolate/event loop, for easier transfer between those. +class MessagePortData { + public: + explicit MessagePortData(MessagePort* owner); + ~MessagePortData(); + + MessagePortData(MessagePortData&& other) = delete; + MessagePortData& operator=(MessagePortData&& other) = delete; + MessagePortData(const MessagePortData& other) = delete; + MessagePortData& operator=(const MessagePortData& other) = delete; + + // Add a message to the incoming queue and notify the receiver. + // This may be called from any thread. + void AddToIncomingQueue(Message&& message); + + // Returns true if and only this MessagePort is currently not entangled + // with another message port. + bool IsSiblingClosed() const; + + // Turns `a` and `b` into siblings, i.e. connects the sending side of one + // to the receiving side of the other. This is not thread-safe. + static void Entangle(MessagePortData* a, MessagePortData* b); + + // Removes any possible sibling. This is thread-safe (it acquires both + // `sibling_mutex_` and `mutex_`), and has to be because it is called once + // the corresponding JS handle handle wants to close + // which can happen on either side of a worker. + void Disentangle(); + + private: + // After disentangling this message port, the owner handle (if any) + // is asynchronously triggered, so that it can close down naturally. + void PingOwnerAfterDisentanglement(); + + // This mutex protects all fields below it, with the exception of + // sibling_. + mutable Mutex mutex_; + bool receiving_messages_ = false; + std::list incoming_messages_; + MessagePort* owner_ = nullptr; + // This mutex protects the sibling_ field and is shared between two entangled + // MessagePorts. If both mutexes are acquired, this one needs to be + // acquired first. + std::shared_ptr sibling_mutex_ = std::make_shared(); + MessagePortData* sibling_ = nullptr; + + friend class MessagePort; +}; + +// A message port that receives messages from other threads, including +// the uv_async_t handle that is used to notify the current event loop of +// new incoming messages. +class MessagePort : public HandleWrap { + public: + // Create a new MessagePort. The `context` argument specifies the Context + // instance that is used for creating the values emitted from this port. + MessagePort(Environment* env, + v8::Local context, + v8::Local wrap); + ~MessagePort(); + + // Create a new message port instance, optionally over an existing + // `MessagePortData` object. + static MessagePort* New(Environment* env, + v8::Local context, + std::unique_ptr data = nullptr); + + // Send a message, i.e. deliver it into the sibling's incoming queue. + // If there is no sibling, i.e. this port is closed, + // this message is silently discarded. + void Send(Message&& message); + void Send(const v8::FunctionCallbackInfo& args); + // Deliver a single message into this port's incoming queue. + void AddToIncomingQueue(Message&& message); + + // Start processing messages on this port as a receiving end. + void Start(); + // Stop processing messages on this port as a receiving end. + void Stop(); + + static void New(const v8::FunctionCallbackInfo& args); + static void PostMessage(const v8::FunctionCallbackInfo& args); + static void Start(const v8::FunctionCallbackInfo& args); + static void Stop(const v8::FunctionCallbackInfo& args); + + // Turns `a` and `b` into siblings, i.e. connects the sending side of one + // to the receiving side of the other. This is not thread-safe. + static void Entangle(MessagePort* a, MessagePort* b); + static void Entangle(MessagePort* a, MessagePortData* b); + + // Detach this port's data for transferring. After this, the MessagePortData + // is no longer associated with this handle, although it can still receive + // messages. + std::unique_ptr Detach(); + + bool IsSiblingClosed() const; + + size_t self_size() const override; + + private: + void OnClose() override; + void OnMessage(); + void TriggerAsync(); + inline uv_async_t* async(); + + std::unique_ptr data_ = nullptr; + + friend class MessagePortData; +}; + +v8::MaybeLocal GetMessagePortConstructor( + Environment* env, v8::Local context); + +} // namespace worker +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + + +#endif // SRC_NODE_MESSAGING_H_ diff --git a/src/util.h b/src/util.h index e272286d3e4b96..fade27458f3e16 100644 --- a/src/util.h +++ b/src/util.h @@ -436,8 +436,11 @@ struct MallocedBuffer { return ret; } + inline bool is_empty() const { return data == nullptr; } + MallocedBuffer() : data(nullptr) {} explicit MallocedBuffer(size_t size) : data(Malloc(size)), size(size) {} + MallocedBuffer(char* data, size_t size) : data(data), size(size) {} MallocedBuffer(MallocedBuffer&& other) : data(other.data), size(other.size) { other.data = nullptr; } diff --git a/test/parallel/test-message-channel.js b/test/parallel/test-message-channel.js new file mode 100644 index 00000000000000..0facaa1d835ea8 --- /dev/null +++ b/test/parallel/test-message-channel.js @@ -0,0 +1,26 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { MessageChannel } = require('worker'); + +{ + const channel = new MessageChannel(); + + channel.port1.on('message', common.mustCall(({ typedArray }) => { + assert.deepStrictEqual(typedArray, new Uint8Array([0, 1, 2, 3, 4])); + })); + + const typedArray = new Uint8Array([0, 1, 2, 3, 4]); + channel.port2.postMessage({ typedArray }, [ typedArray.buffer ]); + assert.strictEqual(typedArray.buffer.byteLength, 0); + channel.port2.close(); +} + +{ + const channel = new MessageChannel(); + + channel.port1.on('close', common.mustCall()); + channel.port2.on('close', common.mustCall()); + channel.port2.close(); +} diff --git a/test/parallel/test-message-port-arraybuffer.js b/test/parallel/test-message-port-arraybuffer.js new file mode 100644 index 00000000000000..4abeb585b4fb15 --- /dev/null +++ b/test/parallel/test-message-port-arraybuffer.js @@ -0,0 +1,20 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { MessageChannel } = require('worker'); + +{ + const { port1, port2 } = new MessageChannel(); + + const arrayBuffer = new ArrayBuffer(40); + const typedArray = new Uint32Array(arrayBuffer); + typedArray[0] = 0x12345678; + + port1.postMessage(typedArray, [ arrayBuffer ]); + port2.on('message', common.mustCall((received) => { + assert.strictEqual(received[0], 0x12345678); + port2.close(common.mustCall()); + })); +} diff --git a/test/parallel/test-message-port.js b/test/parallel/test-message-port.js new file mode 100644 index 00000000000000..8a7f3805200fa3 --- /dev/null +++ b/test/parallel/test-message-port.js @@ -0,0 +1,56 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { MessageChannel, MessagePort } = require('worker'); + +{ + const { port1, port2 } = new MessageChannel(); + assert(port1 instanceof MessagePort); + assert(port2 instanceof MessagePort); + + const input = { a: 1 }; + port1.postMessage(input); + port2.on('message', common.mustCall((received) => { + assert.deepStrictEqual(received, input); + port2.close(common.mustCall()); + })); +} + +{ + const { port1, port2 } = new MessageChannel(); + + const input = { a: 1 }; + port1.postMessage(input); + // Check that the message still gets delivered if `port2` has its + // `on('message')` handler attached at a later point in time. + setImmediate(() => { + port2.on('message', common.mustCall((received) => { + assert.deepStrictEqual(received, input); + port2.close(common.mustCall()); + })); + }); +} + +{ + const { port1, port2 } = new MessageChannel(); + + const input = { a: 1 }; + + const dummy = common.mustNotCall(); + // Check that the message still gets delivered if `port2` has its + // `on('message')` handler attached at a later point in time, even if a + // listener was removed previously. + port2.addListener('message', dummy); + setImmediate(() => { + port2.removeListener('message', dummy); + port1.postMessage(input); + setImmediate(() => { + port2.on('message', common.mustCall((received) => { + assert.deepStrictEqual(received, input); + port2.close(common.mustCall()); + })); + }); + }); +} diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index 971296915ceecb..84a3e3b1f4dc05 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -35,7 +35,9 @@ common.crashOnUnhandledRejection(); delete providers.HTTP2STREAM; delete providers.HTTP2PING; delete providers.HTTP2SETTINGS; + // TODO(addaleax): Test for these delete providers.STREAMPIPE; + delete providers.MESSAGEPORT; const objKeys = Object.keys(providers); if (objKeys.length > 0) diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js index e7c7aa69da5e95..be72893832373a 100644 --- a/tools/doc/type-parser.js +++ b/tools/doc/type-parser.js @@ -117,7 +117,9 @@ const customTypesMap = { 'Tracing': 'tracing.html#tracing_tracing_object', 'URL': 'url.html#url_the_whatwg_url_api', - 'URLSearchParams': 'url.html#url_class_urlsearchparams' + 'URLSearchParams': 'url.html#url_class_urlsearchparams', + + 'MessagePort': 'worker.html#worker_class_messageport' }; const arrayPart = /(?:\[])+$/; From f2e297b86f4e8ebd5e48081c70e09144a84fcafb Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sat, 2 Jun 2018 13:15:36 +0200 Subject: [PATCH 06/26] fixup! worker: implement `MessagePort` and `MessageChannel` --- lib/internal/worker.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/internal/worker.js b/lib/internal/worker.js index 03caa07a4b1eeb..73f7525aa73cc2 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -5,6 +5,8 @@ const util = require('util'); const { internalBinding } = require('internal/bootstrap/loaders'); const { MessagePort, MessageChannel } = internalBinding('messaging'); +const { handle_onclose } = internalBinding('symbols'); + util.inherits(MessagePort, EventEmitter); const kOnMessageListener = Symbol('kOnMessageListener'); @@ -64,8 +66,8 @@ function onclose() { this.emit('close'); } -Object.defineProperty(MessagePort.prototype, '_onclose', { - enumerable: true, +Object.defineProperty(MessagePort.prototype, handle_onclose, { + enumerable: false, writable: false, value: onclose }); From ef24c5ce7b8aaf64718de14d674a0afe1fae74c6 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sat, 7 Oct 2017 14:39:02 -0700 Subject: [PATCH 07/26] worker: support MessagePort passing in messages Support passing `MessagePort` instances through other `MessagePort`s, as expected by the `MessagePort` spec. Thanks to Stephen Belanger for reviewing this change in its original PR. Refs: https://github.com/ayojs/ayo/pull/106 --- doc/api/errors.md | 12 +++ doc/api/worker.md | 2 +- src/node_errors.h | 5 ++ src/node_messaging.cc | 80 ++++++++++++++++++- src/node_messaging.h | 5 ++ ...-message-port-message-port-transferring.js | 23 ++++++ 6 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 test/parallel/test-message-port-message-port-transferring.js diff --git a/doc/api/errors.md b/doc/api/errors.md index f3e5939f258576..0a49757da3e39e 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -629,6 +629,12 @@ An operation outside the bounds of a `Buffer` was attempted. An attempt has been made to create a `Buffer` larger than the maximum allowed size. + +### ERR_CANNOT_TRANSFER_OBJECT + +The value passed to `postMessage()` contained an object that is not supported +for transferring. + ### ERR_CANNOT_WATCH_SIGINT @@ -1294,6 +1300,12 @@ strict compliance with the API specification (which in some cases may accept `func(undefined)` and `func()` are treated identically, and the [`ERR_INVALID_ARG_TYPE`][] error code may be used instead. + +### ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST + +A `MessagePort` was found in the object passed to a `postMessage()` call, +but not provided in the `transferList` for that call. + ### ERR_MISSING_MODULE diff --git a/doc/api/worker.md b/doc/api/worker.md index 4724714cd62f26..6a391c5a9e3b19 100644 --- a/doc/api/worker.md +++ b/doc/api/worker.md @@ -83,7 +83,7 @@ the [HTML structured clone algorithm][]. In particular, it may contain circular references and objects like typed arrays that the `JSON` API is not able to stringify. -`transferList` may be a list of `ArrayBuffer` objects. +`transferList` may be a list of `ArrayBuffer` and `MessagePort` objects. After transferring, they will not be usable on the sending side of the channel anymore (even if they are not contained in `value`). diff --git a/src/node_errors.h b/src/node_errors.h index 81169d241bc226..9cadb0e18533ca 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -23,6 +23,7 @@ namespace node { #define ERRORS_WITH_CODE(V) \ V(ERR_BUFFER_OUT_OF_BOUNDS, RangeError) \ V(ERR_BUFFER_TOO_LARGE, Error) \ + V(ERR_CANNOT_TRANSFER_OBJECT, TypeError) \ V(ERR_CLOSED_MESSAGE_PORT, Error) \ V(ERR_CONSTRUCT_CALL_REQUIRED, Error) \ V(ERR_INDEX_OUT_OF_RANGE, RangeError) \ @@ -31,6 +32,7 @@ namespace node { V(ERR_INVALID_TRANSFER_OBJECT, TypeError) \ V(ERR_MEMORY_ALLOCATION_FAILED, Error) \ V(ERR_MISSING_ARGS, TypeError) \ + V(ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST, TypeError) \ V(ERR_MISSING_MODULE, Error) \ V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \ V(ERR_SCRIPT_EXECUTION_TIMEOUT, Error) \ @@ -57,11 +59,14 @@ namespace node { // Errors with predefined static messages #define PREDEFINED_ERROR_MESSAGES(V) \ + V(ERR_CANNOT_TRANSFER_OBJECT, "Cannot transfer object of unsupported type")\ V(ERR_CLOSED_MESSAGE_PORT, "Cannot send data on closed MessagePort") \ V(ERR_CONSTRUCT_CALL_REQUIRED, "Cannot call constructor without `new`") \ V(ERR_INDEX_OUT_OF_RANGE, "Index out of range") \ V(ERR_INVALID_TRANSFER_OBJECT, "Found invalid object in transferList") \ V(ERR_MEMORY_ALLOCATION_FAILED, "Failed to allocate memory") \ + V(ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST, \ + "MessagePort was found in message but not listed in transferList") \ V(ERR_SCRIPT_EXECUTION_INTERRUPTED, \ "Script execution was interrupted by `SIGINT`") diff --git a/src/node_messaging.cc b/src/node_messaging.cc index c6e701c7d94426..1c6551e0969f3e 100644 --- a/src/node_messaging.cc +++ b/src/node_messaging.cc @@ -41,14 +41,27 @@ namespace { // `MessagePort`s and `SharedArrayBuffer`s, and make new JS objects out of them. class DeserializerDelegate : public ValueDeserializer::Delegate { public: - DeserializerDelegate(Message* m, Environment* env) - : env_(env), msg_(m) {} + DeserializerDelegate(Message* m, + Environment* env, + const std::vector& message_ports) + : env_(env), msg_(m), message_ports_(message_ports) {} + + MaybeLocal ReadHostObject(Isolate* isolate) override { + // Currently, only MessagePort hosts objects are supported, so identifying + // by the index in the message's MessagePort array is sufficient. + uint32_t id; + if (!deserializer->ReadUint32(&id)) + return MaybeLocal(); + CHECK_LE(id, message_ports_.size()); + return message_ports_[id]->object(); + }; ValueDeserializer* deserializer = nullptr; private: Environment* env_; Message* msg_; + const std::vector& message_ports_; }; } // anonymous namespace @@ -58,7 +71,23 @@ MaybeLocal Message::Deserialize(Environment* env, EscapableHandleScope handle_scope(env->isolate()); Context::Scope context_scope(context); - DeserializerDelegate delegate(this, env); + // Create all necessary MessagePort handles. + std::vector ports(message_ports_.size()); + for (uint32_t i = 0; i < message_ports_.size(); ++i) { + ports[i] = MessagePort::New(env, + context, + std::move(message_ports_[i])); + if (ports[i] == nullptr) { + for (MessagePort* port : ports) { + // This will eventually release the MessagePort object itself. + port->Close(); + } + return MaybeLocal(); + } + } + message_ports_.clear(); + + DeserializerDelegate delegate(this, env, ports); ValueDeserializer deserializer( env->isolate(), reinterpret_cast(main_message_buf_.data), @@ -83,6 +112,10 @@ MaybeLocal Message::Deserialize(Environment* env, deserializer.ReadValue(context).FromMaybe(Local())); } +void Message::AddMessagePort(std::unique_ptr&& data) { + message_ports_.emplace_back(std::move(data)); +} + namespace { // This tells V8 how to serialize objects that it does not understand @@ -97,12 +130,43 @@ class SerializerDelegate : public ValueSerializer::Delegate { env_->isolate()->ThrowException(Exception::Error(message)); } + Maybe WriteHostObject(Isolate* isolate, Local object) override { + if (env_->message_port_constructor_template()->HasInstance(object)) { + return WriteMessagePort(Unwrap(object)); + } + + THROW_ERR_CANNOT_TRANSFER_OBJECT(env_); + return Nothing(); + } + + void Finish() { + // Only close the MessagePort handles and actually transfer them + // once we know that serialization succeeded. + for (MessagePort* port : ports_) { + port->Close(); + msg_->AddMessagePort(port->Detach()); + } + } + ValueSerializer* serializer = nullptr; private: + Maybe WriteMessagePort(MessagePort* port) { + for (uint32_t i = 0; i < ports_.size(); i++) { + if (ports_[i] == port) { + serializer->WriteUint32(i); + return Just(true); + } + } + + THROW_ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST(env_); + return Nothing(); + } + Environment* env_; Local context_; Message* msg_; + std::vector ports_; friend class worker::Message; }; @@ -131,7 +195,7 @@ Maybe Message::Serialize(Environment* env, Local entry; if (!transfer_list->Get(context, i).ToLocal(&entry)) return Nothing(); - // Currently, we support ArrayBuffers. + // Currently, we support ArrayBuffers and MessagePorts. if (entry->IsArrayBuffer()) { Local ab = entry.As(); // If we cannot render the ArrayBuffer unusable in this Isolate and @@ -144,6 +208,12 @@ Maybe Message::Serialize(Environment* env, array_buffers.push_back(ab); serializer.TransferArrayBuffer(id, ab); continue; + } else if (env->message_port_constructor_template() + ->HasInstance(entry)) { + MessagePort* port = Unwrap(entry.As()); + CHECK_NE(port, nullptr); + delegate.ports_.push_back(port); + continue; } THROW_ERR_INVALID_TRANSFER_OBJECT(env); @@ -167,6 +237,8 @@ Maybe Message::Serialize(Environment* env, contents.ByteLength() }); } + delegate.Finish(); + // The serializer gave us a buffer allocated using `malloc()`. std::pair data = serializer.Release(); main_message_buf_ = diff --git a/src/node_messaging.h b/src/node_messaging.h index 7bd60163ea167c..074267bb67c2dd 100644 --- a/src/node_messaging.h +++ b/src/node_messaging.h @@ -37,9 +37,14 @@ class Message { v8::Local input, v8::Local transfer_list); + // Internal method of Message that is called once serialization finishes + // and that transfers ownership of `data` to this message. + void AddMessagePort(std::unique_ptr&& data); + private: MallocedBuffer main_message_buf_; std::vector> array_buffer_contents_; + std::vector> message_ports_; friend class MessagePort; }; diff --git a/test/parallel/test-message-port-message-port-transferring.js b/test/parallel/test-message-port-message-port-transferring.js new file mode 100644 index 00000000000000..a7490b3678ac74 --- /dev/null +++ b/test/parallel/test-message-port-message-port-transferring.js @@ -0,0 +1,23 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { MessageChannel } = require('worker'); + +{ + const { port1: basePort1, port2: basePort2 } = new MessageChannel(); + const { + port1: transferredPort1, port2: transferredPort2 + } = new MessageChannel(); + + basePort1.postMessage({ transferredPort1 }, [ transferredPort1 ]); + basePort2.on('message', common.mustCall(({ transferredPort1 }) => { + transferredPort1.postMessage('foobar'); + transferredPort2.on('message', common.mustCall((msg) => { + assert.strictEqual(msg, 'foobar'); + transferredPort1.close(common.mustCall()); + basePort1.close(common.mustCall()); + })); + })); +} From a57c54fdba0c454521f42a6d7849327a4506f8e1 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sun, 13 May 2018 19:39:32 +0200 Subject: [PATCH 08/26] worker: add `SharedArrayBuffer` sharing Logic is added to the `MessagePort` mechanism that attaches hidden objects to those instances when they are transferred that track their lifetime and maintain a reference count, to make sure that memory is freed at the appropriate times. Thanks to Stephen Belanger for reviewing this change in its original PR. Refs: https://github.com/ayojs/ayo/pull/106 --- doc/api/worker.md | 15 +++- node.gyp | 2 + src/env.h | 2 + src/node_errors.h | 5 +- src/node_messaging.cc | 57 ++++++++++++- src/node_messaging.h | 6 +- src/sharedarraybuffer_metadata.cc | 129 ++++++++++++++++++++++++++++++ src/sharedarraybuffer_metadata.h | 67 ++++++++++++++++ 8 files changed, 274 insertions(+), 9 deletions(-) create mode 100644 src/sharedarraybuffer_metadata.cc create mode 100644 src/sharedarraybuffer_metadata.h diff --git a/doc/api/worker.md b/doc/api/worker.md index 6a391c5a9e3b19..974ff2e46710db 100644 --- a/doc/api/worker.md +++ b/doc/api/worker.md @@ -85,14 +85,16 @@ to stringify. `transferList` may be a list of `ArrayBuffer` and `MessagePort` objects. After transferring, they will not be usable on the sending side of the channel -anymore (even if they are not contained in `value`). +anymore (even if they are not contained in `value`). Unlike with +[child processes][], transferring handles such as network sockets is currently +not supported. + +If `value` contains [`SharedArrayBuffer`][] instances, those will be accessible +from either thread. They cannot be listed in `transferList`. `value` may still contain `ArrayBuffer` instances that are not in `transferList`; in that case, the underlying memory is copied rather than moved. -For more information on the serialization and deserialization mechanisms -behind this API, see the [serialization API of the `v8` module][v8.serdes]. - Because the object cloning uses the structured clone algorithm, non-enumerable properties, property accessors, and object prototypes are not preserved. In particular, [`Buffer`][] objects will be read as @@ -101,6 +103,9 @@ plain [`Uint8Array`][]s on the receiving side. The message object will be cloned immediately, and can be modified after posting without having side effects. +For more information on the serialization and deserialization mechanisms +behind this API, see the [serialization API of the `v8` module][v8.serdes]. + ### port.ref() + +* {boolean} + +Is `true` if this code is not running inside of a [`Worker`][] thread. + +## worker.parentPort + + +* {null|MessagePort} + +If this thread was spawned as a [`Worker`][], this will be a [`MessagePort`][] +allowing communication with the parent thread. Messages sent using +`parentPort.postMessage()` will be available in the parent thread +using `worker.on('message')`, and messages sent from the parent thread +using `worker.postMessage()` will be available in this thread using +`parentPort.on('message')`. + +## worker.threadId + + +* {integer} + +An integer identifier for the current thread. On the corresponding worker object +(if there is any), it is available as [`worker.threadId`][]. + +## worker.workerData + + +An arbitrary JavaScript value that contains a clone of the data passed +to this thread’s `Worker` constructor. + ## Class: MessageChannel + +The `Worker` class represents an independent JavaScript execution thread. +Most Node.js APIs are available inside of it. + +Notable differences inside a Worker environment are: + +- The [`process.stdin`][], [`process.stdout`][] and [`process.stderr`][] + properties are set to `null`. +- The [`require('worker').isMainThread`][] property is set to `false`. +- The [`require('worker').postMessage()`][] method is available and the + [`require('worker').on('workerMessage')`][] event will be emitted. +- [`process.exit()`][] does not stop the whole program, just the single thread, + and [`process.abort()`][] is not available. +- [`process.chdir()`][] and `process` methods that set group or user ids + are not available. +- [`process.env`][] is a read-only reference to the environment variables. +- [`process.title`][] cannot be modified. +- Signals will not be delivered through [`process.on('...')`][Signals events]. +- Execution may stop at any point as a result of [`worker.terminate()`][] + being invoked. +- IPC channels from parent processes are not accessible. + +Currently, the following differences also exist until they are addressed: + +- The [`inspector`][] module is not available yet. +- Native addons are not supported yet. + +Creating `Worker` instances inside of other `Worker`s is possible. + +Like [Web Workers][] and the [`cluster` module][], two-way communication can be +achieved through inter-thread message passing. Internally, a `Worker` has a +built-in pair of [`MessagePort`][]s that are already associated with each other +when the `Worker` is created. While the `MessagePort` objects are not directly +exposed, their functionalities are exposed through [`worker.postMessage()`][] +and the [`worker.on('message')`][] event on the `Worker` object for the parent +thread, and [`require('worker').postMessage()`][] and the +[`require('worker').on('workerMessage')`][] on `require('worker')` for the +child thread. + +To create custom messaging channels (which is encouraged over using the default +global channel because it facilitates separation of concerns), users can create +a `MessageChannel` object on either thread and pass one of the +`MessagePort`s on that `MessageChannel` to the other thread through a +pre-existing channel, such as the global one. + +See [`port.postMessage()`][] for more information on how messages are passed, +and what kind of JavaScript values can be successfully transported through +the thread barrier. + +For example: + +```js +const assert = require('assert'); +const { Worker, MessageChannel, MessagePort, isMainThread } = require('worker'); +if (isMainThread) { + const worker = new Worker(__filename); + const subChannel = new MessageChannel(); + worker.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1]); + subChannel.port2.on('message', (value) => { + console.log('received:', value); + }); +} else { + require('worker').once('workerMessage', (value) => { + assert(value.hereIsYourPort instanceof MessagePort); + value.hereIsYourPort.postMessage('the worker is sending this'); + value.hereIsYourPort.close(); + }); +} +``` + +### new Worker(filename, options) + +* `filename` {string} The absolute path to the Worker’s main script. + If `options.eval` is true, this is a string containing JavaScript code rather + than a path. +* `options` {Object} + * `eval` {boolean} If true, interpret the first argument to the constructor + as a script that is executed once the worker is online. + * `data` {any} Any JavaScript value that will be cloned and made + available as [`require('worker').workerData`][]. The cloning will occur as + described in the [HTML structured clone algorithm][], and an error will be + thrown if the object cannot be cloned (e.g. because it contains + `function`s). + +### Event: 'error' + + +* `err` {Error} + +The `'error'` event is emitted if the worker thread throws an uncaught +exception. In that case, the worker will be terminated. + +### Event: 'exit' + + +* `exitCode` {integer} + +The `'exit'` event is emitted once the worker has stopped. If the worker +exited by calling [`process.exit()`][], the `exitCode` parameter will be the +passed exit code. If the worker was terminated, the `exitCode` parameter will +be `1`. + +### Event: 'message' + + +* `value` {any} The transmitted value + +The `'message'` event is emitted when the worker thread has invoked +[`require('worker').postMessage()`][]. See the [`port.on('message')`][] event +for more details. + +### Event: 'online' + + +The `'online'` event is emitted when the worker thread has started executing +JavaScript code. + +### worker.postMessage(value[, transferList]) + + +* `value` {any} +* `transferList` {Object[]} + +Send a message to the worker that will be received via +[`require('worker').on('workerMessage')`][]. See [`port.postMessage()`][] for +more details. + +### worker.ref() + + +Opposite of `unref()`, calling `ref()` on a previously `unref()`ed worker will +*not* let the program exit if it's the only active handle left (the default +behavior). If the worker is `ref()`ed, calling `ref()` again will have +no effect. + +### worker.terminate([callback]) + + +* `callback` {Function} + +Stop all JavaScript execution in the worker thread as soon as possible. +`callback` is an optional function that is invoked once this operation is known +to have completed. + +**Warning**: Currently, not all code in the internals of Node.js is prepared to +expect termination at arbitrary points in time and may crash if it encounters +that condition. Consequently, you should currently only call `.terminate()` if +it is known that the Worker thread is not accessing Node.js core modules other +than what is exposed in the `worker` module. + +### worker.threadId + + +* {integer} + +An integer identifier for the referenced thread. Inside the worker thread, +it is available as [`require('worker').threadId`][]. + +### worker.unref() + + +Calling `unref()` on a worker will allow the thread to exit if this is the only +active handle in the event system. If the worker is already `unref()`ed calling +`unref()` again will have no effect. + [`Buffer`]: buffer.html -[child processes]: child_process.html [`EventEmitter`]: events.html [`MessagePort`]: #worker_class_messageport [`port.postMessage()`]: #worker_port_postmessage_value_transferlist +[`Worker`]: #worker_class_worker +[`worker.terminate()`]: #worker_worker_terminate_callback +[`worker.postMessage()`]: #worker_worker_postmessage_value_transferlist_1 +[`worker.on('message')`]: #worker_event_message_1 +[`worker.threadId`]: #worker_worker_threadid_1 +[`port.on('message')`]: #worker_event_message +[`process.exit()`]: process.html#process_process_exit_code +[`process.abort()`]: process.html#process_process_abort +[`process.chdir()`]: process.html#process_process_chdir_directory +[`process.env`]: process.html#process_process_env +[`process.stdin`]: process.html#process_process_stdin +[`process.stderr`]: process.html#process_process_stderr +[`process.stdout`]: process.html#process_process_stdout +[`process.title`]: process.html#process_process_title +[`require('worker').workerData`]: #worker_worker_workerdata +[`require('worker').on('workerMessage')`]: #worker_event_workermessage +[`require('worker').postMessage()`]: #worker_worker_postmessage_value_transferlist +[`require('worker').isMainThread`]: #worker_worker_ismainthread +[`require('worker').threadId`]: #worker_worker_threadid +[`cluster` module]: cluster.html +[`inspector`]: inspector.html [v8.serdes]: v8.html#v8_serialization_api [`SharedArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer +[Signals events]: process.html#process_signal_events [`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array [browser `MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort +[child processes]: child_process.html [HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm +[Web Workers]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API diff --git a/lib/inspector.js b/lib/inspector.js index 3285c1040a7132..f4ec71fd6c2105 100644 --- a/lib/inspector.js +++ b/lib/inspector.js @@ -12,7 +12,7 @@ const { const util = require('util'); const { Connection, open, url } = process.binding('inspector'); -if (!Connection) +if (!Connection || !require('internal/worker').isMainThread) throw new ERR_INSPECTOR_NOT_AVAILABLE(); const connectionSymbol = Symbol('connectionProperty'); diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index 6477c2d8282f43..4817ec110a99e5 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -24,6 +24,7 @@ _shouldAbortOnUncaughtToggle }, { internalBinding, NativeModule }) { const exceptionHandlerState = { captureFn: null }; + const isMainThread = internalBinding('worker').threadId === 0; function startup() { const EventEmitter = NativeModule.require('events'); @@ -100,7 +101,9 @@ NativeModule.require('internal/inspector_async_hook').setup(); } - _process.setupChannel(); + if (isMainThread) + _process.setupChannel(); + _process.setupRawDebug(_rawDebug); const browserGlobals = !process._noBrowserGlobals; @@ -175,8 +178,11 @@ // are running from a script and running the REPL - but there are a few // others like the debugger or running --eval arguments. Here we decide // which mode we run in. - - if (NativeModule.exists('_third_party_main')) { + if (internalBinding('worker').getEnvMessagePort() !== undefined) { + // This means we are in a Worker context, and any script execution + // will be directed by the worker module. + NativeModule.require('internal/worker').setupChild(evalScript); + } else if (NativeModule.exists('_third_party_main')) { // To allow people to extend Node in different ways, this hook allows // one to drop a file lib/_third_party_main.js into the build // directory which will be executed instead of Node's normal loading. @@ -542,7 +548,7 @@ return `process.binding('inspector').callAndPauseOnStart(${fn}, {})`; } - function evalScript(name) { + function evalScript(name, body = wrapForBreakOnFirstLine(process._eval)) { const CJSModule = NativeModule.require('internal/modules/cjs/loader'); const path = NativeModule.require('path'); const cwd = tryGetCwd(path); @@ -550,7 +556,6 @@ const module = new CJSModule(name); module.filename = path.join(cwd, name); module.paths = CJSModule._nodeModulePaths(cwd); - const body = wrapForBreakOnFirstLine(process._eval); const script = `global.__filename = ${JSON.stringify(name)};\n` + 'global.exports = exports;\n' + 'global.module = module;\n' + diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 89c0139f8b6fde..d59531debbd042 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -844,4 +844,9 @@ E('ERR_VM_MODULE_NOT_LINKED', 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_WORKER_NEED_ABSOLUTE_PATH', + 'The worker script filename must be an absolute path. Received "%s"', + TypeError); +E('ERR_WORKER_UNSERIALIZABLE_ERROR', + 'Serializing an uncaught exception failed', Error); E('ERR_ZLIB_INITIALIZATION_FAILED', 'Initialization failed', Error); diff --git a/lib/internal/process.js b/lib/internal/process.js index 0f0e40d6a0cdbc..f01be32be4b6a3 100644 --- a/lib/internal/process.js +++ b/lib/internal/process.js @@ -16,6 +16,7 @@ const util = require('util'); const constants = process.binding('constants').os.signals; const assert = require('assert').strict; const { deprecate } = require('internal/util'); +const { isMainThread } = require('internal/worker'); process.assert = deprecate( function(x, msg) { @@ -186,6 +187,11 @@ function setupKillAndExit() { function setupSignalHandlers() { + if (!isMainThread) { + // Worker threads don't receive signals. + return; + } + const signalWraps = Object.create(null); let Signal; diff --git a/lib/internal/process/methods.js b/lib/internal/process/methods.js index 91aca398b346d4..9a954f6a9b93cf 100644 --- a/lib/internal/process/methods.js +++ b/lib/internal/process/methods.js @@ -8,11 +8,18 @@ const { validateMode, validateUint32 } = require('internal/validators'); +const { + isMainThread +} = require('internal/worker'); function setupProcessMethods(_chdir, _cpuUsage, _hrtime, _memoryUsage, _rawDebug, _umask, _initgroups, _setegid, _seteuid, _setgid, _setuid, _setgroups) { // Non-POSIX platforms like Windows don't have certain methods. + // Workers also lack these methods since they change process-global state. + if (!isMainThread) + return; + if (_setgid !== undefined) { setupPosixMethods(_initgroups, _setegid, _seteuid, _setgid, _setuid, _setgroups); diff --git a/lib/internal/process/stdio.js b/lib/internal/process/stdio.js index eaba4dfca13a47..76e6ab85140535 100644 --- a/lib/internal/process/stdio.js +++ b/lib/internal/process/stdio.js @@ -6,6 +6,7 @@ const { ERR_UNKNOWN_STDIN_TYPE, ERR_UNKNOWN_STREAM_TYPE } = require('internal/errors').codes; +const { isMainThread } = require('internal/worker'); exports.setup = setupStdio; @@ -16,6 +17,8 @@ function setupStdio() { function getStdout() { if (stdout) return stdout; + if (!isMainThread) + return new (require('stream').Writable)({ write(b, e, cb) { cb(); } }); stdout = createWritableStdioStream(1); stdout.destroySoon = stdout.destroy; stdout._destroy = function(er, cb) { @@ -31,6 +34,8 @@ function setupStdio() { function getStderr() { if (stderr) return stderr; + if (!isMainThread) + return new (require('stream').Writable)({ write(b, e, cb) { cb(); } }); stderr = createWritableStdioStream(2); stderr.destroySoon = stderr.destroy; stderr._destroy = function(er, cb) { @@ -46,6 +51,8 @@ function setupStdio() { function getStdin() { if (stdin) return stdin; + if (!isMainThread) + return new (require('stream').Readable)({ read() { this.push(null); } }); const tty_wrap = process.binding('tty_wrap'); const fd = 0; diff --git a/lib/internal/util/inspector.js b/lib/internal/util/inspector.js index 634d3302333584..3dd73415ded862 100644 --- a/lib/internal/util/inspector.js +++ b/lib/internal/util/inspector.js @@ -1,6 +1,8 @@ 'use strict'; -const hasInspector = process.config.variables.v8_enable_inspector === 1; +// TODO(addaleax): Figure out how to integrate the inspector with workers. +const hasInspector = process.config.variables.v8_enable_inspector === 1 && + require('internal/worker').isMainThread; const inspector = hasInspector ? require('inspector') : undefined; let session; diff --git a/lib/internal/worker.js b/lib/internal/worker.js index 73f7525aa73cc2..c982478b9334e8 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -1,24 +1,49 @@ 'use strict'; +const Buffer = require('buffer').Buffer; const EventEmitter = require('events'); +const assert = require('assert'); +const path = require('path'); const util = require('util'); +const { + ERR_INVALID_ARG_TYPE, + ERR_WORKER_NEED_ABSOLUTE_PATH, + ERR_WORKER_UNSERIALIZABLE_ERROR +} = require('internal/errors').codes; const { internalBinding } = require('internal/bootstrap/loaders'); const { MessagePort, MessageChannel } = internalBinding('messaging'); const { handle_onclose } = internalBinding('symbols'); +const { clearAsyncIdStack } = require('internal/async_hooks'); util.inherits(MessagePort, EventEmitter); +const { + Worker: WorkerImpl, + getEnvMessagePort, + threadId +} = internalBinding('worker'); + +const isMainThread = threadId === 0; + const kOnMessageListener = Symbol('kOnMessageListener'); +const kHandle = Symbol('kHandle'); +const kPort = Symbol('kPort'); +const kPublicPort = Symbol('kPublicPort'); +const kDispose = Symbol('kDispose'); +const kOnExit = Symbol('kOnExit'); +const kOnMessage = Symbol('kOnMessage'); +const kOnCouldNotSerializeErr = Symbol('kOnCouldNotSerializeErr'); +const kOnErrorMessage = Symbol('kOnErrorMessage'); const debug = util.debuglog('worker'); -// A MessagePort consists of a handle (that wraps around an +// A communication channel consisting of a handle (that wraps around an // uv_async_t) which can receive information from other threads and emits // .onmessage events, and a function used for sending data to a MessagePort // in some other thread. MessagePort.prototype[kOnMessageListener] = function onmessage(payload) { - debug('received message', payload); + debug(`[${threadId}] received message`, payload); // Emit the deserialized object to userland. this.emit('message', payload); }; @@ -79,6 +104,9 @@ MessagePort.prototype.close = function(cb) { originalClose.call(this); }; +const drainMessagePort = MessagePort.prototype.drain; +delete MessagePort.prototype.drain; + function setupPortReferencing(port, eventEmitter, eventName) { // Keep track of whether there are any workerMessage listeners: // If there are some, ref() the channel so it keeps the event loop alive. @@ -99,7 +127,194 @@ function setupPortReferencing(port, eventEmitter, eventName) { }); } + +class Worker extends EventEmitter { + constructor(filename, options = {}) { + super(); + debug(`[${threadId}] create new worker`, filename, options); + if (typeof filename !== 'string') { + throw new ERR_INVALID_ARG_TYPE('filename', 'string', filename); + } + + if (!options.eval && !path.isAbsolute(filename)) { + throw new ERR_WORKER_NEED_ABSOLUTE_PATH(filename); + } + + // Set up the C++ handle for the worker, as well as some internal wiring. + this[kHandle] = new WorkerImpl(); + this[kHandle].onexit = (code) => this[kOnExit](code); + this[kPort] = this[kHandle].messagePort; + this[kPort].on('message', (data) => this[kOnMessage](data)); + this[kPort].start(); + this[kPort].unref(); + debug(`[${threadId}] created Worker with ID ${this.threadId}`); + + const { port1, port2 } = new MessageChannel(); + this[kPublicPort] = port1; + this[kPublicPort].on('message', (message) => this.emit('message', message)); + setupPortReferencing(this[kPublicPort], this, 'message'); + this[kPort].postMessage({ + type: 'loadScript', + filename, + doEval: !!options.eval, + workerData: options.workerData, + publicPort: port2 + }, [port2]); + // Actually start the new thread now that everything is in place. + this[kHandle].startThread(); + } + + [kOnExit](code) { + debug(`[${threadId}] hears end event for Worker ${this.threadId}`); + drainMessagePort.call(this[kPublicPort]); + this[kDispose](); + this.emit('exit', code); + this.removeAllListeners(); + } + + [kOnCouldNotSerializeErr]() { + this.emit('error', new ERR_WORKER_UNSERIALIZABLE_ERROR()); + } + + [kOnErrorMessage](serialized) { + // This is what is called for uncaught exceptions. + const error = deserializeError(serialized); + this.emit('error', error); + } + + [kOnMessage](message) { + switch (message.type) { + case 'upAndRunning': + return this.emit('online'); + case 'couldNotSerializeError': + return this[kOnCouldNotSerializeErr](); + case 'errorMessage': + return this[kOnErrorMessage](message.error); + } + + assert.fail(`Unknown worker message type ${message.type}`); + } + + [kDispose]() { + this[kHandle].onexit = null; + this[kHandle] = null; + this[kPort] = null; + this[kPublicPort] = null; + } + + postMessage(...args) { + this[kPublicPort].postMessage(...args); + } + + terminate(callback) { + if (this[kHandle] === null) return; + + debug(`[${threadId}] terminates Worker with ID ${this.threadId}`); + + if (typeof callback !== 'undefined') + this.once('exit', (exitCode) => callback(null, exitCode)); + + this[kHandle].stopThread(); + } + + ref() { + if (this[kHandle] === null) return; + + this[kHandle].ref(); + this[kPublicPort].ref(); + } + + unref() { + if (this[kHandle] === null) return; + + this[kHandle].unref(); + this[kPublicPort].unref(); + } + + get threadId() { + if (this[kHandle] === null) return -1; + + return this[kHandle].threadId; + } +} + +let originalFatalException; + +function setupChild(evalScript) { + // Called during bootstrap to set up worker script execution. + debug(`[${threadId}] is setting up worker child environment`); + const port = getEnvMessagePort(); + + const publicWorker = require('worker'); + + port.on('message', (message) => { + if (message.type === 'loadScript') { + const { filename, doEval, workerData, publicPort } = message; + publicWorker.parentPort = publicPort; + setupPortReferencing(publicPort, publicPort, 'message'); + publicWorker.workerData = workerData; + debug(`[${threadId}] starts worker script ${filename} ` + + `(eval = ${eval}) at cwd = ${process.cwd()}`); + port.unref(); + port.postMessage({ type: 'upAndRunning' }); + if (doEval) { + evalScript('[worker eval]', filename); + } else { + process.argv[1] = filename; // script filename + require('module').runMain(); + } + return; + } + + assert.fail(`Unknown worker message type ${message.type}`); + }); + + port.start(); + + originalFatalException = process._fatalException; + process._fatalException = fatalException; + + function fatalException(error) { + debug(`[${threadId}] gets fatal exception`); + let caught = false; + try { + caught = originalFatalException.call(this, error); + } catch (e) { + error = e; + } + debug(`[${threadId}] fatal exception caught = ${caught}`); + + if (!caught) { + let serialized; + try { + serialized = serializeError(error); + } catch {} + debug(`[${threadId}] fatal exception serialized = ${!!serialized}`); + if (serialized) + port.postMessage({ type: 'errorMessage', error: serialized }); + else + port.postMessage({ type: 'couldNotSerializeError' }); + clearAsyncIdStack(); + } + } +} + +// TODO(addaleax): These can be improved a lot. +function serializeError(error) { + return Buffer.from(util.inspect(error), 'utf8'); +} + +function deserializeError(error) { + return Buffer.from(error.buffer, + error.byteOffset, + error.byteLength).toString('utf8'); +} + module.exports = { MessagePort, - MessageChannel + MessageChannel, + threadId, + Worker, + setupChild, + isMainThread }; diff --git a/lib/worker.js b/lib/worker.js index d67fb4efe40a33..0609650cd5731d 100644 --- a/lib/worker.js +++ b/lib/worker.js @@ -1,5 +1,18 @@ 'use strict'; -const { MessagePort, MessageChannel } = require('internal/worker'); +const { + isMainThread, + MessagePort, + MessageChannel, + threadId, + Worker +} = require('internal/worker'); -module.exports = { MessagePort, MessageChannel }; +module.exports = { + isMainThread, + MessagePort, + MessageChannel, + threadId, + Worker, + parentPort: null +}; diff --git a/node.gyp b/node.gyp index 804c102a5b7eab..e9a2cb46a851da 100644 --- a/node.gyp +++ b/node.gyp @@ -349,6 +349,7 @@ 'src/node_v8.cc', 'src/node_stat_watcher.cc', 'src/node_watchdog.cc', + 'src/node_worker.cc', 'src/node_zlib.cc', 'src/node_i18n.cc', 'src/pipe_wrap.cc', @@ -407,6 +408,7 @@ 'src/node_wrap.h', 'src/node_revert.h', 'src/node_i18n.h', + 'src/node_worker.h', 'src/pipe_wrap.h', 'src/tty_wrap.h', 'src/tcp_wrap.h', diff --git a/src/async_wrap.h b/src/async_wrap.h index cf269a4c1f5e1e..b2f96477b490e0 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -67,6 +67,7 @@ namespace node { V(TTYWRAP) \ V(UDPSENDWRAP) \ V(UDPWRAP) \ + V(WORKER) \ V(WRITEWRAP) \ V(ZLIB) diff --git a/src/base_object-inl.h b/src/base_object-inl.h index 3bd854639b2c6d..06a29223973c5d 100644 --- a/src/base_object-inl.h +++ b/src/base_object-inl.h @@ -65,6 +65,14 @@ v8::Local BaseObject::object() { return PersistentToLocal(env_->isolate(), persistent_handle_); } +v8::Local BaseObject::object(v8::Isolate* isolate) { + v8::Local handle = object(); +#ifdef DEBUG + CHECK_EQ(handle->CreationContext()->GetIsolate(), isolate); + CHECK_EQ(env_->isolate(), isolate); +#endif + return handle; +} Environment* BaseObject::env() const { return env_; diff --git a/src/base_object.h b/src/base_object.h index e0b60843401681..38291d598feb1c 100644 --- a/src/base_object.h +++ b/src/base_object.h @@ -43,6 +43,10 @@ class BaseObject { // persistent.IsEmpty() is true. inline v8::Local object(); + // Same as the above, except it additionally verifies that this object + // is associated with the passed Isolate in debug mode. + inline v8::Local object(v8::Isolate* isolate); + inline Persistent& persistent(); inline Environment* env() const; diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc index 35c7c4dc696ebd..f9db02562d9c8a 100644 --- a/src/bootstrapper.cc +++ b/src/bootstrapper.cc @@ -114,12 +114,14 @@ void SetupBootstrapObject(Environment* env, BOOTSTRAP_METHOD(_umask, Umask); #if defined(__POSIX__) && !defined(__ANDROID__) && !defined(__CloudABI__) - BOOTSTRAP_METHOD(_initgroups, InitGroups); - BOOTSTRAP_METHOD(_setegid, SetEGid); - BOOTSTRAP_METHOD(_seteuid, SetEUid); - BOOTSTRAP_METHOD(_setgid, SetGid); - BOOTSTRAP_METHOD(_setuid, SetUid); - BOOTSTRAP_METHOD(_setgroups, SetGroups); + if (env->is_main_thread()) { + BOOTSTRAP_METHOD(_initgroups, InitGroups); + BOOTSTRAP_METHOD(_setegid, SetEGid); + BOOTSTRAP_METHOD(_seteuid, SetEUid); + BOOTSTRAP_METHOD(_setgid, SetGid); + BOOTSTRAP_METHOD(_setuid, SetUid); + BOOTSTRAP_METHOD(_setgroups, SetGroups); + } #endif // __POSIX__ && !defined(__ANDROID__) && !defined(__CloudABI__) Local should_abort_on_uncaught_toggle = diff --git a/src/callback_scope.cc b/src/callback_scope.cc index 9eac7beb038a26..23e6d5b0632f2c 100644 --- a/src/callback_scope.cc +++ b/src/callback_scope.cc @@ -79,6 +79,11 @@ void InternalCallbackScope::Close() { closed_ = true; HandleScope handle_scope(env_->isolate()); + if (!env_->can_call_into_js()) return; + if (failed_ && !env_->is_main_thread() && env_->is_stopping_worker()) { + env_->async_hooks()->clear_async_id_stack(); + } + if (pushed_ids_) env_->async_hooks()->pop_async_id(async_context_.async_id); diff --git a/src/env-inl.h b/src/env-inl.h index 50328bd77c1a89..eeb419b4a0fad2 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -582,13 +582,42 @@ void Environment::SetUnrefImmediate(native_immediate_callback cb, } inline bool Environment::can_call_into_js() const { - return can_call_into_js_; + return can_call_into_js_ && (is_main_thread() || !is_stopping_worker()); } inline void Environment::set_can_call_into_js(bool can_call_into_js) { can_call_into_js_ = can_call_into_js; } +inline bool Environment::is_main_thread() const { + return thread_id_ == 0; +} + +inline double Environment::thread_id() const { + return thread_id_; +} + +inline void Environment::set_thread_id(double id) { + thread_id_ = id; +} + +inline worker::Worker* Environment::worker_context() const { + return worker_context_; +} + +inline void Environment::set_worker_context(worker::Worker* context) { + CHECK_EQ(worker_context_, nullptr); // Should be set only once. + worker_context_ = context; +} + +inline void Environment::add_sub_worker_context(worker::Worker* context) { + sub_worker_contexts_.insert(context); +} + +inline void Environment::remove_sub_worker_context(worker::Worker* context) { + sub_worker_contexts_.erase(context); +} + inline performance::performance_state* Environment::performance_state() { return performance_state_.get(); } diff --git a/src/env.cc b/src/env.cc index 090b43968bf665..8df59d1546dbdd 100644 --- a/src/env.cc +++ b/src/env.cc @@ -4,6 +4,7 @@ #include "node_buffer.h" #include "node_platform.h" #include "node_file.h" +#include "node_worker.h" #include "tracing/agent.h" #include @@ -25,6 +26,7 @@ using v8::StackTrace; using v8::String; using v8::Symbol; using v8::Value; +using worker::Worker; IsolateData::IsolateData(Isolate* isolate, uv_loop_t* event_loop, @@ -444,7 +446,9 @@ void Environment::RunAndClearNativeImmediates() { if (it->refed_) ref_count++; if (UNLIKELY(try_catch.HasCaught())) { - FatalException(isolate(), try_catch); + if (!try_catch.HasTerminated()) + FatalException(isolate(), try_catch); + // Bail out, remove the already executed callbacks from list // and set up a new TryCatch for the other pending callbacks. std::move_backward(it, list.end(), list.begin() + (list.end() - it)); @@ -632,4 +636,25 @@ void Environment::AsyncHooks::grow_async_ids_stack() { uv_key_t Environment::thread_local_env = {}; +void Environment::Exit(int exit_code) { + if (is_main_thread()) + exit(exit_code); + else + worker_context_->Exit(exit_code); +} + +void Environment::stop_sub_worker_contexts() { + while (!sub_worker_contexts_.empty()) { + Worker* w = *sub_worker_contexts_.begin(); + remove_sub_worker_context(w); + w->Exit(1); + w->JoinThread(); + } +} + +bool Environment::is_stopping_worker() const { + CHECK(!is_main_thread()); + return worker_context_->is_stopped(); +} + } // namespace node diff --git a/src/env.h b/src/env.h index cdb592732a4264..cf6873e5fe7c6a 100644 --- a/src/env.h +++ b/src/env.h @@ -55,6 +55,10 @@ namespace performance { class performance_state; } +namespace worker { +class Worker; +} + namespace loader { class ModuleWrap; @@ -193,7 +197,10 @@ struct PackageConfig { V(mac_string, "mac") \ V(main_string, "main") \ V(max_buffer_string, "maxBuffer") \ + V(max_semi_space_size_string, "maxSemiSpaceSize") \ + V(max_old_space_size_string, "maxOldSpaceSize") \ V(message_string, "message") \ + V(message_port_string, "messagePort") \ V(message_port_constructor_string, "MessagePort") \ V(minttl_string, "minttl") \ V(modulus_string, "modulus") \ @@ -280,6 +287,7 @@ struct PackageConfig { V(subject_string, "subject") \ V(subjectaltname_string, "subjectaltname") \ V(syscall_string, "syscall") \ + V(thread_id_string, "threadId") \ V(ticketkeycallback_string, "onticketkeycallback") \ V(timeout_string, "timeout") \ V(tls_ticket_string, "tlsTicket") \ @@ -328,6 +336,7 @@ struct PackageConfig { V(http2stream_constructor_template, v8::ObjectTemplate) \ V(immediate_callback_function, v8::Function) \ V(inspector_console_api_object, v8::Object) \ + V(message_port, v8::Object) \ V(message_port_constructor_template, v8::FunctionTemplate) \ V(pbkdf2_constructor_template, v8::ObjectTemplate) \ V(pipe_constructor_template, v8::FunctionTemplate) \ @@ -601,6 +610,7 @@ class Environment { void RegisterHandleCleanups(); void CleanupHandles(); + void Exit(int code); // Register clean-up cb to be called on environment destruction. inline void RegisterHandleCleanup(uv_handle_t* handle, @@ -714,6 +724,18 @@ class Environment { inline bool can_call_into_js() const; inline void set_can_call_into_js(bool can_call_into_js); + // TODO(addaleax): This should be inline. + bool is_stopping_worker() const; + + inline bool is_main_thread() const; + inline double thread_id() const; + inline void set_thread_id(double id); + inline worker::Worker* worker_context() const; + inline void set_worker_context(worker::Worker* context); + inline void add_sub_worker_context(worker::Worker* context); + inline void remove_sub_worker_context(worker::Worker* context); + void stop_sub_worker_contexts(); + inline void ThrowError(const char* errmsg); inline void ThrowTypeError(const char* errmsg); inline void ThrowRangeError(const char* errmsg); @@ -855,12 +877,15 @@ class Environment { std::vector destroy_async_id_list_; AliasedBuffer should_abort_on_uncaught_toggle_; - int should_not_abort_scope_counter_ = 0; std::unique_ptr performance_state_; std::unordered_map performance_marks_; + bool can_call_into_js_ = true; + double thread_id_ = 0; + std::unordered_set sub_worker_contexts_; + #if HAVE_INSPECTOR std::unique_ptr inspector_agent_; @@ -893,6 +918,8 @@ class Environment { std::vector> file_handle_read_wrap_freelist_; + worker::Worker* worker_context_ = nullptr; + struct ExitCallback { void (*cb_)(void* arg); void* arg_; diff --git a/src/js_stream.cc b/src/js_stream.cc index c766c322e3017a..e562a62f3d1bb2 100644 --- a/src/js_stream.cc +++ b/src/js_stream.cc @@ -44,7 +44,8 @@ bool JSStream::IsClosing() { TryCatch try_catch(env()->isolate()); Local value; if (!MakeCallback(env()->isclosing_string(), 0, nullptr).ToLocal(&value)) { - FatalException(env()->isolate(), try_catch); + if (!try_catch.HasTerminated()) + FatalException(env()->isolate(), try_catch); return true; } return value->IsTrue(); @@ -59,7 +60,8 @@ int JSStream::ReadStart() { int value_int = UV_EPROTO; if (!MakeCallback(env()->onreadstart_string(), 0, nullptr).ToLocal(&value) || !value->Int32Value(env()->context()).To(&value_int)) { - FatalException(env()->isolate(), try_catch); + if (!try_catch.HasTerminated()) + FatalException(env()->isolate(), try_catch); } return value_int; } @@ -73,7 +75,8 @@ int JSStream::ReadStop() { int value_int = UV_EPROTO; if (!MakeCallback(env()->onreadstop_string(), 0, nullptr).ToLocal(&value) || !value->Int32Value(env()->context()).To(&value_int)) { - FatalException(env()->isolate(), try_catch); + if (!try_catch.HasTerminated()) + FatalException(env()->isolate(), try_catch); } return value_int; } @@ -94,7 +97,8 @@ int JSStream::DoShutdown(ShutdownWrap* req_wrap) { arraysize(argv), argv).ToLocal(&value) || !value->Int32Value(env()->context()).To(&value_int)) { - FatalException(env()->isolate(), try_catch); + if (!try_catch.HasTerminated()) + FatalException(env()->isolate(), try_catch); } return value_int; } @@ -128,7 +132,8 @@ int JSStream::DoWrite(WriteWrap* w, arraysize(argv), argv).ToLocal(&value) || !value->Int32Value(env()->context()).To(&value_int)) { - FatalException(env()->isolate(), try_catch); + if (!try_catch.HasTerminated()) + FatalException(env()->isolate(), try_catch); } return value_int; } diff --git a/src/node.cc b/src/node.cc index baa97281b064a7..663e4a222eba91 100644 --- a/src/node.cc +++ b/src/node.cc @@ -1021,9 +1021,9 @@ void AppendExceptionLine(Environment* env, } -static void ReportException(Environment* env, - Local er, - Local message) { +void ReportException(Environment* env, + Local er, + Local message) { CHECK(!er.IsEmpty()); HandleScope scope(env->isolate()); @@ -1110,9 +1110,9 @@ static void ReportException(Environment* env, const TryCatch& try_catch) { // Executes a str within the current v8 context. -static Local ExecuteString(Environment* env, - Local source, - Local filename) { +static MaybeLocal ExecuteString(Environment* env, + Local source, + Local filename) { EscapableHandleScope scope(env->isolate()); TryCatch try_catch(env->isolate()); @@ -1125,13 +1125,19 @@ static Local ExecuteString(Environment* env, v8::Script::Compile(env->context(), source, &origin); if (script.IsEmpty()) { ReportException(env, try_catch); - exit(3); + env->Exit(3); + return MaybeLocal(); } MaybeLocal result = script.ToLocalChecked()->Run(env->context()); if (result.IsEmpty()) { + if (try_catch.HasTerminated()) { + env->isolate()->CancelTerminateExecution(); + return MaybeLocal(); + } ReportException(env, try_catch); - exit(4); + env->Exit(4); + return MaybeLocal(); } return scope.Escape(result.ToLocalChecked()); @@ -1230,6 +1236,7 @@ static void Abort(const FunctionCallbackInfo& args) { void Chdir(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + CHECK(env->is_main_thread()); CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsString()); @@ -1411,6 +1418,7 @@ static void GetEGid(const FunctionCallbackInfo& args) { void SetGid(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + CHECK(env->is_main_thread()); CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsUint32() || args[0]->IsString()); @@ -1430,6 +1438,7 @@ void SetGid(const FunctionCallbackInfo& args) { void SetEGid(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + CHECK(env->is_main_thread()); CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsUint32() || args[0]->IsString()); @@ -1449,6 +1458,7 @@ void SetEGid(const FunctionCallbackInfo& args) { void SetUid(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + CHECK(env->is_main_thread()); CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsUint32() || args[0]->IsString()); @@ -1468,6 +1478,7 @@ void SetUid(const FunctionCallbackInfo& args) { void SetEUid(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + CHECK(env->is_main_thread()); CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsUint32() || args[0]->IsString()); @@ -1629,9 +1640,10 @@ static void WaitForInspectorDisconnect(Environment* env) { static void Exit(const FunctionCallbackInfo& args) { - WaitForInspectorDisconnect(Environment::GetCurrent(args)); + Environment* env = Environment::GetCurrent(args); + WaitForInspectorDisconnect(env); v8_platform.StopTracingAgent(); - exit(args[0]->Int32Value()); + env->Exit(args[0]->Int32Value()); } @@ -2040,6 +2052,9 @@ void FatalException(Isolate* isolate, Local caught = fatal_exception_function->Call(process_object, 1, &error); + if (fatal_try_catch.HasTerminated()) + return; + if (fatal_try_catch.HasCaught()) { // The fatal exception function threw, so we must exit ReportException(env, fatal_try_catch); @@ -2053,6 +2068,12 @@ void FatalException(Isolate* isolate, void FatalException(Isolate* isolate, const TryCatch& try_catch) { + // If we try to print out a termination exception, we'd just get 'null', + // so just crashing here with that information seems like a better idea, + // and in particular it seems like we should handle terminations at the call + // site for this function rather than by printing them out somewhere. + CHECK(!try_catch.HasTerminated()); + HandleScope scope(isolate); if (!try_catch.IsVerbose()) { FatalException(isolate, try_catch.Exception(), try_catch.Message()); @@ -2574,11 +2595,12 @@ void SetupProcessObject(Environment* env, Local process = env->process_object(); auto title_string = FIXED_ONE_BYTE_STRING(env->isolate(), "title"); - CHECK(process->SetAccessor(env->context(), - title_string, - ProcessTitleGetter, - ProcessTitleSetter, - env->as_external()).FromJust()); + CHECK(process->SetAccessor( + env->context(), + title_string, + ProcessTitleGetter, + env->is_main_thread() ? ProcessTitleSetter : nullptr, + env->as_external()).FromJust()); // process.version READONLY_PROPERTY(process, @@ -2862,25 +2884,27 @@ void SetupProcessObject(Environment* env, CHECK(process->SetAccessor(env->context(), debug_port_string, DebugPortGetter, - DebugPortSetter, + env->is_main_thread() ? DebugPortSetter : nullptr, env->as_external()).FromJust()); // define various internal methods - env->SetMethod(process, - "_startProfilerIdleNotifier", - StartProfilerIdleNotifier); - env->SetMethod(process, - "_stopProfilerIdleNotifier", - StopProfilerIdleNotifier); + if (env->is_main_thread()) { + env->SetMethod(process, + "_startProfilerIdleNotifier", + StartProfilerIdleNotifier); + env->SetMethod(process, + "_stopProfilerIdleNotifier", + StopProfilerIdleNotifier); + env->SetMethod(process, "abort", Abort); + env->SetMethod(process, "chdir", Chdir); + env->SetMethod(process, "umask", Umask); + } + env->SetMethod(process, "_getActiveRequests", GetActiveRequests); env->SetMethod(process, "_getActiveHandles", GetActiveHandles); env->SetMethod(process, "reallyExit", Exit); - env->SetMethod(process, "abort", Abort); - env->SetMethod(process, "chdir", Chdir); env->SetMethod(process, "cwd", Cwd); - env->SetMethod(process, "umask", Umask); - #if defined(__POSIX__) && !defined(__ANDROID__) && !defined(__CloudABI__) env->SetMethod(process, "getuid", GetUid); env->SetMethod(process, "geteuid", GetEUid); @@ -2890,16 +2914,17 @@ void SetupProcessObject(Environment* env, #endif // __POSIX__ && !defined(__ANDROID__) && !defined(__CloudABI__) env->SetMethod(process, "_kill", Kill); + env->SetMethod(process, "dlopen", DLOpen); - env->SetMethod(process, "_debugProcess", DebugProcess); - env->SetMethod(process, "_debugEnd", DebugEnd); + if (env->is_main_thread()) { + env->SetMethod(process, "_debugProcess", DebugProcess); + env->SetMethod(process, "_debugEnd", DebugEnd); + } env->SetMethod(process, "hrtime", Hrtime); env->SetMethod(process, "cpuUsage", CPUUsage); - env->SetMethod(process, "dlopen", DLOpen); - env->SetMethod(process, "uptime", Uptime); env->SetMethod(process, "memoryUsage", MemoryUsage); } @@ -2935,8 +2960,10 @@ void RawDebug(const FunctionCallbackInfo& args) { } -static Local GetBootstrapper(Environment* env, Local source, - Local script_name) { +static MaybeLocal GetBootstrapper( + Environment* env, + Local source, + Local script_name) { EscapableHandleScope scope(env->isolate()); TryCatch try_catch(env->isolate()); @@ -2947,16 +2974,17 @@ static Local GetBootstrapper(Environment* env, Local source, try_catch.SetVerbose(false); // Execute the bootstrapper javascript file - Local bootstrapper_v = ExecuteString(env, source, script_name); + MaybeLocal bootstrapper_v = ExecuteString(env, source, script_name); + if (bootstrapper_v.IsEmpty()) // This happens when execution was interrupted. + return MaybeLocal(); + if (try_catch.HasCaught()) { ReportException(env, try_catch); exit(10); } - CHECK(bootstrapper_v->IsFunction()); - Local bootstrapper = Local::Cast(bootstrapper_v); - - return scope.Escape(bootstrapper); + CHECK(bootstrapper_v.ToLocalChecked()->IsFunction()); + return scope.Escape(bootstrapper_v.ToLocalChecked().As()); } static bool ExecuteBootstrapper(Environment* env, Local bootstrapper, @@ -2995,13 +3023,18 @@ void LoadEnvironment(Environment* env) { // node_js2c. Local loaders_name = FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/loaders.js"); - Local loaders_bootstrapper = + MaybeLocal loaders_bootstrapper = GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name); Local node_name = FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/node.js"); - Local node_bootstrapper = + MaybeLocal node_bootstrapper = GetBootstrapper(env, NodeBootstrapperSource(env), node_name); + if (loaders_bootstrapper.IsEmpty() || node_bootstrapper.IsEmpty()) { + // Execution was interrupted. + return; + } + // Add a reference to the global object Local global = env->context()->Global(); @@ -3049,7 +3082,7 @@ void LoadEnvironment(Environment* env) { // Bootstrap internal loaders Local bootstrapped_loaders; - if (!ExecuteBootstrapper(env, loaders_bootstrapper, + if (!ExecuteBootstrapper(env, loaders_bootstrapper.ToLocalChecked(), arraysize(loaders_bootstrapper_args), loaders_bootstrapper_args, &bootstrapped_loaders)) { @@ -3065,7 +3098,7 @@ void LoadEnvironment(Environment* env) { bootstrapper, bootstrapped_loaders }; - if (!ExecuteBootstrapper(env, node_bootstrapper, + if (!ExecuteBootstrapper(env, node_bootstrapper.ToLocalChecked(), arraysize(node_bootstrapper_args), node_bootstrapper_args, &bootstrapped_node)) { @@ -4279,6 +4312,7 @@ inline int Start(Isolate* isolate, IsolateData* isolate_data, WaitForInspectorDisconnect(&env); env.set_can_call_into_js(false); + env.stop_sub_worker_contexts(); env.RunCleanup(); RunAtExit(&env); diff --git a/src/node_errors.h b/src/node_errors.h index 931ce7b8fdbf33..2c97088cc553b4 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -34,6 +34,7 @@ namespace node { V(ERR_MISSING_ARGS, TypeError) \ V(ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST, TypeError) \ V(ERR_MISSING_MODULE, Error) \ + V(ERR_MISSING_PLATFORM_FOR_WORKER, Error) \ V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \ V(ERR_SCRIPT_EXECUTION_TIMEOUT, Error) \ V(ERR_STRING_TOO_LONG, Error) \ @@ -68,6 +69,9 @@ namespace node { V(ERR_MEMORY_ALLOCATION_FAILED, "Failed to allocate memory") \ V(ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST, \ "MessagePort was found in message but not listed in transferList") \ + V(ERR_MISSING_PLATFORM_FOR_WORKER, \ + "The V8 platform used by this instance of Node does not support " \ + "creating Workers") \ V(ERR_SCRIPT_EXECUTION_INTERRUPTED, \ "Script execution was interrupted by `SIGINT`") \ V(ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER, \ diff --git a/src/node_internals.h b/src/node_internals.h index a5d8ed0e5d3ad7..7760eb26c6c15c 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -137,6 +137,7 @@ struct sockaddr; V(util) \ V(uv) \ V(v8) \ + V(worker) \ V(zlib) #define NODE_BUILTIN_MODULES(V) \ @@ -314,6 +315,10 @@ class FatalTryCatch : public v8::TryCatch { Environment* env_; }; +void ReportException(Environment* env, + v8::Local er, + v8::Local message); + v8::Maybe ProcessEmitWarning(Environment* env, const char* fmt, ...); v8::Maybe ProcessEmitDeprecationWarning(Environment* env, const char* warning, diff --git a/src/node_messaging.cc b/src/node_messaging.cc index b56cef2d7767cf..352749ea48f483 100644 --- a/src/node_messaging.cc +++ b/src/node_messaging.cc @@ -57,7 +57,7 @@ class DeserializerDelegate : public ValueDeserializer::Delegate { if (!deserializer->ReadUint32(&id)) return MaybeLocal(); CHECK_LE(id, message_ports_.size()); - return message_ports_[id]->object(); + return message_ports_[id]->object(isolate); }; MaybeLocal GetSharedArrayBufferFromId( @@ -436,7 +436,7 @@ MessagePort* MessagePort::New( void MessagePort::OnMessage() { HandleScope handle_scope(env()->isolate()); - Local context = object()->CreationContext(); + Local context = object(env()->isolate())->CreationContext(); // data_ can only ever be modified by the owner thread, so no need to lock. // However, the message port may be transferred while it is processing @@ -447,6 +447,13 @@ void MessagePort::OnMessage() { { // Get the head of the message queue. Mutex::ScopedLock lock(data_->mutex_); + + if (stop_event_loop_) { + CHECK(!data_->receiving_messages_); + uv_stop(env()->event_loop()); + break; + } + if (!data_->receiving_messages_) break; if (data_->incoming_messages_.empty()) @@ -514,8 +521,9 @@ void MessagePort::Send(Message&& message) { void MessagePort::Send(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + Local context = object(env->isolate())->CreationContext(); Message msg; - if (msg.Serialize(env, object()->CreationContext(), args[0], args[1]) + if (msg.Serialize(env, context, args[0], args[1]) .IsNothing()) { return; } @@ -548,6 +556,14 @@ void MessagePort::Stop() { data_->receiving_messages_ = false; } +void MessagePort::StopEventLoop() { + Mutex::ScopedLock lock(data_->mutex_); + data_->receiving_messages_ = false; + stop_event_loop_ = true; + + TriggerAsync(); +} + void MessagePort::Start(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); MessagePort* port; @@ -570,6 +586,12 @@ void MessagePort::Stop(const FunctionCallbackInfo& args) { port->Stop(); } +void MessagePort::Drain(const FunctionCallbackInfo& args) { + MessagePort* port; + ASSIGN_OR_RETURN_UNWRAP(&port, args.This()); + port->OnMessage(); +} + size_t MessagePort::self_size() const { Mutex::ScopedLock lock(data_->mutex_); size_t sz = sizeof(*this) + sizeof(*data_); @@ -604,6 +626,7 @@ MaybeLocal GetMessagePortConstructor( env->SetProtoMethod(m, "postMessage", MessagePort::PostMessage); env->SetProtoMethod(m, "start", MessagePort::Start); env->SetProtoMethod(m, "stop", MessagePort::Stop); + env->SetProtoMethod(m, "drain", MessagePort::Drain); env->SetProtoMethod(m, "close", HandleWrap::Close); env->SetProtoMethod(m, "unref", HandleWrap::Unref); env->SetProtoMethod(m, "ref", HandleWrap::Ref); diff --git a/src/node_messaging.h b/src/node_messaging.h index ff8fcc72439e9f..9a13437d19a331 100644 --- a/src/node_messaging.h +++ b/src/node_messaging.h @@ -133,11 +133,15 @@ class MessagePort : public HandleWrap { void Start(); // Stop processing messages on this port as a receiving end. void Stop(); + // Stop processing messages on this port as a receiving end, + // and stop the event loop that this port is associated with. + void StopEventLoop(); static void New(const v8::FunctionCallbackInfo& args); static void PostMessage(const v8::FunctionCallbackInfo& args); static void Start(const v8::FunctionCallbackInfo& args); static void Stop(const v8::FunctionCallbackInfo& args); + static void Drain(const v8::FunctionCallbackInfo& args); // Turns `a` and `b` into siblings, i.e. connects the sending side of one // to the receiving side of the other. This is not thread-safe. @@ -160,6 +164,7 @@ class MessagePort : public HandleWrap { inline uv_async_t* async(); std::unique_ptr data_ = nullptr; + bool stop_event_loop_ = false; friend class MessagePortData; }; diff --git a/src/node_worker.cc b/src/node_worker.cc new file mode 100644 index 00000000000000..4d9e1ee98dca17 --- /dev/null +++ b/src/node_worker.cc @@ -0,0 +1,427 @@ +#include "node_worker.h" +#include "node_errors.h" +#include "node_internals.h" +#include "node_buffer.h" +#include "node_perf.h" +#include "util.h" +#include "util-inl.h" +#include "async_wrap.h" +#include "async_wrap-inl.h" + +using v8::ArrayBuffer; +using v8::Context; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Integer; +using v8::Isolate; +using v8::Local; +using v8::Locker; +using v8::Number; +using v8::Object; +using v8::SealHandleScope; +using v8::String; +using v8::Value; + +namespace node { +namespace worker { + +namespace { + +double next_thread_id = 1; +Mutex next_thread_id_mutex; + +} // anonymous namespace + +Worker::Worker(Environment* env, Local wrap) + : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_WORKER) { + // Generate a new thread id. + { + Mutex::ScopedLock next_thread_id_lock(next_thread_id_mutex); + thread_id_ = next_thread_id++; + } + wrap->Set(env->context(), + env->thread_id_string(), + Number::New(env->isolate(), thread_id_)).FromJust(); + + // Set up everything that needs to be set up in the parent environment. + parent_port_ = MessagePort::New(env, env->context()); + if (parent_port_ == nullptr) { + // This can happen e.g. because execution is terminating. + return; + } + + child_port_data_.reset(new MessagePortData(nullptr)); + MessagePort::Entangle(parent_port_, child_port_data_.get()); + + object()->Set(env->context(), + env->message_port_string(), + parent_port_->object()).FromJust(); + + array_buffer_allocator_.reset(CreateArrayBufferAllocator()); + + isolate_ = NewIsolate(array_buffer_allocator_.get()); + CHECK_NE(isolate_, nullptr); + CHECK_EQ(uv_loop_init(&loop_), 0); + + thread_exit_async_.reset(new uv_async_t); + thread_exit_async_->data = this; + CHECK_EQ(uv_async_init(env->event_loop(), + thread_exit_async_.get(), + [](uv_async_t* handle) { + static_cast(handle->data)->OnThreadStopped(); + }), 0); + + { + // Enter an environment capable of executing code in the child Isolate + // (and only in it). + Locker locker(isolate_); + Isolate::Scope isolate_scope(isolate_); + HandleScope handle_scope(isolate_); + Local context = NewContext(isolate_); + Context::Scope context_scope(context); + + isolate_data_.reset(CreateIsolateData(isolate_, + &loop_, + env->isolate_data()->platform(), + array_buffer_allocator_.get())); + CHECK(isolate_data_); + + // TODO(addaleax): Use CreateEnvironment(), or generally another public API. + env_.reset(new Environment(isolate_data_.get(), + context, + nullptr)); + CHECK_NE(env_, nullptr); + env_->set_abort_on_uncaught_exception(false); + env_->set_worker_context(this); + env_->set_thread_id(thread_id_); + + env_->Start(0, nullptr, 0, nullptr, env->profiler_idle_notifier_started()); + } + + // The new isolate won't be bothered on this thread again. + isolate_->DiscardThreadSpecificMetadata(); +} + +bool Worker::is_stopped() const { + Mutex::ScopedLock stopped_lock(stopped_mutex_); + return stopped_; +} + +void Worker::Run() { + MultiIsolatePlatform* platform = isolate_data_->platform(); + CHECK_NE(platform, nullptr); + + { + Locker locker(isolate_); + Isolate::Scope isolate_scope(isolate_); + SealHandleScope outer_seal(isolate_); + + { + Context::Scope context_scope(env_->context()); + HandleScope handle_scope(isolate_); + + { + HandleScope handle_scope(isolate_); + Mutex::ScopedLock lock(mutex_); + // Set up the message channel for receiving messages in the child. + child_port_ = MessagePort::New(env_.get(), + env_->context(), + std::move(child_port_data_)); + // MessagePort::New() may return nullptr if execution is terminated + // within it. + if (child_port_ != nullptr) + env_->set_message_port(child_port_->object(isolate_)); + } + + if (!is_stopped()) { + HandleScope handle_scope(isolate_); + Environment::AsyncCallbackScope callback_scope(env_.get()); + env_->async_hooks()->push_async_ids(1, 0); + // This loads the Node bootstrapping code. + LoadEnvironment(env_.get()); + env_->async_hooks()->pop_async_id(1); + } + + { + SealHandleScope seal(isolate_); + bool more; + env_->performance_state()->Mark( + node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_START); + do { + if (is_stopped()) break; + uv_run(&loop_, UV_RUN_DEFAULT); + if (is_stopped()) break; + + platform->DrainBackgroundTasks(isolate_); + + more = uv_loop_alive(&loop_); + if (more && !is_stopped()) + continue; + + EmitBeforeExit(env_.get()); + + // Emit `beforeExit` if the loop became alive either after emitting + // event, or after running some callbacks. + more = uv_loop_alive(&loop_); + } while (more == true); + env_->performance_state()->Mark( + node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT); + } + } + + { + int exit_code; + bool stopped = is_stopped(); + if (!stopped) + exit_code = EmitExit(env_.get()); + Mutex::ScopedLock lock(mutex_); + if (exit_code_ == 0 && !stopped) + exit_code_ = exit_code; + } + + env_->set_can_call_into_js(false); + Isolate::DisallowJavascriptExecutionScope disallow_js(isolate_, + Isolate::DisallowJavascriptExecutionScope::THROW_ON_FAILURE); + + // Grab the parent-to-child channel and render is unusable. + MessagePort* child_port; + { + Mutex::ScopedLock lock(mutex_); + child_port = child_port_; + child_port_ = nullptr; + } + + { + Context::Scope context_scope(env_->context()); + child_port->Close(); + env_->stop_sub_worker_contexts(); + env_->RunCleanup(); + RunAtExit(env_.get()); + + { + Mutex::ScopedLock stopped_lock(stopped_mutex_); + stopped_ = true; + } + + env_->RunCleanup(); + + // This call needs to be made while the `Environment` is still alive + // because we assume that it is available for async tracking in the + // NodePlatform implementation. + platform->DrainBackgroundTasks(isolate_); + } + + env_.reset(); + } + + DisposeIsolate(); + + // Need to run the loop one more time to close the platform's uv_async_t + uv_run(&loop_, UV_RUN_ONCE); + + { + Mutex::ScopedLock lock(mutex_); + CHECK(thread_exit_async_); + scheduled_on_thread_stopped_ = true; + uv_async_send(thread_exit_async_.get()); + } +} + +void Worker::DisposeIsolate() { + if (isolate_ == nullptr) + return; + + isolate_->Dispose(); + + CHECK(isolate_data_); + MultiIsolatePlatform* platform = isolate_data_->platform(); + platform->CancelPendingDelayedTasks(isolate_); + + isolate_data_.reset(); + isolate_ = nullptr; +} + +void Worker::JoinThread() { + if (thread_joined_) + return; + CHECK_EQ(uv_thread_join(&tid_), 0); + thread_joined_ = true; + + env()->remove_sub_worker_context(this); + + if (thread_exit_async_) { + env()->CloseHandle(thread_exit_async_.release(), [](uv_async_t* async) { + delete async; + }); + + if (scheduled_on_thread_stopped_) + OnThreadStopped(); + } +} + +void Worker::OnThreadStopped() { + Mutex::ScopedLock lock(mutex_); + scheduled_on_thread_stopped_ = false; + + { + Mutex::ScopedLock stopped_lock(stopped_mutex_); + CHECK(stopped_); + } + + CHECK_EQ(child_port_, nullptr); + parent_port_ = nullptr; + + // It's okay to join the thread while holding the mutex because + // OnThreadStopped means it's no longer doing any work that might grab it + // and really just silently exiting. + JoinThread(); + + { + HandleScope handle_scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + + // Reset the parent port as we're closing it now anyway. + object()->Set(env()->context(), + env()->message_port_string(), + Undefined(env()->isolate())).FromJust(); + + Local code = Integer::New(env()->isolate(), exit_code_); + MakeCallback(env()->onexit_string(), 1, &code); + } + + // JoinThread() cleared all libuv handles bound to this Worker, + // the C++ object is no longer needed for anything now. + MakeWeak(); +} + +Worker::~Worker() { + Mutex::ScopedLock lock(mutex_); + JoinThread(); + + CHECK(stopped_); + CHECK(thread_joined_); + CHECK_EQ(child_port_, nullptr); + CHECK_EQ(uv_loop_close(&loop_), 0); + + // This has most likely already happened within the worker thread -- this + // is just in case Worker creation failed early. + DisposeIsolate(); +} + +void Worker::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + CHECK(args.IsConstructCall()); + + if (env->isolate_data()->platform() == nullptr) { + THROW_ERR_MISSING_PLATFORM_FOR_WORKER(env); + return; + } + + new Worker(env, args.This()); +} + +void Worker::StartThread(const FunctionCallbackInfo& args) { + Worker* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.This()); + Mutex::ScopedLock lock(w->mutex_); + + w->env()->add_sub_worker_context(w); + w->stopped_ = false; + CHECK_EQ(uv_thread_create(&w->tid_, [](void* arg) { + static_cast(arg)->Run(); + }, static_cast(w)), 0); + w->thread_joined_ = false; +} + +void Worker::StopThread(const FunctionCallbackInfo& args) { + Worker* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.This()); + + w->Exit(1); + w->JoinThread(); +} + +void Worker::Ref(const FunctionCallbackInfo& args) { + Worker* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.This()); + if (w->thread_exit_async_) + uv_ref(reinterpret_cast(w->thread_exit_async_.get())); +} + +void Worker::Unref(const FunctionCallbackInfo& args) { + Worker* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.This()); + if (w->thread_exit_async_) + uv_unref(reinterpret_cast(w->thread_exit_async_.get())); +} + +void Worker::Exit(int code) { + Mutex::ScopedLock lock(mutex_); + Mutex::ScopedLock stopped_lock(stopped_mutex_); + if (!stopped_) { + CHECK_NE(env_, nullptr); + stopped_ = true; + exit_code_ = code; + if (child_port_ != nullptr) + child_port_->StopEventLoop(); + isolate_->TerminateExecution(); + } +} + +size_t Worker::self_size() const { + return sizeof(*this); +} + +namespace { + +// Return the MessagePort that is global for this Environment and communicates +// with the internal [kPort] port of the JS Worker class in the parent thread. +void GetEnvMessagePort(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Local port = env->message_port(); + if (!port.IsEmpty()) { + CHECK_EQ(port->CreationContext()->GetIsolate(), args.GetIsolate()); + args.GetReturnValue().Set(port); + } +} + +void InitWorker(Local target, + Local unused, + Local context, + void* priv) { + Environment* env = Environment::GetCurrent(context); + + { + Local w = env->NewFunctionTemplate(Worker::New); + + w->InstanceTemplate()->SetInternalFieldCount(1); + + AsyncWrap::AddWrapMethods(env, w); + env->SetProtoMethod(w, "startThread", Worker::StartThread); + env->SetProtoMethod(w, "stopThread", Worker::StopThread); + env->SetProtoMethod(w, "ref", Worker::Ref); + env->SetProtoMethod(w, "unref", Worker::Unref); + + Local workerString = + FIXED_ONE_BYTE_STRING(env->isolate(), "Worker"); + w->SetClassName(workerString); + target->Set(workerString, w->GetFunction()); + } + + env->SetMethod(target, "getEnvMessagePort", GetEnvMessagePort); + + auto thread_id_string = FIXED_ONE_BYTE_STRING(env->isolate(), "threadId"); + target->Set(env->context(), + thread_id_string, + Number::New(env->isolate(), env->thread_id())).FromJust(); +} + +} // anonymous namespace + +} // namespace worker +} // namespace node + +NODE_MODULE_CONTEXT_AWARE_INTERNAL(worker, node::worker::InitWorker) diff --git a/src/node_worker.h b/src/node_worker.h new file mode 100644 index 00000000000000..0a98d2f11ef00f --- /dev/null +++ b/src/node_worker.h @@ -0,0 +1,83 @@ +#ifndef SRC_NODE_WORKER_H_ +#define SRC_NODE_WORKER_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "node_messaging.h" +#include + +namespace node { +namespace worker { + +// A worker thread, as represented in its parent thread. +class Worker : public AsyncWrap { + public: + Worker(Environment* env, v8::Local wrap); + ~Worker(); + + // Run the worker. This is only called from the worker thread. + void Run(); + + // Forcibly exit the thread with a specified exit code. This may be called + // from any thread. + void Exit(int code); + + // Wait for the worker thread to stop (in a blocking manner). + void JoinThread(); + + size_t self_size() const override; + bool is_stopped() const; + + static void New(const v8::FunctionCallbackInfo& args); + static void StartThread(const v8::FunctionCallbackInfo& args); + static void StopThread(const v8::FunctionCallbackInfo& args); + static void GetMessagePort(const v8::FunctionCallbackInfo& args); + static void Ref(const v8::FunctionCallbackInfo& args); + static void Unref(const v8::FunctionCallbackInfo& args); + + private: + void OnThreadStopped(); + void DisposeIsolate(); + + uv_loop_t loop_; + DeleteFnPtr isolate_data_; + DeleteFnPtr env_; + v8::Isolate* isolate_ = nullptr; + DeleteFnPtr + array_buffer_allocator_; + uv_thread_t tid_; + + // This mutex protects access to all variables listed below it. + mutable Mutex mutex_; + + // Currently only used for telling the parent thread that the child + // thread exited. + std::unique_ptr thread_exit_async_; + bool scheduled_on_thread_stopped_ = false; + + // This mutex only protects stopped_. If both locks are acquired, this needs + // to be the latter one. + mutable Mutex stopped_mutex_; + bool stopped_ = true; + + bool thread_joined_ = true; + int exit_code_ = 0; + double thread_id_ = -1; + + std::unique_ptr child_port_data_; + + // The child port is always kept alive by the child Environment's persistent + // handle to it. + MessagePort* child_port_ = nullptr; + // This is always kept alive because the JS object associated with the Worker + // instance refers to it via its [kPort] property. + MessagePort* parent_port_ = nullptr; +}; + +} // namespace worker +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + + +#endif // SRC_NODE_WORKER_H_ diff --git a/test/fixtures/worker-script.mjs b/test/fixtures/worker-script.mjs new file mode 100644 index 00000000000000..b712248b2788e8 --- /dev/null +++ b/test/fixtures/worker-script.mjs @@ -0,0 +1,3 @@ +import worker from 'worker'; + +worker.parentPort.postMessage('Hello, world!'); diff --git a/test/parallel/test-message-channel-sharedarraybuffer.js b/test/parallel/test-message-channel-sharedarraybuffer.js new file mode 100644 index 00000000000000..7ae922adbc4e40 --- /dev/null +++ b/test/parallel/test-message-channel-sharedarraybuffer.js @@ -0,0 +1,28 @@ +// Flags: --expose-gc --experimental-worker +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker'); + +{ + const sharedArrayBuffer = new SharedArrayBuffer(12); + const local = Buffer.from(sharedArrayBuffer); + + const w = new Worker(` + const { parentPort } = require('worker'); + parentPort.on('message', ({ sharedArrayBuffer }) => { + const local = Buffer.from(sharedArrayBuffer); + local.write('world!', 6); + parentPort.postMessage('written!'); + }); + `, { eval: true }); + w.on('message', common.mustCall(() => { + assert.strictEqual(local.toString(), 'Hello world!'); + global.gc(); + w.terminate(); + })); + w.postMessage({ sharedArrayBuffer }); + // This would be a race condition if the memory regions were overlapping + local.write('Hello '); +} diff --git a/test/parallel/test-message-channel.js b/test/parallel/test-message-channel.js index 0facaa1d835ea8..eb13fa57c6aa0f 100644 --- a/test/parallel/test-message-channel.js +++ b/test/parallel/test-message-channel.js @@ -2,7 +2,7 @@ 'use strict'; const common = require('../common'); const assert = require('assert'); -const { MessageChannel } = require('worker'); +const { MessageChannel, MessagePort, Worker } = require('worker'); { const channel = new MessageChannel(); @@ -24,3 +24,23 @@ const { MessageChannel } = require('worker'); channel.port2.on('close', common.mustCall()); channel.port2.close(); } + +{ + const channel = new MessageChannel(); + + const w = new Worker(` + const { MessagePort } = require('worker'); + const assert = require('assert'); + require('worker').parentPort.on('message', ({ port }) => { + assert(port instanceof MessagePort); + port.postMessage('works'); + }); + `, { eval: true }); + w.postMessage({ port: channel.port2 }, [ channel.port2 ]); + assert(channel.port1 instanceof MessagePort); + assert(channel.port2 instanceof MessagePort); + channel.port1.on('message', common.mustCall((message) => { + assert.strictEqual(message, 'works'); + w.terminate(); + })); +} diff --git a/test/parallel/test-worker-cleanup-handles.js b/test/parallel/test-worker-cleanup-handles.js new file mode 100644 index 00000000000000..ba4f6aa51a9d41 --- /dev/null +++ b/test/parallel/test-worker-cleanup-handles.js @@ -0,0 +1,30 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker, isMainThread, parentPort } = require('worker'); +const { Server } = require('net'); +const fs = require('fs'); + +if (isMainThread) { + const w = new Worker(__filename); + let fd = null; + w.on('message', common.mustCall((fd_) => { + assert.strictEqual(typeof fd_, 'number'); + fd = fd_; + })); + w.on('exit', common.mustCall((code) => { + if (fd === -1) { + // This happens when server sockets don’t have file descriptors, + // i.e. on Windows. + return; + } + common.expectsError(() => fs.fstatSync(fd), + { code: 'EBADF' }); + })); +} else { + const server = new Server(); + server.listen(0); + parentPort.postMessage(server._handle.fd); + server.unref(); +} diff --git a/test/parallel/test-worker-dns-terminate.js b/test/parallel/test-worker-dns-terminate.js new file mode 100644 index 00000000000000..079a29d52e09a3 --- /dev/null +++ b/test/parallel/test-worker-dns-terminate.js @@ -0,0 +1,15 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const { Worker } = require('worker'); + +const w = new Worker(` +const dns = require('dns'); +dns.lookup('nonexistent.org', () => {}); +require('worker').parentPort.postMessage('0'); +`, { eval: true }); + +w.on('message', common.mustCall(() => { + // This should not crash the worker during a DNS request. + w.terminate(common.mustCall()); +})); diff --git a/test/parallel/test-worker-esmodule.js b/test/parallel/test-worker-esmodule.js new file mode 100644 index 00000000000000..4189eeca3f8908 --- /dev/null +++ b/test/parallel/test-worker-esmodule.js @@ -0,0 +1,11 @@ +// Flags: --experimental-worker --experimental-modules +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { Worker } = require('worker'); + +const w = new Worker(fixtures.path('worker-script.mjs')); +w.on('message', common.mustCall((message) => { + assert.strictEqual(message, 'Hello, world!'); +})); diff --git a/test/parallel/test-worker-memory.js b/test/parallel/test-worker-memory.js new file mode 100644 index 00000000000000..34b1e0acaf2f2f --- /dev/null +++ b/test/parallel/test-worker-memory.js @@ -0,0 +1,41 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const util = require('util'); +const { Worker } = require('worker'); + +const numWorkers = +process.env.JOBS || require('os').cpus().length; + +// Verify that a Worker's memory isn't kept in memory after the thread finishes. + +function run(n, done) { + if (n <= 0) + return done(); + const worker = new Worker( + 'require(\'worker\').parentPort.postMessage(2 + 2)', + { eval: true }); + worker.on('message', common.mustCall((value) => { + assert.strictEqual(value, 4); + })); + worker.on('exit', common.mustCall(() => { + run(n - 1, done); + })); +} + +const startStats = process.memoryUsage(); +let finished = 0; +for (let i = 0; i < numWorkers; ++i) { + run(60 / numWorkers, () => { + if (++finished === numWorkers) { + const finishStats = process.memoryUsage(); + // A typical value for this ratio would be ~1.15. + // 5 as a upper limit is generous, but the main point is that we + // don't have the memory of 50 Isolates/Node.js environments just lying + // around somewhere. + assert.ok(finishStats.rss / startStats.rss < 5, + 'Unexpected memory overhead: ' + + util.inspect([startStats, finishStats])); + } + }); +} diff --git a/test/parallel/test-worker-nexttick-terminate.js b/test/parallel/test-worker-nexttick-terminate.js new file mode 100644 index 00000000000000..b010a7dbe5727f --- /dev/null +++ b/test/parallel/test-worker-nexttick-terminate.js @@ -0,0 +1,20 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const { Worker } = require('worker'); + +// Checks that terminating in the middle of `process.nextTick()` does not +// Crash the process. + +const w = new Worker(` +require('worker').parentPort.postMessage('0'); +process.nextTick(() => { + while(1); +}); +`, { eval: true }); + +w.on('message', common.mustCall(() => { + setTimeout(() => { + w.terminate(common.mustCall()); + }, 1); +})); diff --git a/test/parallel/test-worker-syntax-error-file.js b/test/parallel/test-worker-syntax-error-file.js new file mode 100644 index 00000000000000..37798f334387d8 --- /dev/null +++ b/test/parallel/test-worker-syntax-error-file.js @@ -0,0 +1,18 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { Worker } = require('worker'); + +// Do not use isMainThread so that this test itself can be run inside a Worker. +if (!process.env.HAS_STARTED_WORKER) { + process.env.HAS_STARTED_WORKER = 1; + const w = new Worker(fixtures.path('syntax', 'bad_syntax.js')); + w.on('message', common.mustNotCall()); + w.on('error', common.mustCall((err) => { + assert(/SyntaxError/.test(err)); + })); +} else { + throw new Error('foo'); +} diff --git a/test/parallel/test-worker-syntax-error.js b/test/parallel/test-worker-syntax-error.js new file mode 100644 index 00000000000000..8f9812a721132b --- /dev/null +++ b/test/parallel/test-worker-syntax-error.js @@ -0,0 +1,17 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker'); + +// Do not use isMainThread so that this test itself can be run inside a Worker. +if (!process.env.HAS_STARTED_WORKER) { + process.env.HAS_STARTED_WORKER = 1; + const w = new Worker('abc)', { eval: true }); + w.on('message', common.mustNotCall()); + w.on('error', common.mustCall((err) => { + assert(/SyntaxError/.test(err)); + })); +} else { + throw new Error('foo'); +} diff --git a/test/parallel/test-worker-uncaught-exception-async.js b/test/parallel/test-worker-uncaught-exception-async.js new file mode 100644 index 00000000000000..c1d2a5f4fcab16 --- /dev/null +++ b/test/parallel/test-worker-uncaught-exception-async.js @@ -0,0 +1,20 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker'); + +// Do not use isMainThread so that this test itself can be run inside a Worker. +if (!process.env.HAS_STARTED_WORKER) { + process.env.HAS_STARTED_WORKER = 1; + const w = new Worker(__filename); + w.on('message', common.mustNotCall()); + w.on('error', common.mustCall((err) => { + // TODO(addaleax): be more specific here + assert(/foo/.test(err)); + })); +} else { + setImmediate(() => { + throw new Error('foo'); + }); +} diff --git a/test/parallel/test-worker-uncaught-exception.js b/test/parallel/test-worker-uncaught-exception.js new file mode 100644 index 00000000000000..b0e3ad11fae839 --- /dev/null +++ b/test/parallel/test-worker-uncaught-exception.js @@ -0,0 +1,18 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker'); + +// Do not use isMainThread so that this test itself can be run inside a Worker. +if (!process.env.HAS_STARTED_WORKER) { + process.env.HAS_STARTED_WORKER = 1; + const w = new Worker(__filename); + w.on('message', common.mustNotCall()); + w.on('error', common.mustCall((err) => { + // TODO(addaleax): be more specific here + assert(/foo/.test(err)); + })); +} else { + throw new Error('foo'); +} diff --git a/test/parallel/test-worker.js b/test/parallel/test-worker.js new file mode 100644 index 00000000000000..3fa6e67a347b37 --- /dev/null +++ b/test/parallel/test-worker.js @@ -0,0 +1,18 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker, isMainThread, parentPort } = require('worker'); + +if (isMainThread) { + const w = new Worker(__filename); + w.on('message', common.mustCall((message) => { + assert.strictEqual(message, 'Hello, world!'); + })); +} else { + setImmediate(() => { + process.nextTick(() => { + parentPort.postMessage('Hello, world!'); + }); + }); +} diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index 84a3e3b1f4dc05..af08d7b6567018 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -38,6 +38,7 @@ common.crashOnUnhandledRejection(); // TODO(addaleax): Test for these delete providers.STREAMPIPE; delete providers.MESSAGEPORT; + delete providers.WORKER; const objKeys = Object.keys(providers); if (objKeys.length > 0) From 396d78567ca795846342fc1c67aeeac6c2948d1b Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sun, 3 Jun 2018 11:13:51 +0200 Subject: [PATCH 12/26] fixup! worker: initial implementation --- doc/api/worker.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/doc/api/worker.md b/doc/api/worker.md index e9fde9ffc1d777..3517a4c86ac767 100644 --- a/doc/api/worker.md +++ b/doc/api/worker.md @@ -242,8 +242,7 @@ Notable differences inside a Worker environment are: - The [`process.stdin`][], [`process.stdout`][] and [`process.stderr`][] properties are set to `null`. - The [`require('worker').isMainThread`][] property is set to `false`. -- The [`require('worker').postMessage()`][] method is available and the - [`require('worker').on('workerMessage')`][] event will be emitted. +- The [`require('worker').parentPort`][] message port is available, - [`process.exit()`][] does not stop the whole program, just the single thread, and [`process.abort()`][] is not available. - [`process.chdir()`][] and `process` methods that set group or user ids @@ -265,12 +264,10 @@ Creating `Worker` instances inside of other `Worker`s is possible. Like [Web Workers][] and the [`cluster` module][], two-way communication can be achieved through inter-thread message passing. Internally, a `Worker` has a built-in pair of [`MessagePort`][]s that are already associated with each other -when the `Worker` is created. While the `MessagePort` objects are not directly -exposed, their functionalities are exposed through [`worker.postMessage()`][] -and the [`worker.on('message')`][] event on the `Worker` object for the parent -thread, and [`require('worker').postMessage()`][] and the -[`require('worker').on('workerMessage')`][] on `require('worker')` for the -child thread. +when the `Worker` is created. While the `MessagePort` object on the parent side +is not directly exposed, its functionalities are exposed through +[`worker.postMessage()`][] and the [`worker.on('message')`][] event +on the `Worker` object for the parent thread. To create custom messaging channels (which is encouraged over using the default global channel because it facilitates separation of concerns), users can create From 65d45425d2c37c0dc9ddadc87c703f15329fb45d Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 5 Jun 2018 01:21:56 +0200 Subject: [PATCH 13/26] fixup! worker: initial implementation --- src/node_worker.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/node_worker.cc b/src/node_worker.cc index 4d9e1ee98dca17..24b5f6477f7395 100644 --- a/src/node_worker.cc +++ b/src/node_worker.cc @@ -79,8 +79,6 @@ Worker::Worker(Environment* env, Local wrap) Locker locker(isolate_); Isolate::Scope isolate_scope(isolate_); HandleScope handle_scope(isolate_); - Local context = NewContext(isolate_); - Context::Scope context_scope(context); isolate_data_.reset(CreateIsolateData(isolate_, &loop_, @@ -88,6 +86,9 @@ Worker::Worker(Environment* env, Local wrap) array_buffer_allocator_.get())); CHECK(isolate_data_); + Local context = NewContext(isolate_); + Context::Scope context_scope(context); + // TODO(addaleax): Use CreateEnvironment(), or generally another public API. env_.reset(new Environment(isolate_data_.get(), context, From d0535eb8af3a647bc8412d9460e451da6b08cb7c Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Wed, 6 Jun 2018 00:37:26 +0200 Subject: [PATCH 14/26] fixup! worker: initial implementation --- src/node_worker.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node_worker.cc b/src/node_worker.cc index 24b5f6477f7395..366dca353d345c 100644 --- a/src/node_worker.cc +++ b/src/node_worker.cc @@ -234,13 +234,13 @@ void Worker::DisposeIsolate() { if (isolate_ == nullptr) return; - isolate_->Dispose(); - CHECK(isolate_data_); MultiIsolatePlatform* platform = isolate_data_->platform(); platform->CancelPendingDelayedTasks(isolate_); isolate_data_.reset(); + + isolate_->Dispose(); isolate_ = nullptr; } From 17e01a20f93e0804cf81f58de56a39e075ba3a4a Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 25 Sep 2017 16:30:09 -0700 Subject: [PATCH 15/26] test: add test against unsupported worker features Refs: https://github.com/ayojs/ayo/pull/113 Reviewed-By: Anna Henningsen --- .../test-worker-unsupported-things.js | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 test/parallel/test-worker-unsupported-things.js diff --git a/test/parallel/test-worker-unsupported-things.js b/test/parallel/test-worker-unsupported-things.js new file mode 100644 index 00000000000000..9f407a6806a7c9 --- /dev/null +++ b/test/parallel/test-worker-unsupported-things.js @@ -0,0 +1,41 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker, isMainThread, parentPort } = require('worker'); + +if (isMainThread) { + const w = new Worker(__filename); + w.on('message', common.mustCall((message) => { + assert.strictEqual(message, true); + })); +} else { + { + const before = process.title; + process.title += ' in worker'; + assert.strictEqual(process.title, before); + } + + { + const before = process.debugPort; + process.debugPort++; + assert.strictEqual(process.debugPort, before); + } + + assert.strictEqual('abort' in process, false); + assert.strictEqual('chdir' in process, false); + assert.strictEqual('setuid' in process, false); + assert.strictEqual('seteuid' in process, false); + assert.strictEqual('setgid' in process, false); + assert.strictEqual('setegid' in process, false); + assert.strictEqual('setgroups' in process, false); + assert.strictEqual('initgroups' in process, false); + + assert.strictEqual('_startProfilerIdleNotifier' in process, false); + assert.strictEqual('_stopProfilerIdleNotifier' in process, false); + assert.strictEqual('_debugProcess' in process, false); + assert.strictEqual('_debugPause' in process, false); + assert.strictEqual('_debugEnd' in process, false); + + parentPort.postMessage(true); +} From 53b660ba13286e7a850952e26cbaf2a5e1316f53 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 20 Sep 2017 14:23:31 -0700 Subject: [PATCH 16/26] worker: restrict supported extensions Only allow `.js` and `.mjs` extensions to provide future-proofing for file type detection. Refs: https://github.com/ayojs/ayo/pull/117 Reviewed-By: Stephen Belanger Reviewed-By: Olivia Hugger Reviewed-By: Anna Henningsen --- lib/internal/errors.js | 3 +++ lib/internal/worker.js | 13 ++++++--- test/parallel/test-worker-unsupported-path.js | 27 +++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 test/parallel/test-worker-unsupported-path.js diff --git a/lib/internal/errors.js b/lib/internal/errors.js index d59531debbd042..54201d0d1e7f4c 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -849,4 +849,7 @@ E('ERR_WORKER_NEED_ABSOLUTE_PATH', TypeError); E('ERR_WORKER_UNSERIALIZABLE_ERROR', 'Serializing an uncaught exception failed', Error); +E('ERR_WORKER_UNSUPPORTED_EXTENSION', + 'The worker script extension must be ".js" or ".mjs". Received "%s"', + TypeError); E('ERR_ZLIB_INITIALIZATION_FAILED', 'Initialization failed', Error); diff --git a/lib/internal/worker.js b/lib/internal/worker.js index c982478b9334e8..edd954d8a3a2be 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -8,7 +8,8 @@ const util = require('util'); const { ERR_INVALID_ARG_TYPE, ERR_WORKER_NEED_ABSOLUTE_PATH, - ERR_WORKER_UNSERIALIZABLE_ERROR + ERR_WORKER_UNSERIALIZABLE_ERROR, + ERR_WORKER_UNSUPPORTED_EXTENSION, } = require('internal/errors').codes; const { internalBinding } = require('internal/bootstrap/loaders'); @@ -136,8 +137,14 @@ class Worker extends EventEmitter { throw new ERR_INVALID_ARG_TYPE('filename', 'string', filename); } - if (!options.eval && !path.isAbsolute(filename)) { - throw new ERR_WORKER_NEED_ABSOLUTE_PATH(filename); + if (!options.eval) { + if (!path.isAbsolute(filename)) { + throw new ERR_WORKER_NEED_ABSOLUTE_PATH(filename); + } + const ext = path.extname(filename); + if (ext !== '.js' && ext !== '.mjs') { + throw new ERR_WORKER_UNSUPPORTED_EXTENSION(ext); + } } // Set up the C++ handle for the worker, as well as some internal wiring. diff --git a/test/parallel/test-worker-unsupported-path.js b/test/parallel/test-worker-unsupported-path.js new file mode 100644 index 00000000000000..3716377ec2fb1f --- /dev/null +++ b/test/parallel/test-worker-unsupported-path.js @@ -0,0 +1,27 @@ +// Flags: --experimental-worker +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker'); + +{ + const expectedErr = common.expectsError({ + code: 'ERR_WORKER_NEED_ABSOLUTE_PATH', + type: TypeError + }, 4); + assert.throws(() => { new Worker('a.js'); }, expectedErr); + assert.throws(() => { new Worker('b'); }, expectedErr); + assert.throws(() => { new Worker('c/d.js'); }, expectedErr); + assert.throws(() => { new Worker('a.mjs'); }, expectedErr); +} + +{ + const expectedErr = common.expectsError({ + code: 'ERR_WORKER_UNSUPPORTED_EXTENSION', + type: TypeError + }, 3); + assert.throws(() => { new Worker('/b'); }, expectedErr); + assert.throws(() => { new Worker('/c.wasm'); }, expectedErr); + assert.throws(() => { new Worker('/d.txt'); }, expectedErr); +} From 004075b25fd43e9743c3fc024b14bebb4212fd10 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sun, 13 May 2018 23:25:14 +0200 Subject: [PATCH 17/26] worker: enable stdio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provide `stdin`, `stdout` and `stderr` options for the `Worker` constructor, and make these available to the worker thread under their usual names. The default for `stdin` is an empty stream, the default for `stdout` and `stderr` is redirecting to the parent thread’s corresponding stdio streams. --- doc/api/worker.md | 44 +++++++- lib/internal/process/stdio.js | 14 +-- lib/internal/worker.js | 166 ++++++++++++++++++++++++++++- test/parallel/test-worker-stdio.js | 43 ++++++++ 4 files changed, 256 insertions(+), 11 deletions(-) create mode 100644 test/parallel/test-worker-stdio.js diff --git a/doc/api/worker.md b/doc/api/worker.md index 3517a4c86ac767..2fa55cfa2ddadf 100644 --- a/doc/api/worker.md +++ b/doc/api/worker.md @@ -240,7 +240,7 @@ Most Node.js APIs are available inside of it. Notable differences inside a Worker environment are: - The [`process.stdin`][], [`process.stdout`][] and [`process.stderr`][] - properties are set to `null`. + may be redirected by the parent thread. - The [`require('worker').isMainThread`][] property is set to `false`. - The [`require('worker').parentPort`][] message port is available, - [`process.exit()`][] does not stop the whole program, just the single thread, @@ -313,6 +313,13 @@ if (isMainThread) { described in the [HTML structured clone algorithm][], and an error will be thrown if the object cannot be cloned (e.g. because it contains `function`s). + * stdin {boolean} If this is set to `true`, then `worker.stdin` will + provide a writable stream whose contents will appear as `process.stdin` + inside the Worker. By default, no data is provided. + * stdout {boolean} If this is set to `true`, then `worker.stdout` will + not automatically be piped through to `process.stdout` in the parent. + * stderr {boolean} If this is set to `true`, then `worker.stderr` will + not automatically be piped through to `process.stderr` in the parent. ### Event: 'error' + +* {stream.Readable} + +This is a readable stream which contains data written to [`process.stderr`][] +inside the worker thread. If `stderr: true` was not passed to the +[`Worker`][] constructor, then data will be piped to the parent thread's +[`process.stderr`][] stream. + +### worker.stdin + + +* {null|stream.Writable} + +If `stdin: true` was passed to the [`Worker`][] constructor, this is a +writable stream. The data written to this stream will be made available in +the worker thread as [`process.stdin`][]. + +### worker.stdout + + +* {stream.Readable} + +This is a readable stream which contains data written to [`process.stdout`][] +inside the worker thread. If `stdout: true` was not passed to the +[`Worker`][] constructor, then data will be piped to the parent thread's +[`process.stdout`][] stream. + ### worker.terminate([callback]) @@ -461,14 +461,14 @@ active handle in the event system. If the worker is already `unref()`ed calling [`Buffer`]: buffer.html [`EventEmitter`]: events.html -[`MessagePort`]: #worker_class_messageport -[`port.postMessage()`]: #worker_port_postmessage_value_transferlist -[`Worker`]: #worker_class_worker -[`worker.terminate()`]: #worker_worker_terminate_callback -[`worker.postMessage()`]: #worker_worker_postmessage_value_transferlist_1 -[`worker.on('message')`]: #worker_event_message_1 -[`worker.threadId`]: #worker_worker_threadid_1 -[`port.on('message')`]: #worker_event_message +[`MessagePort`]: #worker_threads_class_messageport +[`port.postMessage()`]: #worker_threads_port_postmessage_value_transferlist +[`Worker`]: #worker_threads_class_worker +[`worker.terminate()`]: #worker_threads_worker_terminate_callback +[`worker.postMessage()`]: #worker_threads_worker_postmessage_value_transferlist_1 +[`worker.on('message')`]: #worker_threads_event_message_1 +[`worker.threadId`]: #worker_threads_worker_threadid_1 +[`port.on('message')`]: #worker_threads_event_message [`process.exit()`]: process.html#process_process_exit_code [`process.abort()`]: process.html#process_process_abort [`process.chdir()`]: process.html#process_process_chdir_directory @@ -477,11 +477,11 @@ active handle in the event system. If the worker is already `unref()`ed calling [`process.stderr`]: process.html#process_process_stderr [`process.stdout`]: process.html#process_process_stdout [`process.title`]: process.html#process_process_title -[`require('worker_threads').workerData`]: #worker_worker_workerdata -[`require('worker_threads').on('workerMessage')`]: #worker_event_workermessage -[`require('worker_threads').postMessage()`]: #worker_worker_postmessage_value_transferlist -[`require('worker_threads').isMainThread`]: #worker_worker_ismainthread -[`require('worker_threads').threadId`]: #worker_worker_threadid +[`require('worker_threads').workerData`]: #worker_threads_worker_workerdata +[`require('worker_threads').on('workerMessage')`]: #worker_threads_event_workermessage +[`require('worker_threads').postMessage()`]: #worker_threads_worker_postmessage_value_transferlist +[`require('worker_threads').isMainThread`]: #worker_threads_worker_ismainthread +[`require('worker_threads').threadId`]: #worker_threads_worker_threadid [`cluster` module]: cluster.html [`inspector`]: inspector.html [v8.serdes]: v8.html#v8_serialization_api From 2947dea823e9d6fad9bed2f6542aa1c326f460d2 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sun, 3 Jun 2018 12:24:22 +0200 Subject: [PATCH 24/26] fixup! worker: rename to worker_threads --- lib/internal/modules/cjs/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/modules/cjs/helpers.js b/lib/internal/modules/cjs/helpers.js index 55eaed7d376506..5b5199c262ae3b 100644 --- a/lib/internal/modules/cjs/helpers.js +++ b/lib/internal/modules/cjs/helpers.js @@ -106,7 +106,7 @@ const builtinLibs = [ ]; if (process.binding('config').experimentalWorker) { - builtinLibs.push('worker'); + builtinLibs.push('worker_threads'); builtinLibs.sort(); } From 735227ed30c52f5b124bf73c1c5c92571a226f74 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 5 Jun 2018 22:00:52 +0200 Subject: [PATCH 25/26] fixup! worker: rename to worker_threads --- lib/internal/bootstrap/loaders.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/internal/bootstrap/loaders.js b/lib/internal/bootstrap/loaders.js index 417e8594e14aab..4291092532ec94 100644 --- a/lib/internal/bootstrap/loaders.js +++ b/lib/internal/bootstrap/loaders.js @@ -195,7 +195,8 @@ NativeModule.isInternal = function(id) { return id.startsWith('internal/') || - (id === 'worker' && !process.binding('config').experimentalWorker); + (id === 'worker_threads' && + !process.binding('config').experimentalWorker); }; } From cbbddeab1a3d9e62319a8574025c2b8e7ee42ced Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Wed, 6 Jun 2018 01:55:41 +0200 Subject: [PATCH 26/26] fixup! worker: rename to worker_threads --- tools/run-worker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/run-worker.js b/tools/run-worker.js index a9dd773ecf304f..7590e460a404ae 100644 --- a/tools/run-worker.js +++ b/tools/run-worker.js @@ -5,7 +5,7 @@ if (typeof require === 'undefined') { } const path = require('path'); -const { Worker } = require('worker'); +const { Worker } = require('worker_threads'); new Worker(path.resolve(process.cwd(), process.argv[2])) .on('exit', (code) => process.exitCode = code);