diff --git a/Makefile b/Makefile index fb978380e5..850db10783 100644 --- a/Makefile +++ b/Makefile @@ -465,6 +465,8 @@ test-with-async-hooks: $(CI_JS_SUITES) \ $(CI_NATIVE_SUITES) +test-worker: + $(PYTHON) tools/test.py --mode=release workers ifneq ("","$(wildcard deps/v8/tools/run-tests.py)") test-v8: v8 diff --git a/benchmark/fixtures/echo.worker.js b/benchmark/fixtures/echo.worker.js new file mode 100644 index 0000000000..61ee804815 --- /dev/null +++ b/benchmark/fixtures/echo.worker.js @@ -0,0 +1,7 @@ +'use strict'; + +const worker = require('worker'); + +worker.on('workerMessage', (msg) => { + worker.postMessage(msg); +}); diff --git a/benchmark/worker/echo.js b/benchmark/worker/echo.js new file mode 100644 index 0000000000..e83b525b67 --- /dev/null +++ b/benchmark/worker/echo.js @@ -0,0 +1,72 @@ +'use strict'; + +const { Worker } = require('worker'); +const common = require('../common.js'); +const path = require('path'); +const bench = common.createBenchmark(main, { + workers: [1], + payload: ['string', 'object'], + sendsPerBroadcast: [1, 10], + n: [1e5] +}); + +const workerPath = path.resolve(__dirname, '..', 'fixtures', 'echo.worker.js'); + +function main(conf) { + const n = +conf.n; + const workers = +conf.workers; + const sends = +conf.sendsPerBroadcast; + const expectedPerBroadcast = sends * workers; + var payload; + var readies = 0; + var broadcasts = 0; + var msgCount = 0; + + switch (conf.payload) { + case 'string': + payload = 'hello world!'; + break; + case 'object': + payload = { action: 'pewpewpew', powerLevel: 9001 }; + break; + default: + throw new Error('Unsupported payload type'); + } + + const workerObjs = []; + + for (var i = 0; i < workers; ++i) { + const worker = new Worker(workerPath); + workerObjs.push(worker); + worker.on('online', onOnline); + worker.on('message', onMessage); + } + + function onOnline() { + if (++readies === workers) { + bench.start(); + broadcast(); + } + } + + function broadcast() { + if (broadcasts++ === n) { + bench.end(n); + for (const worker of workerObjs) { + worker.unref(); + } + return; + } + for (const worker of workerObjs) { + for (var i = 0; i < sends; ++i) + worker.postMessage(payload); + } + } + + function onMessage() { + if (++msgCount === expectedPerBroadcast) { + msgCount = 0; + broadcast(); + } + } +} diff --git a/common.gypi b/common.gypi index 98268068f9..0b1db8e2e6 100644 --- a/common.gypi +++ b/common.gypi @@ -28,6 +28,11 @@ # Enable disassembler for `--print-code` v8 options 'v8_enable_disassembler': 1, + 'v8_extra_library_files': [ + './lib/extras/events.js', + './lib/extras/messaging.js' + ], + # Don't bake anything extra into the snapshot. 'v8_use_external_startup_data%': 0, diff --git a/doc/api/_toc.md b/doc/api/_toc.md index b3987ed8e4..36f528132b 100644 --- a/doc/api/_toc.md +++ b/doc/api/_toc.md @@ -50,6 +50,7 @@ * [Utilities](util.html) * [V8](v8.html) * [VM](vm.html) +* [Worker](worker.html) * [ZLIB](zlib.html)
diff --git a/doc/api/domain.md b/doc/api/domain.md index 30e93e2bea..940831d0f0 100644 --- a/doc/api/domain.md +++ b/doc/api/domain.md @@ -31,6 +31,8 @@ will be notified, rather than losing the context of the error in the `process.on('uncaughtException')` handler, or causing the program to exit immediately with an error code. +*Note*: This module is not available in [`Worker`][]s. + ## Warning: Don't Ignore Errors! @@ -495,3 +497,4 @@ rejections. [`setInterval()`]: timers.html#timers_setinterval_callback_delay_args [`setTimeout()`]: timers.html#timers_settimeout_callback_delay_args [`throw`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/throw +[`Worker`]: #worker_class_worker diff --git a/doc/api/process.md b/doc/api/process.md index 8b8ce0f43d..c17bd42d17 100644 --- a/doc/api/process.md +++ b/doc/api/process.md @@ -415,6 +415,8 @@ added: v0.7.0 The `process.abort()` method causes the Node.js process to exit immediately and generate a core file. +*Note*: This feature is not available in [`Worker`][] threads. + ## process.arch + +* `port` {MessagePort} +* `contextifiedSandbox` {Object} A contextified object as returned by the + `vm.createContext()` method. +* Returns: {MessagePort} + +Bind a `MessagePort` to a specific VM context. This returns a new `MessagePort` +object, whose prototype and methods act as if they were created in the passed +context. The received messages will also be emitted as objects from the passed +context. + +The `port` object on which this method was called can not be used for sending +or receiving further messages. + ## vm.runInDebugContext(code) + +> Stability: 1 - Experimental + +The `worker` module provides a way to create multiple environments running +on independent threads, and to create message channels between those. It +can be accessed using: + +```js +const worker = require('worker'); +``` + +Workers are useful for performing CPU-intensive JavaScript operations; do not +use them for I/O, since Ayo’s built-in mechanisms for performing operations +asynchronously already treat it more efficiently than Worker threads can. + +Workers can also, unlike child processes or when using the `cluster` module, +share memory efficiently by transferring `ArrayBuffer` instances or sharing +`SharedArrayBuffer` instances between them. + +## Example + +```js +const { Worker, isMainThread, postMessage, workerData } = require('worker'); + +if (isMainThread) { + module.exports = async function parseJSAsync(script) { + return new Promise((resolve, reject) => { + const worker = new Worker(__filename, { + workerData: script + }); + worker.on('message', resolve); + worker.on('error', reject); + worker.on('exit', (code) => { + if (code !== 0) + reject(new Error(`Worker stopped with exit code ${code}`)); + }); + }); + }; +} else { + const { parse } = require('some-js-parsing-library'); + const script = workerData; + postMessage(parse(script)); +} +``` + +## Event: 'workerMessage' + + +If this thread was spawned by a `Worker`, this event emits messages sent using +[`worker.postMessage()`][]. See the [`port.on('message')`][] event for +more details. + +Listeners on this event will receive a clone of the `value` parameter as passed +to `postMessage()` and no further arguments. + +*Note*: This event is emitted on the `worker` module as returned by +`require('worker')` itself. + +## worker.isMainThread + + +* {boolean} + +Is `true` if this code is not running inside of a [`Worker`][] thread. + +## worker.postMessage(value[, transferList]) + + +* `value` {any} +* `transferList` {Object[]} + +* Returns: {undefined} + +This method is available if this is a `Worker` thread, which can be tested +using [`require('worker').isMainThread`][]. + +Send a message to the parent thread’s `Worker` instance that will be received +via [`worker.on('message')`][]. See [`port.postMessage()`][] for +more details. + +### worker.ref() + + +Opposite of `unref`, calling `ref` on a previously `unref`d worker will *not* +let the program exit if it's the only active handle left (the default behavior). +If the worker is `ref`d calling `ref` again will have no effect. + +### 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`d calling +`unref` again will have no effect. + +## 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 + + + +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 or [`vm` context][vm]s. + +For transferring `MessagePort` instances between VM contexts, see +[`vm.moveMessagePortToContext()`][]. + +*Note*: 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: 'error' + + +The `'error'` event is emitted if the worker thread throws an uncaught +expection. In that case, the worker will be terminated. + +### 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() + + +* Returns: {undefined} + +Disables further sending of messages on either side of the connection. +You should call this method once you know that no further communication +will happen over this `MessagePort`. + +### port.postMessage(value[, transferList]) + + +* `value` {any} +* `transferList` {Object[]} + +* Returns: {undefined} + +Sends a JavaScript value to the receiving side of this channel. +`value` will be transferred in a way +that is compatible with the [HTML structured clone algorithm][]. In particular, +it may contain circular references and objects like typed arrays that `JSON` +is not able to serialize. + +`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`). + +If `value` contains [`SharedArrayBuffer`][] instances, those will be accessible +from either thread. + +`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]. + +*Note*: Because the object cloning uses the structured clone algorithm, +non-enumberable properties, accessors, and prototypes are not preserved. +In particular, [`Buffer`][] objects will be read as plain [`Uint8Array`][]s +on the receiving side. + +### port.ref() + + +Opposite of `unref`, calling `ref` on a previously `unref`d port will *not* +let the program exit if it's the only active handle left (the default behavior). +If the port is `ref`d calling `ref` again will have no effect. + +### 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`d calling +`unref` again will have no effect. + +### port.start() + + +* Returns: {undefined} + +Starts receiving messages on this `MessagePort`. When using this port +as an event emitter, this will be called automatically once `'message'` +listeners are attached. This means that this method does not need to be used +unless you are using [`vm.moveMessagePortToContext()`][] to move this `port` +into another VM context. + +## Class: Worker + + +The `Worker` class represents an independent JavaScript execution thread. +Most Ayo 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 [`domain`][] module is not usable inside of workers. +- The [`require('worker').isMainThread`][] property is set to `false`. +- The [`require('worker').postMessage()`][] method is available and the + [`require('worker').on('workerMessage')`][] event is being emitted. +- [`process.exit()`][] does not stop the whole program, just the single thread, + and [`process.abort()`][] is not available. +- [`process.chdir()`][] as well as `process` methods that set group or user ids + are not available. +- [`process.env`][] is a read-only reference to the environment variables. +- [`process.title`][] can not be modified. +- Native addons that were not build with explicit `Worker` support can not be + loaded. +- Execution may stop at any point as a result of the [`worker.terminate()`][] + method being invoked. +- IPC channels from parent processes are not accessible. + +Creating `Worker` instances inside of other `Worker`s is permitted. + +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 seperation 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 can not be cloned (e.g. because it contains + `function`s). + * maxSemiSpaceSize {integer} An optional memory limit in MB for the thread’s + heap’s semi-space, which contains most short-lived objects. + * maxOldSpaceSize {integer} An optional memory limit in MB for the thread’s + main heap. + +### 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.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. + +*Note*: Currently, not all code in the internals of Ayo.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 Ayo.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`][]. + +[`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.postMessage()`]: #worker_port_postmessage_value_transferlist +[`port.on('message')`]: #worker_event_message +[`process.exit()`]: process.html#process_process_exit +[`process.exit()`]: process.html#process_process_exit +[`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_is_main_thread +[`require('worker').threadId`]: #worker_worker_threadid +[`domain`]: domain.html +[`vm.moveMessagePortToContext()`]: vm.html#vm_vm_movemessageporttocontext_port_context +[`cluster` module]: cluster.html +[vm]: vm.html#vm_vm_executing_javascript +[v8.serdes]: v8.html#v8_serialization_api +[`SharedArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer +[`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 +[Web Workers]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API + diff --git a/lib/console.js b/lib/console.js index 54c8aba829..ea19fbd54b 100644 --- a/lib/console.js +++ b/lib/console.js @@ -245,7 +245,12 @@ Console.prototype.groupEnd = function groupEnd() { this[kGroupIndent].slice(0, this[kGroupIndent].length - 2); }; -module.exports = new Console(process.stdout, process.stderr); +if (isMainThread) { + module.exports = new Console(process.stdout, process.stderr); +} else { + const { SyncWriteStream } = require('internal/fs'); + module.exports = new Console(new SyncWriteStream(1), new SyncWriteStream(2)); +} module.exports.Console = Console; function noop() {} diff --git a/lib/events.js b/lib/events.js index 1414a1429d..06e0209a5c 100644 --- a/lib/events.js +++ b/lib/events.js @@ -21,51 +21,9 @@ 'use strict'; -var domain; - -function EventEmitter() { - EventEmitter.init.call(this); -} -module.exports = EventEmitter; - -// Backwards-compat with node 0.10.x -EventEmitter.EventEmitter = EventEmitter; - -EventEmitter.usingDomains = false; - -EventEmitter.prototype.domain = undefined; -EventEmitter.prototype._events = undefined; -EventEmitter.prototype._maxListeners = undefined; - -// By default EventEmitters will print a warning if more than 10 listeners are -// added to it. This is a useful default which helps finding memory leaks. -var defaultMaxListeners = 10; +var { EventEmitter } = process.binding('extras').binding; -var errors; -function lazyErrors() { - if (errors === undefined) - errors = require('internal/errors'); - return errors; -} - -Object.defineProperty(EventEmitter, 'defaultMaxListeners', { - enumerable: true, - get: function() { - return defaultMaxListeners; - }, - set: function(arg) { - // force global console to be compiled. - // see https://github.com/nodejs/node/issues/4467 - console; - // check whether the input is a positive number (whose value is zero or - // greater and not a NaN). - if (typeof arg !== 'number' || arg < 0 || arg !== arg) { - const errors = lazyErrors(); - throw new errors.TypeError('ERR_OUT_OF_RANGE', 'defaultMaxListeners'); - } - defaultMaxListeners = arg; - } -}); +var domain; EventEmitter.init = function() { this.domain = null; @@ -85,468 +43,4 @@ EventEmitter.init = function() { this._maxListeners = this._maxListeners || undefined; }; -// Obviously not all Emitters should be limited to 10. This function allows -// that to be increased. Set to zero for unlimited. -EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { - if (typeof n !== 'number' || n < 0 || isNaN(n)) { - const errors = lazyErrors(); - throw new errors.TypeError('ERR_OUT_OF_RANGE', 'n'); - } - this._maxListeners = n; - return this; -}; - -function $getMaxListeners(that) { - if (that._maxListeners === undefined) - return EventEmitter.defaultMaxListeners; - return that._maxListeners; -} - -EventEmitter.prototype.getMaxListeners = function getMaxListeners() { - return $getMaxListeners(this); -}; - -// These standalone emit* functions are used to optimize calling of event -// handlers for fast cases because emit() itself often has a variable number of -// arguments and can be deoptimized because of that. These functions always have -// the same number of arguments and thus do not get deoptimized, so the code -// inside them can execute faster. -function emitNone(handler, isFn, self) { - if (isFn) - handler.call(self); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].call(self); - } -} -function emitOne(handler, isFn, self, arg1) { - if (isFn) - handler.call(self, arg1); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].call(self, arg1); - } -} -function emitTwo(handler, isFn, self, arg1, arg2) { - if (isFn) - handler.call(self, arg1, arg2); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].call(self, arg1, arg2); - } -} -function emitThree(handler, isFn, self, arg1, arg2, arg3) { - if (isFn) - handler.call(self, arg1, arg2, arg3); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].call(self, arg1, arg2, arg3); - } -} - -function emitMany(handler, isFn, self, args) { - if (isFn) - handler.apply(self, args); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].apply(self, args); - } -} - -EventEmitter.prototype.emit = function emit(type) { - var er, handler, len, args, i, events, domain; - var needDomainExit = false; - var doError = (type === 'error'); - - events = this._events; - if (events) - doError = (doError && events.error == null); - else if (!doError) - return false; - - domain = this.domain; - - // If there is no 'error' event listener then throw. - if (doError) { - if (arguments.length > 1) - er = arguments[1]; - if (domain) { - if (!er) { - const errors = lazyErrors(); - er = new errors.Error('ERR_UNHANDLED_ERROR'); - } - if (typeof er === 'object' && er !== null) { - er.domainEmitter = this; - er.domain = domain; - er.domainThrown = false; - } - domain.emit('error', er); - } else if (er instanceof Error) { - throw er; // Unhandled 'error' event - } else { - // At least give some kind of context to the user - const errors = lazyErrors(); - const err = new errors.Error('ERR_UNHANDLED_ERROR', er); - err.context = er; - throw err; - } - return false; - } - - handler = events[type]; - - if (!handler) - return false; - - if (domain && this !== process) { - domain.enter(); - needDomainExit = true; - } - - var isFn = typeof handler === 'function'; - len = arguments.length; - switch (len) { - // fast cases - case 1: - emitNone(handler, isFn, this); - break; - case 2: - emitOne(handler, isFn, this, arguments[1]); - break; - case 3: - emitTwo(handler, isFn, this, arguments[1], arguments[2]); - break; - case 4: - emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); - break; - // slower - default: - args = new Array(len - 1); - for (i = 1; i < len; i++) - args[i - 1] = arguments[i]; - emitMany(handler, isFn, this, args); - } - - if (needDomainExit) - domain.exit(); - - return true; -}; - -function _addListener(target, type, listener, prepend) { - var m; - var events; - var existing; - - if (typeof listener !== 'function') { - const errors = lazyErrors(); - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'listener', 'function'); - } - - events = target._events; - if (!events) { - events = target._events = Object.create(null); - target._eventsCount = 0; - } else { - // To avoid recursion in the case that type === "newListener"! Before - // adding it to the listeners, first emit "newListener". - if (events.newListener) { - target.emit('newListener', type, - listener.listener ? listener.listener : listener); - - // Re-assign `events` because a newListener handler could have caused the - // this._events to be assigned to a new object - events = target._events; - } - existing = events[type]; - } - - if (!existing) { - // Optimize the case of one listener. Don't need the extra array object. - existing = events[type] = listener; - ++target._eventsCount; - } else { - if (typeof existing === 'function') { - // Adding the second element, need to change to array. - existing = events[type] = - prepend ? [listener, existing] : [existing, listener]; - } else { - // If we've already got an array, just append. - if (prepend) { - existing.unshift(listener); - } else { - existing.push(listener); - } - } - - // Check for listener leak - if (!existing.warned) { - m = $getMaxListeners(target); - if (m && m > 0 && existing.length > m) { - existing.warned = true; - // No error code for this since it is a Warning - const w = new Error('Possible EventEmitter memory leak detected. ' + - `${existing.length} ${String(type)} listeners ` + - 'added. Use emitter.setMaxListeners() to ' + - 'increase limit'); - w.name = 'MaxListenersExceededWarning'; - w.emitter = target; - w.type = type; - w.count = existing.length; - process.emitWarning(w); - } - } - } - - return target; -} - -EventEmitter.prototype.addListener = function addListener(type, listener) { - return _addListener(this, type, listener, false); -}; - -EventEmitter.prototype.on = EventEmitter.prototype.addListener; - -EventEmitter.prototype.prependListener = - function prependListener(type, listener) { - return _addListener(this, type, listener, true); - }; - -function onceWrapper() { - if (!this.fired) { - this.target.removeListener(this.type, this.wrapFn); - this.fired = true; - switch (arguments.length) { - case 0: - return this.listener.call(this.target); - case 1: - return this.listener.call(this.target, arguments[0]); - case 2: - return this.listener.call(this.target, arguments[0], arguments[1]); - case 3: - return this.listener.call(this.target, arguments[0], arguments[1], - arguments[2]); - default: - const args = new Array(arguments.length); - for (var i = 0; i < args.length; ++i) - args[i] = arguments[i]; - this.listener.apply(this.target, args); - } - } -} - -function _onceWrap(target, type, listener) { - var state = { fired: false, wrapFn: undefined, target, type, listener }; - var wrapped = onceWrapper.bind(state); - wrapped.listener = listener; - state.wrapFn = wrapped; - return wrapped; -} - -EventEmitter.prototype.once = function once(type, listener) { - if (typeof listener !== 'function') { - const errors = lazyErrors(); - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'listener', 'function'); - } - this.on(type, _onceWrap(this, type, listener)); - return this; -}; - -EventEmitter.prototype.prependOnceListener = - function prependOnceListener(type, listener) { - if (typeof listener !== 'function') { - const errors = lazyErrors(); - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'listener', - 'function'); - } - this.prependListener(type, _onceWrap(this, type, listener)); - return this; - }; - -// Emits a 'removeListener' event if and only if the listener was removed. -EventEmitter.prototype.removeListener = - function removeListener(type, listener) { - var list, events, position, i, originalListener; - - if (typeof listener !== 'function') { - const errors = lazyErrors(); - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'listener', - 'function'); - } - - events = this._events; - if (!events) - return this; - - list = events[type]; - if (!list) - return this; - - if (list === listener || list.listener === listener) { - if (--this._eventsCount === 0) - this._events = Object.create(null); - else { - delete events[type]; - if (events.removeListener) - this.emit('removeListener', type, list.listener || listener); - } - } else if (typeof list !== 'function') { - position = -1; - - for (i = list.length - 1; i >= 0; i--) { - if (list[i] === listener || list[i].listener === listener) { - originalListener = list[i].listener; - position = i; - break; - } - } - - if (position < 0) - return this; - - if (position === 0) - list.shift(); - else - spliceOne(list, position); - - if (list.length === 1) - events[type] = list[0]; - - if (events.removeListener) - this.emit('removeListener', type, originalListener || listener); - } - - return this; - }; - -EventEmitter.prototype.removeAllListeners = - function removeAllListeners(type) { - var listeners, events, i; - - events = this._events; - if (!events) - return this; - - // not listening for removeListener, no need to emit - if (!events.removeListener) { - if (arguments.length === 0) { - this._events = Object.create(null); - this._eventsCount = 0; - } else if (events[type]) { - if (--this._eventsCount === 0) - this._events = Object.create(null); - else - delete events[type]; - } - return this; - } - - // emit removeListener for all listeners on all events - if (arguments.length === 0) { - var keys = Object.keys(events); - var key; - for (i = 0; i < keys.length; ++i) { - key = keys[i]; - if (key === 'removeListener') continue; - this.removeAllListeners(key); - } - this.removeAllListeners('removeListener'); - this._events = Object.create(null); - this._eventsCount = 0; - return this; - } - - listeners = events[type]; - - if (typeof listeners === 'function') { - this.removeListener(type, listeners); - } else if (listeners) { - // LIFO order - for (i = listeners.length - 1; i >= 0; i--) { - this.removeListener(type, listeners[i]); - } - } - - return this; - }; - -EventEmitter.prototype.listeners = function listeners(type) { - var evlistener; - var ret; - var events = this._events; - - if (!events) - ret = []; - else { - evlistener = events[type]; - if (!evlistener) - ret = []; - else if (typeof evlistener === 'function') - ret = [evlistener.listener || evlistener]; - else - ret = unwrapListeners(evlistener); - } - - return ret; -}; - -EventEmitter.listenerCount = function(emitter, type) { - if (typeof emitter.listenerCount === 'function') { - return emitter.listenerCount(type); - } else { - return listenerCount.call(emitter, type); - } -}; - -EventEmitter.prototype.listenerCount = listenerCount; -function listenerCount(type) { - const events = this._events; - - if (events) { - const evlistener = events[type]; - - if (typeof evlistener === 'function') { - return 1; - } else if (evlistener) { - return evlistener.length; - } - } - - return 0; -} - -EventEmitter.prototype.eventNames = function eventNames() { - return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; -}; - -// About 1.5x faster than the two-arg version of Array#splice(). -function spliceOne(list, index) { - for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) - list[i] = list[k]; - list.pop(); -} - -function arrayClone(arr, n) { - var copy = new Array(n); - for (var i = 0; i < n; ++i) - copy[i] = arr[i]; - return copy; -} - -function unwrapListeners(arr) { - const ret = new Array(arr.length); - for (var i = 0; i < ret.length; ++i) { - ret[i] = arr[i].listener || arr[i]; - } - return ret; -} +module.exports = EventEmitter; diff --git a/lib/extras/events.js b/lib/extras/events.js new file mode 100644 index 0000000000..d06d89cce5 --- /dev/null +++ b/lib/extras/events.js @@ -0,0 +1,554 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +/* eslint-disable indent, strict */ +(function(global, binding, v8) { + +'use strict'; + +function EventEmitter() { + EventEmitter.init.call(this); +} +binding.EventEmitter = EventEmitter; + +// Backwards-compat with node 0.10.x +EventEmitter.EventEmitter = EventEmitter; + +// Overridden when domain module is loaded +EventEmitter.usingDomains = false; + +EventEmitter.prototype.domain = undefined; +EventEmitter.prototype._events = undefined; +EventEmitter.prototype._maxListeners = undefined; + +// By default EventEmitters will print a warning if more than 10 listeners are +// added to it. This is a useful default which helps finding memory leaks. +var defaultMaxListeners = 10; + +Object.defineProperty(EventEmitter, 'defaultMaxListeners', { + enumerable: true, + get: function() { + return defaultMaxListeners; + }, + set: function(arg) { + // force global console to be compiled. + // see https://github.com/nodejs/node/issues/4467 + console; + // check whether the input is a positive number (whose value is zero or + // greater and not a NaN). + if (typeof arg !== 'number' || arg < 0 || arg !== arg) { + const err = + new TypeError('The "defaultMaxListeners" argument is out of range'); + err.code = 'ERR_OUT_OF_RANGE'; + throw err; + } + defaultMaxListeners = arg; + } +}); + +// This version of the function is not domain-aware, suitable for contexts that +// do not have access to Node.js domains. It will get monkey patched in +// lib/events.js. +EventEmitter.init = function() { + this.domain = null; + + if (!this._events || this._events === Object.getPrototypeOf(this)._events) { + this._events = Object.create(null); + this._eventsCount = 0; + } + + this._maxListeners = this._maxListeners || undefined; +}; + +// Obviously not all Emitters should be limited to 10. This function allows +// that to be increased. Set to zero for unlimited. +EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { + if (typeof n !== 'number' || n < 0 || isNaN(n)) { + const err = new TypeError('The "n" argument is out of range'); + err.code = 'ERR_OUT_OF_RANGE'; + throw err; + } + this._maxListeners = n; + return this; +}; + +function $getMaxListeners(that) { + if (that._maxListeners === undefined) + return EventEmitter.defaultMaxListeners; + return that._maxListeners; +} + +EventEmitter.prototype.getMaxListeners = function getMaxListeners() { + return $getMaxListeners(this); +}; + +// These standalone emit* functions are used to optimize calling of event +// handlers for fast cases because emit() itself often has a variable number of +// arguments and can be deoptimized because of that. These functions always have +// the same number of arguments and thus do not get deoptimized, so the code +// inside them can execute faster. +function emitNone(handler, isFn, self) { + if (isFn) + handler.call(self); + else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + listeners[i].call(self); + } +} +function emitOne(handler, isFn, self, arg1) { + if (isFn) + handler.call(self, arg1); + else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + listeners[i].call(self, arg1); + } +} +function emitTwo(handler, isFn, self, arg1, arg2) { + if (isFn) + handler.call(self, arg1, arg2); + else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + listeners[i].call(self, arg1, arg2); + } +} +function emitThree(handler, isFn, self, arg1, arg2, arg3) { + if (isFn) + handler.call(self, arg1, arg2, arg3); + else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + listeners[i].call(self, arg1, arg2, arg3); + } +} + +function emitMany(handler, isFn, self, args) { + if (isFn) + handler.apply(self, args); + else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + listeners[i].apply(self, args); + } +} + +EventEmitter.prototype.emit = function emit(type) { + var er, handler, len, args, i, events, domain; + var needDomainExit = false; + var doError = (type === 'error'); + + events = this._events; + if (events) + doError = (doError && events.error == null); + else if (!doError) + return false; + + domain = this.domain; + + // If there is no 'error' event listener then throw. + if (doError) { + if (arguments.length > 1) + er = arguments[1]; + if (domain) { + if (!er) { + er = new Error('Unhandled "error" event'); + er.code = 'ERR_UNHANDLED_ERROR'; + } + if (typeof er === 'object' && er !== null) { + er.domainEmitter = this; + er.domain = domain; + er.domainThrown = false; + } + domain.emit('error', er); + } else if (er instanceof Error) { + throw er; // Unhandled 'error' event + } else { + // At least give some kind of context to the user + const err = new Error('Unhandled error. (' + er + ')'); + err.context = er; + err.code = 'ERR_UNHANDLED_ERROR'; + throw err; + } + return false; + } + + handler = events[type]; + + if (!handler) + return false; + + if (typeof process !== 'undefined' && domain && this !== process) { + domain.enter(); + needDomainExit = true; + } + + var isFn = typeof handler === 'function'; + len = arguments.length; + switch (len) { + // fast cases + case 1: + emitNone(handler, isFn, this); + break; + case 2: + emitOne(handler, isFn, this, arguments[1]); + break; + case 3: + emitTwo(handler, isFn, this, arguments[1], arguments[2]); + break; + case 4: + emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); + break; + // slower + default: + args = new Array(len - 1); + for (i = 1; i < len; i++) + args[i - 1] = arguments[i]; + emitMany(handler, isFn, this, args); + } + + if (needDomainExit) + domain.exit(); + + return true; +}; + +function _addListener(target, type, listener, prepend) { + var m; + var events; + var existing; + + if (typeof listener !== 'function') { + const err = + new TypeError('The "listener" argument must be of type function'); + err.code = 'ERR_INVALID_ARG_TYPE'; + throw err; + } + + events = target._events; + if (!events) { + events = target._events = Object.create(null); + target._eventsCount = 0; + } else { + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (events.newListener) { + target.emit('newListener', type, + listener.listener ? listener.listener : listener); + + // Re-assign `events` because a newListener handler could have caused the + // this._events to be assigned to a new object + events = target._events; + } + existing = events[type]; + } + + if (!existing) { + // Optimize the case of one listener. Don't need the extra array object. + existing = events[type] = listener; + ++target._eventsCount; + } else { + if (typeof existing === 'function') { + // Adding the second element, need to change to array. + existing = events[type] = + prepend ? [listener, existing] : [existing, listener]; + } else { + // If we've already got an array, just append. + if (prepend) { + existing.unshift(listener); + } else { + existing.push(listener); + } + } + + // Check for listener leak + // Ignore if the current context does not have a process object + if (typeof process !== 'undefined' && !existing.warned) { + m = $getMaxListeners(target); + if (m && m > 0 && existing.length > m) { + existing.warned = true; + const w = new Error('Possible EventEmitter memory leak detected. ' + + `${existing.length} ${String(type)} listeners ` + + 'added. Use emitter.setMaxListeners() to ' + + 'increase limit'); + w.name = 'MaxListenersExceededWarning'; + w.emitter = target; + w.type = type; + w.count = existing.length; + process.emitWarning(w); + } + } + } + + return target; +} + +EventEmitter.prototype.addListener = function addListener(type, listener) { + return _addListener(this, type, listener, false); +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.prependListener = + function prependListener(type, listener) { + return _addListener(this, type, listener, true); + }; + +function onceWrapper() { + if (!this.fired) { + this.target.removeListener(this.type, this.wrapFn); + this.fired = true; + switch (arguments.length) { + case 0: + return this.listener.call(this.target); + case 1: + return this.listener.call(this.target, arguments[0]); + case 2: + return this.listener.call(this.target, arguments[0], arguments[1]); + case 3: + return this.listener.call(this.target, arguments[0], arguments[1], + arguments[2]); + default: + const args = new Array(arguments.length); + for (var i = 0; i < args.length; ++i) + args[i] = arguments[i]; + this.listener.apply(this.target, args); + } + } +} + +function _onceWrap(target, type, listener) { + var state = { fired: false, wrapFn: undefined, target, type, listener }; + var wrapped = onceWrapper.bind(state); + wrapped.listener = listener; + state.wrapFn = wrapped; + return wrapped; +} + +EventEmitter.prototype.once = function once(type, listener) { + if (typeof listener !== 'function') { + const err = + new TypeError('The "listener" argument must be of type function'); + err.code = 'ERR_INVALID_ARG_TYPE'; + throw err; + } + this.on(type, _onceWrap(this, type, listener)); + return this; +}; + +EventEmitter.prototype.prependOnceListener = + function prependOnceListener(type, listener) { + if (typeof listener !== 'function') { + const err = + new TypeError('The "listener" argument must be of type function'); + err.code = 'ERR_INVALID_ARG_TYPE'; + throw err; + } + this.prependListener(type, _onceWrap(this, type, listener)); + return this; + }; + +// Emits a 'removeListener' event if and only if the listener was removed. +EventEmitter.prototype.removeListener = + function removeListener(type, listener) { + var list, events, position, i, originalListener; + + if (typeof listener !== 'function') { + const err = + new TypeError('The "listener" argument must be of type function'); + err.code = 'ERR_INVALID_ARG_TYPE'; + throw err; + } + + events = this._events; + if (!events) + return this; + + list = events[type]; + if (!list) + return this; + + if (list === listener || list.listener === listener) { + if (--this._eventsCount === 0) + this._events = Object.create(null); + else { + delete events[type]; + if (events.removeListener) + this.emit('removeListener', type, list.listener || listener); + } + } else if (typeof list !== 'function') { + position = -1; + + for (i = list.length - 1; i >= 0; i--) { + if (list[i] === listener || list[i].listener === listener) { + originalListener = list[i].listener; + position = i; + break; + } + } + + if (position < 0) + return this; + + if (position === 0) + list.shift(); + else + spliceOne(list, position); + + if (list.length === 1) + events[type] = list[0]; + + if (events.removeListener) + this.emit('removeListener', type, originalListener || listener); + } + + return this; + }; + +EventEmitter.prototype.removeAllListeners = + function removeAllListeners(type) { + var listeners, events, i; + + events = this._events; + if (!events) + return this; + + // not listening for removeListener, no need to emit + if (!events.removeListener) { + if (arguments.length === 0) { + this._events = Object.create(null); + this._eventsCount = 0; + } else if (events[type]) { + if (--this._eventsCount === 0) + this._events = Object.create(null); + else + delete events[type]; + } + return this; + } + + // emit removeListener for all listeners on all events + if (arguments.length === 0) { + var keys = Object.keys(events); + var key; + for (i = 0; i < keys.length; ++i) { + key = keys[i]; + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = Object.create(null); + this._eventsCount = 0; + return this; + } + + listeners = events[type]; + + if (typeof listeners === 'function') { + this.removeListener(type, listeners); + } else if (listeners) { + // LIFO order + for (i = listeners.length - 1; i >= 0; i--) { + this.removeListener(type, listeners[i]); + } + } + + return this; + }; + +EventEmitter.prototype.listeners = function listeners(type) { + var evlistener; + var ret; + var events = this._events; + + if (!events) + ret = []; + else { + evlistener = events[type]; + if (!evlistener) + ret = []; + else if (typeof evlistener === 'function') + ret = [evlistener.listener || evlistener]; + else + ret = unwrapListeners(evlistener); + } + + return ret; +}; + +EventEmitter.listenerCount = function(emitter, type) { + if (typeof emitter.listenerCount === 'function') { + return emitter.listenerCount(type); + } else { + return listenerCount.call(emitter, type); + } +}; + +EventEmitter.prototype.listenerCount = listenerCount; +function listenerCount(type) { + const events = this._events; + + if (events) { + const evlistener = events[type]; + + if (typeof evlistener === 'function') { + return 1; + } else if (evlistener) { + return evlistener.length; + } + } + + return 0; +} + +EventEmitter.prototype.eventNames = function eventNames() { + return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; +}; + +// About 1.5x faster than the two-arg version of Array#splice(). +function spliceOne(list, index) { + for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) + list[i] = list[k]; + list.pop(); +} + +function arrayClone(arr, n) { + var copy = new Array(n); + for (var i = 0; i < n; ++i) + copy[i] = arr[i]; + return copy; +} + +function unwrapListeners(arr) { + const ret = new Array(arr.length); + for (var i = 0; i < ret.length; ++i) { + ret[i] = arr[i].listener || arr[i]; + } + return ret; +} + +}); diff --git a/lib/extras/messaging.js b/lib/extras/messaging.js new file mode 100644 index 0000000000..cc8373890f --- /dev/null +++ b/lib/extras/messaging.js @@ -0,0 +1,82 @@ +/* eslint-disable indent, strict */ +(function(global, binding, v8) { + +'use strict'; + +const { defineProperties, setPrototypeOf } = global.Object; + +// 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. +function onmessage(payload, flag) { + if (flag !== 0 /*MESSAGE_FLAG_NONE*/ && + flag < 100 /*MESSAGE_FLAG_CUSTOM_OFFSET*/) { + // This was not handled in C++, but it is also not a custom message in the + // sense that it was generated in JS, so some special handling is still + // required for deserialization. + // (This is primarily for error situations) + // debug(`[${threadId}] received raw message`, flag, payload); + return this.emit('flaggedMessage', flag, payload); + } + + // debug(`[${threadId}] received message`, flag, payload); + // Emit the flag and deserialized object to userland. + if (flag === 0 || flag === undefined) + this.emit('message', payload); + else + this.emit('flaggedMessage', flag, payload); +} + +function oninit() { + // 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. + this.unref(); + this.on('newListener', (name) => { + if (name === 'message' && this.listenerCount('message') === 0) { + this.ref(); + this.start(); + } + }); + this.on('removeListener', (name) => { + if (name === 'message' && this.listenerCount('message') === 0) { + this.stop(); + this.unref(); + } + }); +} + +function onclose() { + this.emit('close'); +} + +function makeMessagePort(MessagePort) { + setPrototypeOf(MessagePort, binding.EventEmitter); + setPrototypeOf(MessagePort.prototype, binding.EventEmitter.prototype); + + defineProperties(MessagePort.prototype, { + onmessage: { + enumerable: true, + writable: false, + value: onmessage + }, + + oninit: { + enumerable: true, + writable: false, + value: oninit + }, + + onclose: { + enumerable: true, + writable: false, + value: onclose + } + }); +} + +binding.makeMessagePort = makeMessagePort; + +}); diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js index 4ebc81c488..edcc3eb4f0 100644 --- a/lib/internal/bootstrap_node.js +++ b/lib/internal/bootstrap_node.js @@ -11,6 +11,10 @@ let internalBinding; let isMainThread; + // Add process to global first as it may be used by native modules loaded + // later on. + global.process = process; + function startup() { const EventEmitter = NativeModule.require('events'); process._eventsCount = 0; @@ -265,7 +269,6 @@ enumerable: false, configurable: true }); - global.process = process; const util = NativeModule.require('util'); function makeGetter(name) { diff --git a/lib/internal/error-serdes.js b/lib/internal/error-serdes.js new file mode 100644 index 0000000000..afedc3ae58 --- /dev/null +++ b/lib/internal/error-serdes.js @@ -0,0 +1,118 @@ +'use strict'; + +const Buffer = require('buffer').Buffer; +const { serialize, deserialize } = require('v8'); +const { SafeSet } = require('internal/safe_globals'); + +const kSerializedError = 0; +const kSerializedObject = 1; +const kInspectedError = 2; + +const GetPrototypeOf = Object.getPrototypeOf; +const GetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; +const GetOwnPropertyNames = Object.getOwnPropertyNames; +const DefineProperty = Object.defineProperty; +const Assign = Object.assign; +const ObjectPrototypeToString = + Function.prototype.call.bind(Object.prototype.toString); +const ForEach = Function.prototype.call.bind(Array.prototype.forEach); +const Call = Function.prototype.call.bind(Function.prototype.call); + +const errors = { + Error, TypeError, RangeError, URIError, SyntaxError, ReferenceError, EvalError +}; +const errorConstructorNames = new SafeSet(Object.keys(errors)); + +function TryGetAllProperties(object, target = object) { + const all = Object.create(null); + if (object === null) + return all; + Assign(all, TryGetAllProperties(GetPrototypeOf(object), target)); + const keys = GetOwnPropertyNames(object); + ForEach(keys, (key) => { + const descriptor = GetOwnPropertyDescriptor(object, key); + const getter = descriptor.get; + if (getter && key !== '__proto__') { + try { + descriptor.value = Call(getter, target); + } catch (e) {} + } + if ('value' in descriptor && typeof descriptor.value !== 'function') { + delete descriptor.get; + delete descriptor.set; + all[key] = descriptor; + } + }); + return all; +} + +function GetConstructors(object) { + const constructors = []; + + for (var current = object; + current !== null; + current = GetPrototypeOf(current)) { + const desc = GetOwnPropertyDescriptor(current, 'constructor'); + if (desc && desc.value) { + DefineProperty(constructors, constructors.length, { + value: desc.value, enumerable: true + }); + } + } + + return constructors; +} + +function GetName(object) { + const desc = GetOwnPropertyDescriptor(object, 'name'); + return desc && desc.value; +} + +let util; +function lazyUtil() { + if (!util) + util = require('util'); + return util; +} + +function serializeError(error) { + try { + if (typeof error === 'object' && + ObjectPrototypeToString(error) === '[object Error]') { + const constructors = GetConstructors(error); + for (var i = constructors.length - 1; i >= 0; i--) { + const name = GetName(constructors[i]); + if (errorConstructorNames.has(name)) { + try { error.stack; } catch (e) {} + const serialized = serialize({ + constructor: name, + properties: TryGetAllProperties(error) + }); + return Buffer.concat([Buffer.from([kSerializedError]), serialized]); + } + } + } + } catch (e) {} + try { + const serialized = serialize(error); + return Buffer.concat([Buffer.from([kSerializedObject]), serialized]); + } catch (e) {} + return Buffer.concat([Buffer.from([kInspectedError]), + Buffer.from(lazyUtil().inspect(error), 'utf8')]); +} + +function deserializeError(error) { + switch (error[0]) { + case kSerializedError: + const { constructor, properties } = deserialize(error.slice(1)); + const ctor = errors[constructor]; + return Object.create(ctor.prototype, properties); + case kSerializedObject: + return deserialize(error.slice(1)); + case kInspectedError: + return error.toString('utf8', 1); + } + require('assert').fail('This should not happen'); +} + +module.exports = { serializeError, deserializeError }; diff --git a/lib/internal/errors.js b/lib/internal/errors.js index ec8f7c1885..7a5fab2cd0 100755 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -357,6 +357,8 @@ E('ERR_WORKER_NEED_ABSOLUTE_PATH', E('ERR_WORKER_OUT_OF_MEMORY', 'The worker script ran out of memory'); E('ERR_WORKER_UNSERIALIZABLE_ERROR', 'Serializing an uncaught exception failed'); +E('ERR_WORKER_UNSUPPORTED_EXTENSION', + 'The worker script extension must be ".js" or ".mjs". Received "%s"'); E('ERR_ZLIB_BINDING_CLOSED', 'zlib binding closed'); function invalidArgType(name, expected, actual) { diff --git a/lib/internal/worker.js b/lib/internal/worker.js index 86530346f5..2ea824b6c1 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -1,6 +1,5 @@ 'use strict'; -const Buffer = require('buffer').Buffer; const EventEmitter = require('events'); const assert = require('assert'); const path = require('path'); @@ -8,6 +7,8 @@ const util = require('util'); const errors = require('internal/errors'); const { MessagePort, MessageChannel } = internalBinding('messaging'); +const { serializeError, deserializeError } = require('internal/error-serdes'); + util.inherits(MessagePort, EventEmitter); const { @@ -41,65 +42,8 @@ const debug = util.debuglog('worker'); const kUpAndRunning = MESSAGE_FLAG_CUSTOM_OFFSET; const kLoadScript = MESSAGE_FLAG_CUSTOM_OFFSET + 1; -// 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. -function onmessage(payload, flag) { - if (flag !== MESSAGE_FLAG_NONE && flag < MESSAGE_FLAG_CUSTOM_OFFSET) { - // This was not handled in C++, but it is also not a custom message in the - // sense that it was generated in JS, so some special handling is still - // required for deserialization. - // (This is primarily for error situations) - debug(`[${threadId}] received raw message`, flag, payload); - return this.emit('flaggedMessage', flag, payload); - } - - debug(`[${threadId}] received message`, flag, payload); - // Emit the flag and deserialized object to userland. - if (flag === 0 || flag === undefined) - this.emit('message', payload); - else - this.emit('flaggedMessage', flag, payload); -} - -Object.defineProperty(MessagePort.prototype, 'onmessage', { - enumerable: true, - configurable: true, - get() { return onmessage; }, - set(value) { - Object.defineProperty(this, { - writable: true, - enumerable: true, - configurable: true, - value - }); - this.ref(); - this.start(); - } -}); - -function oninit() { - setupPortReferencing(this, this, 'message'); -} - -Object.defineProperty(MessagePort.prototype, 'oninit', { - enumerable: true, - writable: false, - value: oninit -}); - -function onclose() { - this.emit('close'); -} - -Object.defineProperty(MessagePort.prototype, 'onclose', { - enumerable: true, - writable: false, - value: onclose -}); - function setupPortReferencing(port, eventEmitter, eventName) { + // TODO(addaleax): Merge with oninit() in lib/extras/messaging.js // 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 @@ -129,8 +73,14 @@ class Worker extends EventEmitter { 'string', filename); } - if (!options.eval && !path.isAbsolute(filename)) { - throw new errors.TypeError('ERR_WORKER_NEED_ABSOLUTE_PATH', filename); + if (!options.eval) { + if (!path.isAbsolute(filename)) { + throw new errors.TypeError('ERR_WORKER_NEED_ABSOLUTE_PATH', filename); + } + const ext = path.extname(filename); + if (ext !== '.js' && ext !== '.mjs') { + throw new errors.TypeError('ERR_WORKER_UNSUPPORTED_EXTENSION', ext); + } } const resourceLimits = { @@ -271,14 +221,6 @@ function setupChild(evalScript) { port.start(); } -// TODO(addaleax): These can be improved a lot. -function serializeError(error) { - return Buffer.from(util.inspect(error), 'utf8'); -} - -function deserializeError(error) { - return error.toString('utf8'); -} module.exports = { MessagePort, diff --git a/lib/vm.js b/lib/vm.js index e7fccc9749..f4a5d7443b 100644 --- a/lib/vm.js +++ b/lib/vm.js @@ -30,6 +30,8 @@ const { runInDebugContext } = process.binding('contextify'); +const { moveMessagePortToContext } = internalBinding('messaging'); + // The binding provides a few useful primitives: // - Script(code, { filename = "evalmachine.anonymous", // displayErrors = true } = {}) @@ -143,6 +145,7 @@ module.exports = { Script, createContext, createScript, + moveMessagePortToContext, runInDebugContext, runInContext, runInNewContext, diff --git a/node.gyp b/node.gyp index 76423c9554..b979d86d4d 100644 --- a/node.gyp +++ b/node.gyp @@ -95,6 +95,7 @@ 'lib/internal/crypto/util.js', 'lib/internal/encoding.js', 'lib/internal/errors.js', + 'lib/internal/error-serdes.js', 'lib/internal/freelist.js', 'lib/internal/fs.js', 'lib/internal/http.js', @@ -206,6 +207,7 @@ 'src/node_constants.cc', 'src/node_contextify.cc', 'src/node_debug_options.cc', + 'src/node_extras.cc', 'src/node_file.cc', 'src/node_http2.cc', 'src/node_http_parser.cc', @@ -259,6 +261,7 @@ 'src/node_http2_core-inl.h', 'src/node_buffer.h', 'src/node_constants.h', + 'src/node_contextify.h', 'src/node_debug_options.h', 'src/node_http2.h', 'src/node_http2_state.h', @@ -689,10 +692,12 @@ '<(OBJ_PATH)<(OBJ_SEPARATOR)handle_wrap.<(OBJ_SUFFIX)', '<(OBJ_PATH)<(OBJ_SEPARATOR)node.<(OBJ_SUFFIX)', '<(OBJ_PATH)<(OBJ_SEPARATOR)node_buffer.<(OBJ_SUFFIX)', + '<(OBJ_PATH)<(OBJ_SEPARATOR)node_contextify.<(OBJ_SUFFIX)', '<(OBJ_PATH)<(OBJ_SEPARATOR)node_i18n.<(OBJ_SUFFIX)', '<(OBJ_PATH)<(OBJ_SEPARATOR)node_messaging.<(OBJ_SUFFIX)', '<(OBJ_PATH)<(OBJ_SEPARATOR)node_perf.<(OBJ_SUFFIX)', '<(OBJ_PATH)<(OBJ_SEPARATOR)node_url.<(OBJ_SUFFIX)', + '<(OBJ_PATH)<(OBJ_SEPARATOR)node_watchdog.<(OBJ_SUFFIX)', '<(OBJ_PATH)<(OBJ_SEPARATOR)node_worker.<(OBJ_SUFFIX)', '<(OBJ_PATH)<(OBJ_SEPARATOR)util.<(OBJ_SUFFIX)', '<(OBJ_PATH)<(OBJ_SEPARATOR)sharedarraybuffer-metadata.<(OBJ_SUFFIX)', diff --git a/src/env.h b/src/env.h index cc8f5a6303..d980122dda 100644 --- a/src/env.h +++ b/src/env.h @@ -95,6 +95,7 @@ class Worker; V(contextify_context_private_symbol, "node:contextify:context") \ V(contextify_global_private_symbol, "node:contextify:global") \ V(decorated_private_symbol, "node:decorated") \ + V(messageport_initialized_private_symbol, "node:messagePortInitialized") \ V(npn_buffer_private_symbol, "node:npnBuffer") \ V(processed_private_symbol, "node:processed") \ V(sab_lifetimepartner_symbol, "node:sharedArrayBufferLifetimePartner") \ diff --git a/src/node.cc b/src/node.cc index 640fbdf8a8..c0b67d6e15 100644 --- a/src/node.cc +++ b/src/node.cc @@ -2663,11 +2663,28 @@ node_module* get_linked_module(const char* name) { return FindModule(modlist_linked, name, NM_F_LINKED); } -struct DLib { +namespace { + +Mutex dlib_mutex; + +struct DLib; + +std::unordered_map> dlopen_cache; +std::unordered_map> + handle_to_dlib; + +struct DLib : public std::enable_shared_from_this { std::string filename_; std::string errmsg_; - void* handle_; + void* handle_ = nullptr; int flags_; + std::unordered_set users_; + node_module* own_info = nullptr; + + DLib() {} + ~DLib() { + Close(); + } #ifdef __POSIX__ static const int kDefaultFlags = RTLD_LAZY; @@ -2703,90 +2720,162 @@ struct DLib { uv_dlclose(&lib_); } #endif // !__POSIX__ + + DLib(const DLib& other) = delete; + DLib(DLib&& other) = delete; + DLib& operator=(const DLib& other) = delete; + DLib& operator=(DLib&& other) = delete; + + void AddEnvironment(Environment* env) { + if (users_.count(env) > 0) return; + users_.insert(env); + if (env->is_main_thread()) return; + struct cleanup_hook_data { + std::shared_ptr info; + Environment* env; + }; + env->AddCleanupHook([](void* arg) { + Mutex::ScopedLock lock(dlib_mutex); + cleanup_hook_data* cbdata = static_cast(arg); + std::shared_ptr info = cbdata->info; + info->users_.erase(cbdata->env); + delete cbdata; + if (info->users_.empty()) { + std::vector filenames; + + for (const auto& entry : dlopen_cache) { + if (entry.second == info) + filenames.push_back(entry.first); + } + for (const std::string& filename : filenames) + dlopen_cache.erase(filename); + + handle_to_dlib.erase(info->handle_); + } + }, static_cast(new cleanup_hook_data { shared_from_this(), env })); + } }; +} // anonymous namespace + // DLOpen is process.dlopen(module, filename, flags). // Used to load 'module.node' dynamically shared objects. -// -// FIXME(bnoordhuis) Not multi-context ready. TBD how to resolve the conflict -// when two contexts try to load the same shared object. Maybe have a shadow -// cache that's a plain C list or hash table that's shared across contexts? static void DLOpen(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + node_module* mp; + std::shared_ptr dlib; + Local module = args[0]->ToObject(env->isolate()); - CHECK_EQ(modpending, nullptr); + do { + Mutex::ScopedLock lock(dlib_mutex); + CHECK_EQ(modpending, nullptr); - if (args.Length() < 2) { - env->ThrowError("process.dlopen needs at least 2 arguments."); - return; - } + if (args.Length() < 2) { + env->ThrowError("process.dlopen needs at least 2 arguments."); + return; + } - int32_t flags = DLib::kDefaultFlags; - if (args.Length() > 2 && !args[2]->Int32Value(env->context()).To(&flags)) { - return env->ThrowTypeError("flag argument must be an integer."); - } + int32_t flags = DLib::kDefaultFlags; + if (args.Length() > 2 && !args[2]->Int32Value(env->context()).To(&flags)) { + return env->ThrowTypeError("flag argument must be an integer."); + } + + node::Utf8Value filename(env->isolate(), args[1]); // Cast + auto it = dlopen_cache.find(*filename); - Local module = args[0]->ToObject(env->isolate()); // Cast - node::Utf8Value filename(env->isolate(), args[1]); // Cast - DLib dlib; - dlib.filename_ = *filename; - dlib.flags_ = flags; - bool is_opened = dlib.Open(); + if (it != dlopen_cache.end()) { + dlib = it->second; + mp = dlib->own_info; + dlib->AddEnvironment(env); + break; + } + + dlib = std::make_shared(); + dlib->filename_ = *filename; + dlib->flags_ = flags; + bool is_opened = dlib->Open(); + + if (is_opened) { + if (handle_to_dlib.count(dlib->handle_) > 0) { + dlib = handle_to_dlib[dlib->handle_]; + mp = dlib->own_info; + dlib->AddEnvironment(env); + break; + } + } - // Objects containing v14 or later modules will have registered themselves - // on the pending list. Activate all of them now. At present, only one - // module per object is supported. - node_module* const mp = modpending; - modpending = nullptr; + // Objects containing v14 or later modules will have registered themselves + // on the pending list. Activate all of them now. At present, only one + // module per object is supported. + mp = modpending; + modpending = nullptr; - if (!is_opened) { - Local errmsg = OneByteString(env->isolate(), dlib.errmsg_.c_str()); - dlib.Close(); + if (!is_opened) { + Local errmsg = + OneByteString(env->isolate(), dlib->errmsg_.c_str()); #ifdef _WIN32 - // Windows needs to add the filename into the error message - errmsg = String::Concat(errmsg, args[1]->ToString(env->isolate())); + // Windows needs to add the filename into the error message + errmsg = String::Concat(errmsg, args[1]->ToString(env->isolate())); #endif // _WIN32 - env->isolate()->ThrowException(Exception::Error(errmsg)); - return; - } + env->isolate()->ThrowException(Exception::Error(errmsg)); + return; + } - if (mp == nullptr) { - dlib.Close(); - env->ThrowError("Module did not self-register."); - return; - } - if (mp->nm_version == -1) { - if (env->EmitNapiWarning()) { - ProcessEmitWarning(env, "N-API is an experimental feature and could " - "change at any time."); + if (mp == nullptr) { + env->ThrowError("Module did not self-register."); + return; } - } else if (mp->nm_version != NODE_MODULE_VERSION) { - char errmsg[1024]; - snprintf(errmsg, - sizeof(errmsg), - "The module '%s'" - "\nwas compiled against a different Node.js version using" - "\nNODE_MODULE_VERSION %d. This version of Node.js requires" - "\nNODE_MODULE_VERSION %d. Please try re-compiling or " - "re-installing\nthe module (for instance, using `npm rebuild` " - "or `npm install`).", - *filename, mp->nm_version, NODE_MODULE_VERSION); - - // NOTE: `mp` is allocated inside of the shared library's memory, calling - // `dlclose` will deallocate it - dlib.Close(); - env->ThrowError(errmsg); - return; - } - if (mp->nm_flags & NM_F_BUILTIN) { - dlib.Close(); - env->ThrowError("Built-in module self-registered."); - return; - } + if (mp->nm_version == -1) { + if (env->EmitNapiWarning()) { + ProcessEmitWarning(env, "N-API is an experimental feature and could " + "change at any time."); + } + } else if (mp->nm_version != NODE_MODULE_VERSION) { + char errmsg[1024]; + snprintf(errmsg, + sizeof(errmsg), + "The module '%s'" + "\nwas compiled against a different Node.js version using" + "\nNODE_MODULE_VERSION %d. This version of Node.js requires" + "\nNODE_MODULE_VERSION %d. Please try re-compiling or " + "re-installing\nthe module (for instance, using `npm rebuild` " + "or `npm install`).", + *filename, mp->nm_version, NODE_MODULE_VERSION); + + // NOTE: `mp` is allocated inside of the shared library's memory, + // calling `dlclose` will deallocate it + env->ThrowError(errmsg); + return; + } + + if (mp->nm_flags & NM_F_BUILTIN) { + env->ThrowError("Built-in module self-registered."); + return; + } + + if (!env->is_main_thread() && !(mp->nm_flags & NM_F_WORKER_ENABLED)) { + env->ThrowError("Native modules need to explicitly indicate multi-" + "isolate/multi-thread support by using " + "`NODE_MODULE_WORKER_ENABLED` or " + "`NAPI_MODULE_WORKER_ENABLED` to register themselves."); + return; + } + if (mp->nm_context_register_func == nullptr && + mp->nm_register_func == nullptr) { + env->ThrowError("Module has no declared entry point."); + return; + } + + dlib->own_info = mp; + handle_to_dlib[dlib->handle_] = dlib; + dlopen_cache[*filename] = dlib; + + dlib->AddEnvironment(env); - mp->nm_dso_handle = dlib.handle_; - mp->nm_link = modlist_addon; - modlist_addon = mp; + mp->nm_dso_handle = dlib->handle_; + mp->nm_link = modlist_addon; + modlist_addon = mp; + } while (false); Local exports_string = env->exports_string(); Local exports = module->Get(exports_string)->ToObject(env->isolate()); @@ -2796,7 +2885,6 @@ static void DLOpen(const FunctionCallbackInfo& args) { } else if (mp->nm_register_func != nullptr) { mp->nm_register_func(exports, module, mp->nm_priv); } else { - dlib.Close(); env->ThrowError("Module has no declared entry point."); return; } diff --git a/src/node.h b/src/node.h index 7531c97a33..bba573e4a7 100644 --- a/src/node.h +++ b/src/node.h @@ -444,6 +444,7 @@ typedef void (*addon_context_register_func)( #define NM_F_BUILTIN 0x01 #define NM_F_LINKED 0x02 #define NM_F_INTERNAL 0x04 +#define NM_F_WORKER_ENABLED 0x400 struct node_module { int nm_version; @@ -531,6 +532,10 @@ extern "C" NODE_EXTERN void node_module_register(void* mod); #define NODE_MODULE_CONTEXT_AWARE_BUILTIN(modname, regfunc) \ NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, NULL, NM_F_BUILTIN) \ +#define NODE_MODULE_WORKER_ENABLED(modname, regfunc) \ + NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, NULL, \ + NM_F_WORKER_ENABLED) + /* * For backward compatibility in add-on modules. */ diff --git a/src/node_api.cc b/src/node_api.cc index 59ca97bbb4..bc82894226 100644 --- a/src/node_api.cc +++ b/src/node_api.cc @@ -856,6 +856,8 @@ void napi_module_register_cb(v8::Local exports, // Registers a NAPI module. void napi_module_register(napi_module* mod) { + static_assert(NAPI_F_WORKER_ENABLED == NM_F_WORKER_ENABLED, + "Worker-enabled flags match for N-API and Node"); int module_version = -1; #ifdef EXTERNAL_NAPI module_version = NODE_MODULE_VERSION; diff --git a/src/node_api.h b/src/node_api.h index 29070c3ec8..8af363ef9c 100644 --- a/src/node_api.h +++ b/src/node_api.h @@ -80,6 +80,8 @@ typedef struct { #define EXTERN_C_END #endif +#define NAPI_F_WORKER_ENABLED 0x400 + #define NAPI_MODULE_X(modname, regfunc, priv, flags) \ EXTERN_C_START \ static napi_module _module = \ @@ -100,6 +102,9 @@ typedef struct { #define NAPI_MODULE(modname, regfunc) \ NAPI_MODULE_X(modname, regfunc, NULL, 0) +#define NAPI_MODULE_WORKER_ENABLED(modname, regfunc) \ + NAPI_MODULE_X(modname, regfunc, NULL, NAPI_F_WORKER_ENABLED) + #define NAPI_AUTO_LENGTH SIZE_MAX EXTERN_C_START diff --git a/src/node_contextify.cc b/src/node_contextify.cc index c8830b45f3..5b05104c0c 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -1071,6 +1071,18 @@ void InitContextify(Local target, } } // anonymous namespace + +MaybeLocal ContextFromContextifiedSandbox( + Environment* env, + Local sandbox) { + auto contextify_context = + ContextifyContext::ContextFromContextifiedSandbox(env, sandbox); + if (contextify_context == nullptr) + return MaybeLocal(); + else + return contextify_context->context(); +} + } // namespace node NODE_MODULE_CONTEXT_AWARE_BUILTIN(contextify, node::InitContextify) diff --git a/src/node_contextify.h b/src/node_contextify.h new file mode 100644 index 0000000000..86ecdf55a7 --- /dev/null +++ b/src/node_contextify.h @@ -0,0 +1,21 @@ +#ifndef SRC_NODE_CONTEXTIFY_H_ +#define SRC_NODE_CONTEXTIFY_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "v8.h" + +namespace node { + +class Environment; + +v8::MaybeLocal ContextFromContextifiedSandbox( + Environment* env, + v8::Local sandbox); + +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + + +#endif // SRC_NODE_CONTEXTIFY_H_ diff --git a/src/node_extras.cc b/src/node_extras.cc new file mode 100644 index 0000000000..3a70693b44 --- /dev/null +++ b/src/node_extras.cc @@ -0,0 +1,30 @@ +#include "node.h" +#include "util.h" +#include "util-inl.h" +#include "v8.h" + +namespace node { + +using v8::Context; +using v8::Isolate; +using v8::Local; +using v8::Object; +using v8::Value; + +namespace v8_extras { + +static void Init(Local target, + Local unused, + Local context, + void* priv) { + Isolate* isolate = context->GetIsolate(); + Local binding = context->GetExtrasBindingObject(); + CHECK(target->Set(context, FIXED_ONE_BYTE_STRING(isolate, "binding"), + binding).FromMaybe(false)); +} + +} // namespace v8_extras + +} // namespace node + +NODE_MODULE_CONTEXT_AWARE_BUILTIN(extras, node::v8_extras::Init) diff --git a/src/node_messaging.cc b/src/node_messaging.cc index b6b4173c5f..d361872269 100644 --- a/src/node_messaging.cc +++ b/src/node_messaging.cc @@ -1,4 +1,5 @@ #include "node_messaging.h" +#include "node_contextify.h" #include "node_internals.h" #include "node_buffer.h" #include "util.h" @@ -25,8 +26,10 @@ using v8::Maybe; using v8::MaybeLocal; using v8::Nothing; using v8::Object; +using v8::Private; using v8::SharedArrayBuffer; using v8::String; +using v8::Undefined; using v8::Value; using v8::ValueDeserializer; using v8::ValueSerializer; @@ -604,6 +607,37 @@ void MessagePort::StopBinding(const FunctionCallbackInfo& args) { port->Stop(); } +void MessagePort::MoveToContext(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + MessagePort* port; + if (!args[0]->IsObject() || + (port = Unwrap(args[0].As())) == nullptr) { + env->ThrowTypeError("First argument needs to be a MessagePort instance"); + } + if (!port->data_) { + env->ThrowError("Cannot transfer a closed MessagePort"); + return; + } + if (port->is_privileged_ || port->fm_listener_) { + env->ThrowError("Cannot transfer MessagePort with special semantics"); + return; + } + Local context_arg = args[1]; + Local context; + if (!context_arg->IsObject() || + !ContextFromContextifiedSandbox(env, context_arg.As()) + .ToLocal(&context)) { + env->ThrowError("Invalid context argument"); + return; + } + Context::Scope context_scope(context); + MessagePort* target = + MessagePort::New(env, context, nullptr, port->Detach()); + if (target) { + args.GetReturnValue().Set(target->object()); + } +} + size_t MessagePort::self_size() const { Mutex::ScopedLock lock(data_->mutex_); size_t sz = sizeof(*this) + sizeof(*data_); @@ -625,8 +659,41 @@ MaybeLocal GetMessagePortConstructor( // 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); + + if (!templ.IsEmpty()) { + auto maybe_ctor = templ->GetFunction(context); + Local ctor; + if (!maybe_ctor.ToLocal(&ctor)) return maybe_ctor; + + // Set up EventEmitter inheritance and some default listners. + Local initialized = env->messageport_initialized_private_symbol(); + if (!ctor->HasPrivate(context, initialized).FromJust()) { + Local extras = context->GetExtrasBindingObject(); + Local make_message_port; + + if (!extras->Get(context, + FIXED_ONE_BYTE_STRING(env->isolate(), "makeMessagePort")) + .ToLocal(&make_message_port)) { + return MaybeLocal(); + } + if (!make_message_port->IsFunction()) { + env->ThrowError("'makeMessagePort' is not a function"); + return MaybeLocal(); + } + Local args[] = { ctor }; + if (make_message_port.As()->Call(context, + Undefined(env->isolate()), + arraysize(args), + args).IsEmpty()) { + return MaybeLocal(); + } + + ctor->SetPrivate(context, initialized, Undefined(env->isolate())) + .FromJust(); + } + + return maybe_ctor; + } { Local m = env->NewFunctionTemplate(MessagePort::New); @@ -687,6 +754,8 @@ static void InitMessaging(Local target, templ->GetFunction(context).ToLocalChecked()).FromJust(); } + env->SetMethod(target, "moveMessagePortToContext", + MessagePort::MoveToContext); target->Set(context, env->message_port_constructor_string(), GetMessagePortConstructor(env, context).ToLocalChecked()) diff --git a/src/node_messaging.h b/src/node_messaging.h index fdff9eee98..69c8f9ef05 100644 --- a/src/node_messaging.h +++ b/src/node_messaging.h @@ -157,11 +157,16 @@ class MessagePort : public HandleWrap { // Stop processing messages on this port as a receiving end. void Stop(); + /* constructor */ static void New(const v8::FunctionCallbackInfo& args); + /* prototype methods */ static void PostMessage(const v8::FunctionCallbackInfo& args); static void StartBinding(const v8::FunctionCallbackInfo& args); static void StopBinding(const v8::FunctionCallbackInfo& args); + /* static */ + static void MoveToContext(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); diff --git a/test/addons/dlopen-ping-pong/test.js b/test/addons/dlopen-ping-pong/test.js index c533593496..6d4a648951 100644 --- a/test/addons/dlopen-ping-pong/test.js +++ b/test/addons/dlopen-ping-pong/test.js @@ -14,7 +14,4 @@ process.dlopen(module, bindingPath, module.exports.load(`${path.dirname(bindingPath)}/ping.so`); assert.strictEqual(module.exports.ping(), 'pong'); -// Check that after the addon is loaded with -// process.dlopen() a require() call fails. -const re = /^Error: Module did not self-register\.$/; -assert.throws(() => require(`./build/${common.buildType}/binding`), re); +assert.doesNotThrow(() => require(`./build/${common.buildType}/binding`)); diff --git a/test/addons/hello-world/test-worker.js b/test/addons/hello-world/test-worker.js new file mode 100644 index 0000000000..6794fda7b6 --- /dev/null +++ b/test/addons/hello-world/test-worker.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const path = require('path'); +const { Worker } = require('worker'); +const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`); + +const w = new Worker(`require(${JSON.stringify(binding)});`, { eval: true }); +w.on('error', common.mustCall((err) => { + assert(String(err).includes( + 'Error: Native modules need to explicitly indicate ' + + 'multi-isolate/multi-thread support by using ' + + '`NODE_MODULE_WORKER_ENABLED` or `NAPI_MODULE_WORKER_ENABLED` to ' + + 'register themselves.')); +})); diff --git a/test/addons/worker-addon/binding.cc b/test/addons/worker-addon/binding.cc new file mode 100644 index 0000000000..3d3c367d7d --- /dev/null +++ b/test/addons/worker-addon/binding.cc @@ -0,0 +1,43 @@ +#include +#include +#include +#include +#include + +using v8::Context; +using v8::HandleScope; +using v8::Isolate; +using v8::Local; +using v8::Object; +using v8::Value; + +size_t count = 0; + +struct statically_allocated { + statically_allocated() { + assert(count == 0); + printf("ctor "); + } + ~statically_allocated() { + assert(count == 0); + printf("dtor"); + } +} var; + +void Dummy(void*) { + assert(0); +} + +void Cleanup(void* str) { + printf("%s ", static_cast(str)); +} + +void Init(Local exports, Local module, Local context) { + node::AddEnvironmentCleanupHook( + context->GetIsolate(), Cleanup, + const_cast(static_cast("cleanup"))); + node::AddEnvironmentCleanupHook(context->GetIsolate(), Dummy, nullptr); + node::RemoveEnvironmentCleanupHook(context->GetIsolate(), Dummy, nullptr); +} + +NODE_MODULE_WORKER_ENABLED(binding, Init) diff --git a/test/addons/worker-addon/binding.gyp b/test/addons/worker-addon/binding.gyp new file mode 100644 index 0000000000..7ede63d94a --- /dev/null +++ b/test/addons/worker-addon/binding.gyp @@ -0,0 +1,9 @@ +{ + 'targets': [ + { + 'target_name': 'binding', + 'defines': [ 'V8_DEPRECATION_WARNINGS=1' ], + 'sources': [ 'binding.cc' ] + } + ] +} diff --git a/test/addons/worker-addon/test.js b/test/addons/worker-addon/test.js new file mode 100644 index 0000000000..b3eb35824a --- /dev/null +++ b/test/addons/worker-addon/test.js @@ -0,0 +1,16 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const child_process = require('child_process'); +const path = require('path'); +const { Worker } = require('worker'); +const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`); + +if (process.argv[2] === 'child') { + new Worker(`require(${JSON.stringify(binding)});`, { eval: true }); +} else { + const proc = child_process.spawnSync(process.execPath, [__filename, 'child']); + assert.strictEqual(proc.stderr.toString(), ''); + assert.strictEqual(proc.stdout.toString(), 'ctor cleanup dtor'); + assert.strictEqual(proc.status, 0); +} diff --git a/test/common/index.js b/test/common/index.js index 05f21e25ca..a8373f3c52 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -159,9 +159,13 @@ exports.refreshTmpDir = function() { fs.mkdirSync(exports.tmpDir); }; -if (process.env.TEST_THREAD_ID) { - exports.PORT += process.env.TEST_THREAD_ID * 100; - exports.tmpDirName += `.${process.env.TEST_THREAD_ID}`; +const { workerData } = require('worker'); +if ((workerData && workerData.testThreadId) || process.env.TEST_THREAD_ID) { + const id = +((workerData && workerData.testThreadId) || + process.env.TEST_THREAD_ID); + + exports.PORT += id * 100; + exports.tmpDirName += `.${id}`; } exports.tmpDir = path.join(testRoot, exports.tmpDirName); diff --git a/test/fixtures/worker-script.mjs b/test/fixtures/worker-script.mjs new file mode 100644 index 0000000000..21d792bab3 --- /dev/null +++ b/test/fixtures/worker-script.mjs @@ -0,0 +1,3 @@ +import worker from 'worker'; + +worker.postMessage('Hello, world!'); diff --git a/test/message/stdin_messages.out b/test/message/stdin_messages.out index ad1688f15d..5f2c20c581 100644 --- a/test/message/stdin_messages.out +++ b/test/message/stdin_messages.out @@ -9,10 +9,9 @@ SyntaxError: Strict mode code may not include a with statement at Module._compile (module.js:*:*) at evalScript (bootstrap_node.js:*:*) at Socket. (bootstrap_node.js:*:*) - at emitNone (events.js:*:*) - at Socket.emit (events.js:*:*) at endReadableNT (_stream_readable.js:*:*) at _combinedTickCallback (internal/process/next_tick.js:*:*) + at process._tickCallback (internal/process/next_tick.js:*:*) 42 42 [stdin]:1 @@ -27,9 +26,9 @@ Error: hello at Module._compile (module.js:*:*) at evalScript (bootstrap_node.js:*:*) at Socket. (bootstrap_node.js:*:*) - at emitNone (events.js:*:*) - at Socket.emit (events.js:*:*) at endReadableNT (_stream_readable.js:*:*) + at _combinedTickCallback (internal/process/next_tick.js:*:*) + at process._tickCallback (internal/process/next_tick.js:*:*) [stdin]:1 throw new Error("hello") ^ @@ -42,9 +41,9 @@ Error: hello at Module._compile (module.js:*:*) at evalScript (bootstrap_node.js:*:*) at Socket. (bootstrap_node.js:*:*) - at emitNone (events.js:*:*) - at Socket.emit (events.js:*:*) at endReadableNT (_stream_readable.js:*:*) + at _combinedTickCallback (internal/process/next_tick.js:*:*) + at process._tickCallback (internal/process/next_tick.js:*:*) 100 [stdin]:1 var x = 100; y = x; @@ -58,9 +57,9 @@ ReferenceError: y is not defined at Module._compile (module.js:*:*) at evalScript (bootstrap_node.js:*:*) at Socket. (bootstrap_node.js:*:*) - at emitNone (events.js:*:*) - at Socket.emit (events.js:*:*) at endReadableNT (_stream_readable.js:*:*) + at _combinedTickCallback (internal/process/next_tick.js:*:*) + at process._tickCallback (internal/process/next_tick.js:*:*) [stdin]:1 var ______________________________________________; throw 10 diff --git a/test/parallel/test-error-serdes.js b/test/parallel/test-error-serdes.js new file mode 100644 index 0000000000..ed49496b2b --- /dev/null +++ b/test/parallel/test-error-serdes.js @@ -0,0 +1,46 @@ +// Flags: --expose-internals +'use strict'; +require('../common'); +const assert = require('assert'); +const errors = require('internal/errors'); +const { serializeError, deserializeError } = require('internal/error-serdes'); + +function cycle(err) { + return deserializeError(serializeError(err)); +} + +assert.strictEqual(cycle(0), 0); +assert.strictEqual(cycle(-1), -1); +assert.strictEqual(cycle(1.4), 1.4); +assert.strictEqual(cycle(null), null); +assert.strictEqual(cycle(undefined), undefined); +assert.strictEqual(cycle('foo'), 'foo'); + +{ + const err = cycle(new Error('foo')); + assert(err instanceof Error); + assert.strictEqual(err.name, 'Error'); + assert.strictEqual(err.message, 'foo'); + assert(/^Error: foo\n/.test(err.stack)); +} + +assert.strictEqual(cycle(new RangeError('foo')).name, 'RangeError'); +assert.strictEqual(cycle(new TypeError('foo')).name, 'TypeError'); +assert.strictEqual(cycle(new ReferenceError('foo')).name, 'ReferenceError'); +assert.strictEqual(cycle(new URIError('foo')).name, 'URIError'); +assert.strictEqual(cycle(new EvalError('foo')).name, 'EvalError'); +assert.strictEqual(cycle(new SyntaxError('foo')).name, 'SyntaxError'); + +class SubError extends Error {} + +assert.strictEqual(cycle(new SubError('foo')).name, 'Error'); + +assert.deepStrictEqual(cycle({ message: 'foo' }), { message: 'foo' }); +assert.strictEqual(cycle(Function), '[Function: Function]'); + +{ + const err = new errors.TypeError('ERR_INVALID_ARG_TYPE', 'object', 'object'); + assert(/^TypeError \[ERR_INVALID_ARG_TYPE\]:/.test(err)); + assert.strictEqual(err.name, 'TypeError [ERR_INVALID_ARG_TYPE]'); + assert.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE'); +} diff --git a/test/parallel/test-message-channel-move.js b/test/parallel/test-message-channel-move.js new file mode 100644 index 0000000000..e0a8deea21 --- /dev/null +++ b/test/parallel/test-message-channel-move.js @@ -0,0 +1,45 @@ +/* eslint-disable prefer-assert-methods */ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const vm = require('vm'); +const { MessageChannel } = require('worker'); + +{ + const context = vm.createContext(); + const channel = new MessageChannel(); + context.port = vm.moveMessagePortToContext(channel.port1, context); + context.common = common; + context.global = context; + const port = channel.port2; + vm.runInContext('(' + function() { + function assert(condition) { if (!condition) throw new Error(); } + + { + assert(port instanceof Object); + assert(port.onmessage instanceof Function); + assert(port.postMessage instanceof Function); + port.on('message', common.mustCall((msg) => { + assert(msg instanceof Object); + port.postMessage(msg); + })); + } + + { + let threw = false; + try { + port.postMessage(global); + } catch (e) { + assert(e instanceof Object); + assert(e instanceof Error); + threw = true; + } + assert(threw); + } + } + ')()', context); + port.on('message', common.mustCall((msg) => { + assert(msg instanceof Object); + port.close(); + })); + port.postMessage({}); +} diff --git a/test/parallel/test-message-channel-sharedarraybuffer.js b/test/parallel/test-message-channel-sharedarraybuffer.js new file mode 100644 index 0000000000..16ca396282 --- /dev/null +++ b/test/parallel/test-message-channel-sharedarraybuffer.js @@ -0,0 +1,28 @@ +/*global SharedArrayBuffer*/ +'use strict'; +// Flags: --harmony-sharedarraybuffer --expose-gc + +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(` + require('worker').on('workerMessage', ({ sharedArrayBuffer }) => { + const local = Buffer.from(sharedArrayBuffer); + local.write('world!', 6); + require('worker').postMessage(); + }); + `, { 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 new file mode 100644 index 0000000000..9e425a66f4 --- /dev/null +++ b/test/parallel/test-message-channel.js @@ -0,0 +1,45 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker, MessagePort, MessageChannel } = require('worker'); + +{ + const channel = new MessageChannel(); + + const w = new Worker(` + const { MessagePort } = require('worker'); + const assert = require('assert'); + require('worker').on('workerMessage', ({ 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(); + })); +} + +{ + 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-worker-cleanup-handles.js b/test/parallel/test-worker-cleanup-handles.js new file mode 100644 index 0000000000..92f2c90fcb --- /dev/null +++ b/test/parallel/test-worker-cleanup-handles.js @@ -0,0 +1,24 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker, isMainThread, postMessage } = 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) => { + assert.throws(() => fs.fstatSync(fd), + common.expectsError({ code: 'EBADF' })); + })); +} else { + const server = new Server(); + server.listen(0); + 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 0000000000..e4bbc95402 --- /dev/null +++ b/test/parallel/test-worker-dns-terminate.js @@ -0,0 +1,13 @@ +'use strict'; +const common = require('../common'); +const { Worker } = require('worker'); + +const w = new Worker(` +const dns = require('dns'); +dns.lookup('nonexistent.org', () => {}); +require('worker').postMessage('0'); +`, { eval: true }); + +w.on('message', common.mustCall(() => { + 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 0000000000..be82670401 --- /dev/null +++ b/test/parallel/test-worker-esmodule.js @@ -0,0 +1,11 @@ +'use strict'; +// Flags: --experimental-modules +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-termination-2.js b/test/parallel/test-worker-termination-2.js new file mode 100644 index 0000000000..11330a550f --- /dev/null +++ b/test/parallel/test-worker-termination-2.js @@ -0,0 +1,13 @@ +'use strict'; + +const common = require('../common'); +const { Worker, isMainThread, postMessage } = require('worker'); + +if (isMainThread) { + const aWorker = new Worker(__filename); + aWorker.terminate(common.mustCall()); + aWorker.on('message', common.mustNotCall()); +} else { + while (true) + postMessage({ hello: 'world' }); +} diff --git a/test/parallel/test-worker-termination-3.js b/test/parallel/test-worker-termination-3.js new file mode 100644 index 0000000000..c6f0311a47 --- /dev/null +++ b/test/parallel/test-worker-termination-3.js @@ -0,0 +1,22 @@ +'use strict'; + +const common = require('../common'); +const { Worker, isMainThread, postMessage } = require('worker'); + +if (isMainThread) { + const aWorker = new Worker(__filename); + aWorker.on('message', common.mustCallAtLeast(function() { + aWorker.postMessage(); + aWorker.postMessage(); + aWorker.postMessage(); + aWorker.postMessage(); + aWorker.terminate(common.mustCall()); + })); +} else { + require('worker').on('workerMessage', function() { + while (true) + postMessage({ hello: 'world' }); + }); + + postMessage(); +} diff --git a/test/parallel/test-worker-termination-exit-2.js b/test/parallel/test-worker-termination-exit-2.js new file mode 100644 index 0000000000..da191e07f1 --- /dev/null +++ b/test/parallel/test-worker-termination-exit-2.js @@ -0,0 +1,30 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Worker, isMainThread, postMessage } = require('worker'); + +if (isMainThread) { + const aWorker = new Worker(__filename, { keepAlive: false }); + aWorker.on('exit', common.mustCall((code) => { + assert.strictEqual(1337, code); + })); + aWorker.on('message', common.mustCall((data) => { + assert.strictEqual(data, 0); + })); +} else { + process.on('beforeExit', () => { + setInterval(function() { + postMessage({ hello: 'world' }); + }, 5000); + setImmediate(function f() { + postMessage({ hello: 'world' }); + setImmediate(f); + }); + process.exit(1337); + }); + let emits = 0; + process.on('exit', function() { + postMessage(emits++); + }); +} diff --git a/test/parallel/test-worker-termination-exit.js b/test/parallel/test-worker-termination-exit.js new file mode 100644 index 0000000000..f09618f2e5 --- /dev/null +++ b/test/parallel/test-worker-termination-exit.js @@ -0,0 +1,26 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Worker, isMainThread, postMessage } = require('worker'); + +if (isMainThread) { + const aWorker = new Worker(__filename); + aWorker.on('exit', common.mustCall((code) => { + assert.strictEqual(1337, code); + })); + aWorker.on('message', common.mustNotCall()); +} else { + setInterval(function() { + postMessage({ hello: 'world' }); + }, 5000); + setImmediate(function f() { + postMessage({ hello: 'world' }); + setImmediate(f); + }); + (function() { + [1337, 2, 3].map(function(value) { + process.exit(value); + }); + })(); +} diff --git a/test/parallel/test-worker-termination-grand-parent.js b/test/parallel/test-worker-termination-grand-parent.js new file mode 100644 index 0000000000..fac40f25ea --- /dev/null +++ b/test/parallel/test-worker-termination-grand-parent.js @@ -0,0 +1,54 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Worker, isMainThread, postMessage } = require('worker'); +const ids = []; + +if (isMainThread) { + const aWorker = new Worker(__filename); + aWorker.postMessage({ + init: true, + subWorker: false + }); + aWorker.on('message', common.mustCall((data) => { + ids.push(data.id); + if (ids.length === 4) { + // Terminating the main worker should terminate its 4 sub-workers + aWorker.terminate(); + } + }, 4)); + process.on('beforeExit', function() { + assert.deepStrictEqual([0, 1, 2, 3].sort(), ids.sort()); + }); +} else { + require('worker').on('workerMessage', function(data) { + if (data.init) { + if (data.subWorker) { + subWorker(data.id); + } else { + mainWorker(); + } + } + }); +} + +function mainWorker() { + let l = 4; + while (l--) { + const worker = new Worker(__filename); + worker.postMessage({ + init: true, + subWorker: true, + id: l + }); + worker.on('message', function(payload) { + postMessage(payload); + }); + } +} + +function subWorker(id) { + postMessage({ id: id }); + while (true); +} diff --git a/test/parallel/test-worker-termination-owner.js b/test/parallel/test-worker-termination-owner.js new file mode 100644 index 0000000000..ed0d87bad4 --- /dev/null +++ b/test/parallel/test-worker-termination-owner.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +const { Worker, isMainThread, postMessage } = require('worker'); + +// Test that termination of a worker that is in the middle of processing +// messages from its sub-worker works. + +if (isMainThread) { + const worker = new Worker(__filename); + worker.postMessage({ main: true }); + worker.on('message', common.mustCall(() => { + worker.terminate(common.mustCall()); + })); +} else { + require('worker').on('workerMessage', function(data) { + if (data.main) { + let messagesReceived = 0; + const subworker = new Worker(__filename); + subworker.postMessage({ main: false }); + subworker.on('message', function() { + messagesReceived++; + + if (messagesReceived === 512) + postMessage(); + }); + } else { + while (true) postMessage(); + } + }); +} diff --git a/test/parallel/test-worker-termination-races.js b/test/parallel/test-worker-termination-races.js new file mode 100644 index 0000000000..953f6bf660 --- /dev/null +++ b/test/parallel/test-worker-termination-races.js @@ -0,0 +1,114 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { Worker, isMainThread, postMessage } = require('worker'); +const MESSAGES_PER_GRAND_CHILD_WORKER = 20; + +if (isMainThread) { + let l = 4; + const workers = {}; + while (l--) { + const worker = new Worker(__filename); + worker.on('message', common.mustCallAtLeast((data) => { + if (workers[data.id]) return; + worker.terminate(); + workers[data.id] = true; + })); + worker.postMessage({ id: l }); + } +} else { + require('worker').on('workerMessage', function(data) { + if (data.id <= 3) { + runImmediateChildWorker(data); + } else { + runGrandChildWorker(data); + } + }); +} + +function runImmediateChildWorker(mainData) { + const messages = {}; + let l = 4; + while (l--) { + const subWorkerId = mainData.id * 4 + 4 + l; + messages[subWorkerId] = 0; + const worker = new Worker(__filename); + worker.on('message', function(data) { + const count = ++messages[data.id]; + if (count === MESSAGES_PER_GRAND_CHILD_WORKER) { + process.postMessage({ id: mainData.id }); + } + }); + postMessage({ id: subWorkerId }); + } +} + +function runGrandChildWorker(data) { + let l = MESSAGES_PER_GRAND_CHILD_WORKER; + process.stdout; + process.stderr; + process.stdin; + try { require('assert'); } catch (e) {} + try { require('buffer'); } catch (e) {} + try { require('child_process'); } catch (e) {} + try { require('cluster'); } catch (e) {} + try { require('console'); } catch (e) {} + try { require('constants'); } catch (e) {} + try { require('crypto'); } catch (e) {} + try { require('_debug_agent'); } catch (e) {} + try { require('_debugger'); } catch (e) {} + try { require('dgram'); } catch (e) {} + try { require('dns'); } catch (e) {} + try { require('domain'); } catch (e) {} + try { require('events'); } catch (e) {} + try { require('freelist'); } catch (e) {} + try { require('fs'); } catch (e) {} + try { require('_http_agent'); } catch (e) {} + try { require('_http_client'); } catch (e) {} + try { require('_http_common'); } catch (e) {} + try { require('_http_incoming'); } catch (e) {} + try { require('http'); } catch (e) {} + try { require('_http_outgoing'); } catch (e) {} + try { require('_http_server'); } catch (e) {} + try { require('https'); } catch (e) {} + try { require('_linklist'); } catch (e) {} + try { require('module'); } catch (e) {} + try { require('net'); } catch (e) {} + try { require('os'); } catch (e) {} + try { require('path'); } catch (e) {} + try { require('process'); } catch (e) {} + try { require('punycode'); } catch (e) {} + try { require('querystring'); } catch (e) {} + try { require('readline'); } catch (e) {} + try { require('repl'); } catch (e) {} + try { require('smalloc'); } catch (e) {} + try { require('_stream_duplex'); } catch (e) {} + try { require('stream'); } catch (e) {} + try { require('_stream_passthrough'); } catch (e) {} + try { require('_stream_readable'); } catch (e) {} + try { require('_stream_transform'); } catch (e) {} + try { require('_stream_wrap'); } catch (e) {} + try { require('_stream_writable'); } catch (e) {} + try { require('string_decoder'); } catch (e) {} + try { require('timers'); } catch (e) {} + try { require('_tls_common'); } catch (e) {} + try { require('tls'); } catch (e) {} + try { require('_tls_legacy'); } catch (e) {} + try { require('_tls_wrap'); } catch (e) {} + try { require('tty'); } catch (e) {} + try { require('url'); } catch (e) {} + try { require('util'); } catch (e) {} + try { require('v8'); } catch (e) {} + try { require('vm'); } catch (e) {} + try { require('worker'); } catch (e) {} + try { require('zlib'); } catch (e) {} + while (l--) { + postMessage({ + id: data.id + }); + } +} diff --git a/test/parallel/test-worker-termination.js b/test/parallel/test-worker-termination.js new file mode 100644 index 0000000000..ba42843ccf --- /dev/null +++ b/test/parallel/test-worker-termination.js @@ -0,0 +1,18 @@ +'use strict'; + +const common = require('../common'); +const { Worker, isMainThread, postMessage } = require('worker'); + +if (isMainThread) { + const aWorker = new Worker(__filename); + aWorker.terminate(common.mustCall()); + aWorker.on('message', common.mustNotCall()); +} else { + setInterval(function() { + postMessage({ hello: 'world' }); + }, 5000); + setImmediate(function f() { + postMessage({ hello: 'world' }); + setImmediate(f); + }); +} 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 0000000000..88b1f41c86 --- /dev/null +++ b/test/parallel/test-worker-uncaught-exception-async.js @@ -0,0 +1,16 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker, isMainThread } = require('worker'); + +if (isMainThread) { + const w = new Worker(__filename); + w.on('message', common.mustNotCall()); + w.on('error', common.mustCall((err) => { + assert(/^Error: 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 0000000000..b9a303e6ff --- /dev/null +++ b/test/parallel/test-worker-uncaught-exception.js @@ -0,0 +1,14 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker, isMainThread } = require('worker'); + +if (isMainThread) { + const w = new Worker(__filename); + w.on('message', common.mustNotCall()); + w.on('error', common.mustCall((err) => { + assert(/^Error: foo$/.test(err)); + })); +} else { + throw new Error('foo'); +} diff --git a/test/parallel/test-worker-unref-2.js b/test/parallel/test-worker-unref-2.js new file mode 100644 index 0000000000..3505aa637a --- /dev/null +++ b/test/parallel/test-worker-unref-2.js @@ -0,0 +1,35 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { Worker, isMainThread, postMessage } = require('worker'); +let checks = 0; + +if (isMainThread) { + const timer = setInterval(function() {}, 1000); + const aWorker = new Worker(__filename); + aWorker.on('exit', function() { + checks++; + }); + aWorker.on('message', function() { + checks++; + setTimeout(function() { + checks++; + aWorker.terminate(function() { + checks++; + clearInterval(timer); + }); + }, 5); + }); + process.on('beforeExit', function() { + assert.strictEqual(4, checks); + }); + aWorker.unref(); + aWorker.postMessage(); +} else { + require('worker').on('workerMessage', function() { + setTimeout(function() { + postMessage(); + }, 1); + }); +} diff --git a/test/parallel/test-worker-unref.js b/test/parallel/test-worker-unref.js new file mode 100644 index 0000000000..5030ca1f76 --- /dev/null +++ b/test/parallel/test-worker-unref.js @@ -0,0 +1,28 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { Worker, isMainThread, postMessage } = require('worker'); +let checks = 0; + +if (isMainThread) { + const aWorker = new Worker(__filename); + aWorker.on('exit', function() { + checks++; + }); + aWorker.on('message', function() { + checks++; + setTimeout(function() { + checks++; + aWorker.terminate(); + }, 5); + }); + process.on('beforeExit', function() { + assert.strictEqual(0, checks); + }); + aWorker.unref(); +} else { + setInterval(function() { + postMessage(); + }, 5); +} diff --git a/test/parallel/test-worker-unsupported-path.js b/test/parallel/test-worker-unsupported-path.js new file mode 100644 index 0000000000..04110dfefd --- /dev/null +++ b/test/parallel/test-worker-unsupported-path.js @@ -0,0 +1,26 @@ +'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); +} diff --git a/test/parallel/test-worker-unsupported-things.js b/test/parallel/test-worker-unsupported-things.js new file mode 100644 index 0000000000..53eae47869 --- /dev/null +++ b/test/parallel/test-worker-unsupported-things.js @@ -0,0 +1,60 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker, isMainThread, postMessage } = require('worker'); + +if (isMainThread) { + const w = new Worker(__filename); + w.on('message', common.mustCall((message) => { + assert.strictEqual(message, true); + })); +} else { + assert.throws(() => { + require('domain'); + }, common.expectsError({ + code: 'ERR_WORKER_DOMAIN', + type: TypeError + })); + + assert.doesNotThrow(() => { + process.umask(); + }); + assert.throws(() => { + process.umask(0); + }, TypeError); + + { + const before = process.title; + // This should throw... + process.title += ' in worker'; + assert.strictEqual(process.title, before); + } + + { + const before = process.debugPort; + // This should throw... + process.debugPort++; + assert.strictEqual(process.debugPort, before); + } + + assert.strictEqual(process.stdin, null); + assert.strictEqual(process.stdout, null); + assert.strictEqual(process.stderr, null); + + 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); + + postMessage(true); +} diff --git a/test/parallel/test-worker.js b/test/parallel/test-worker.js new file mode 100644 index 0000000000..06b90a2605 --- /dev/null +++ b/test/parallel/test-worker.js @@ -0,0 +1,17 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker, isMainThread, postMessage } = require('worker'); + +if (isMainThread) { + const w = new Worker(__filename); + w.on('message', common.mustCall((message) => { + assert.strictEqual(message, 'Hello, world!'); + })); +} else { + setImmediate(() => { + process.nextTick(() => { + postMessage('Hello, world!'); + }); + }); +} diff --git a/test/workers/index.js b/test/workers/index.js new file mode 100644 index 0000000000..d71fb722be --- /dev/null +++ b/test/workers/index.js @@ -0,0 +1,38 @@ +'use strict'; +const common = require('../common'); +const path = require('path'); +const { Worker } = require('worker'); + +common.crashOnUnhandledRejection(); + +exports.runTestInsideWorker = function(testFilePath, workerData) { + return new Promise(function(resolve, reject) { + const worker = new Worker(path.resolve(__dirname, '../../', testFilePath), { + keepAlive: false, + workerData + }); + worker.on('exit', function(exitCode) { + if (exitCode === 0) + resolve(); + else + reject(new Error(`${testFilePath} exited with code ${exitCode}`)); + }); + + worker.on('error', function(e) { + reject(new Error(`Running ${testFilePath} inside worker failed:\n` + + e/*.stack*/)); + }); + }); +}; + +exports.runTestsInsideWorker = async function(tests) { + const parallelism = +process.env.JOBS || require('os').cpus().length; + const testsPerThread = Math.ceil(tests.length / parallelism); + await Promise.all([...Array(parallelism).keys()].map(async (i) => { + const shareOfTests = tests.slice(i * testsPerThread, + (i + 1) * testsPerThread); + for (const testFile of shareOfTests) { + await exports.runTestInsideWorker(testFile, { testThreadId: i }); + } + })); +}; diff --git a/test/workers/test-child-process.js b/test/workers/test-child-process.js new file mode 100644 index 0000000000..be424e7b27 --- /dev/null +++ b/test/workers/test-child-process.js @@ -0,0 +1,48 @@ +'use strict'; + +require('../common'); + +require('./index.js').runTestsInsideWorker([ + 'test/parallel/test-child-process-buffering.js', + 'test/parallel/test-child-process-cwd.js', + // TODO(addaleax): currently leaves a zombie + // 'test/parallel/test-child-process-detached.js', + 'test/parallel/test-child-process-disconnect.js', + 'test/parallel/test-child-process-env.js', + 'test/parallel/test-child-process-exec-cwd.js', + 'test/parallel/test-child-process-exec-env.js', + 'test/parallel/test-child-process-exec-error.js', + 'test/parallel/test-child-process-exit-code.js', + 'test/parallel/test-child-process-fork3.js', + // TODO(addaleax): doesn’t handle tmp dirs correctly for workers + // 'test/parallel/test-child-process-fork-and-spawn.js', + 'test/parallel/test-child-process-fork-close.js', + 'test/parallel/test-child-process-fork-dgram.js', + // TODO(addaleax): doesn’t handle tmp dirs correctly for workers + // 'test/parallel/test-child-process-fork-exec-argv.js', + // TODO(addaleax): doesn’t handle tmp dirs correctly for workers + // 'test/parallel/test-child-process-fork-exec-path.js', + 'test/parallel/test-child-process-fork.js', + 'test/parallel/test-child-process-fork-net2.js', + 'test/parallel/test-child-process-fork-net.js', + 'test/parallel/test-child-process-fork-ref2.js', + 'test/parallel/test-child-process-fork-ref.js', + 'test/parallel/test-child-process-internal.js', + 'test/parallel/test-child-process-ipc.js', + 'test/parallel/test-child-process-kill.js', + 'test/parallel/test-child-process-send-utf8.js', + 'test/parallel/test-child-process-set-blocking.js', + 'test/parallel/test-child-process-spawn-error.js', + 'test/parallel/test-child-process-spawnsync-env.js', + 'test/parallel/test-child-process-spawnsync-input.js', + 'test/parallel/test-child-process-spawnsync.js', + 'test/parallel/test-child-process-spawnsync-timeout.js', + 'test/parallel/test-child-process-spawn-typeerror.js', + 'test/parallel/test-child-process-stdin-ipc.js', + 'test/parallel/test-child-process-stdin.js', + 'test/parallel/test-child-process-stdio-big-write-end.js', + // TODO(addaleax): currently fails + //'test/parallel/test-child-process-stdio.js', + 'test/parallel/test-child-process-stdout-flush-exit.js', + 'test/parallel/test-child-process-stdout-flush.js' +]); diff --git a/test/workers/test-crypto.js b/test/workers/test-crypto.js new file mode 100644 index 0000000000..5a48fd6204 --- /dev/null +++ b/test/workers/test-crypto.js @@ -0,0 +1,26 @@ +'use strict'; + +require('../common'); + +// When using OpenSSL this test should put the locking_callback to good use +require('./index.js').runTestsInsideWorker([ + 'test/parallel/test-crypto-authenticated.js', + 'test/parallel/test-crypto-binary-default.js', + 'test/parallel/test-crypto-certificate.js', + 'test/parallel/test-crypto-cipher-decipher.js', + 'test/parallel/test-crypto-dh.js', + 'test/parallel/test-crypto-dh-odd-key.js', + 'test/parallel/test-crypto-ecb.js', + 'test/parallel/test-crypto-from-binary.js', + 'test/parallel/test-crypto-hash.js', + 'test/parallel/test-crypto-hash-stream-pipe.js', + 'test/parallel/test-crypto-hmac.js', + 'test/parallel/test-crypto.js', + 'test/parallel/test-crypto-padding-aes256.js', + 'test/parallel/test-crypto-padding.js', + 'test/parallel/test-crypto-pbkdf2.js', + 'test/parallel/test-crypto-random.js', + 'test/parallel/test-crypto-rsa-dsa.js', + 'test/parallel/test-crypto-sign-verify.js', + 'test/parallel/test-crypto-stream.js' +]); diff --git a/test/workers/test-fs.js b/test/workers/test-fs.js new file mode 100644 index 0000000000..4164ab85ea --- /dev/null +++ b/test/workers/test-fs.js @@ -0,0 +1,59 @@ +'use strict'; + +require('../common'); + +require('./index.js').runTestsInsideWorker([ + 'test/parallel/test-fs-access.js', + 'test/parallel/test-fs-append-file.js', + 'test/parallel/test-fs-append-file-sync.js', + 'test/parallel/test-fs-chmod.js', + 'test/parallel/test-fs-empty-readStream.js', + 'test/parallel/test-fs-error-messages.js', + 'test/parallel/test-fs-exists.js', + 'test/parallel/test-fs-fsync.js', + 'test/parallel/test-fs-long-path.js', + 'test/parallel/test-fs-make-callback.js', + 'test/parallel/test-fs-mkdir.js', + 'test/parallel/test-fs-non-number-arguments-throw.js', + 'test/parallel/test-fs-null-bytes.js', + 'test/parallel/test-fs-open.js', + 'test/parallel/test-fs-readfile-empty.js', + 'test/parallel/test-fs-readfile-error.js', + 'test/parallel/test-fs-readfile-pipe.js', + 'test/parallel/test-fs-read-file-sync-hostname.js', + 'test/parallel/test-fs-read-file-sync.js', + 'test/parallel/test-fs-readfile-unlink.js', + 'test/parallel/test-fs-readfile-zero-byte-liar.js', + 'test/parallel/test-fs-read.js', + 'test/parallel/test-fs-read-stream-err.js', + 'test/parallel/test-fs-read-stream-fd.js', + 'test/parallel/test-fs-read-stream-fd-leak.js', + 'test/parallel/test-fs-read-stream-inherit.js', + 'test/parallel/test-fs-read-stream.js', + 'test/parallel/test-fs-read-stream-resume.js', + // Workers cannot chdir. + // 'test/parallel/test-fs-realpath.js', + 'test/parallel/test-fs-sir-writes-alot.js', + 'test/parallel/test-fs-stat.js', + 'test/parallel/test-fs-stream-double-close.js', + 'test/parallel/test-fs-symlink-dir-junction.js', + 'test/parallel/test-fs-symlink-dir-junction-relative.js', + 'test/parallel/test-fs-symlink.js', + 'test/parallel/test-fs-sync-fd-leak.js', + 'test/parallel/test-fs-truncate-fd.js', + 'test/parallel/test-fs-truncate-GH-6233.js', + 'test/parallel/test-fs-truncate.js', + 'test/parallel/test-fs-utimes.js', + 'test/parallel/test-fs-write-buffer.js', + 'test/parallel/test-fs-write-file-buffer.js', + 'test/parallel/test-fs-write-file.js', + // Workers cannot change umask. + // 'test/parallel/test-fs-write-file-sync.js', + 'test/parallel/test-fs-write.js', + 'test/parallel/test-fs-write-stream-change-open.js', + 'test/parallel/test-fs-write-stream-end.js', + 'test/parallel/test-fs-write-stream-err.js', + 'test/parallel/test-fs-write-stream.js', + 'test/parallel/test-fs-write-string-coerce.js', + 'test/parallel/test-fs-write-sync.js' +]); diff --git a/test/workers/test-process.js b/test/workers/test-process.js new file mode 100644 index 0000000000..f7df1abc9b --- /dev/null +++ b/test/workers/test-process.js @@ -0,0 +1,26 @@ +'use strict'; + +require('../common'); + +require('./index.js').runTestsInsideWorker([ + 'test/parallel/test-process-argv-0.js', + 'test/parallel/test-process-binding.js', + 'test/parallel/test-process-config.js', + // Only main thread can mutate process environment. + // 'test/parallel/test-process-env.js', + 'test/parallel/test-process-exec-argv.js', + 'test/parallel/test-process-exit-code.js', + 'test/parallel/test-process-exit-from-before-exit.js', + 'test/parallel/test-process-exit.js', + 'test/parallel/test-process-exit-recursive.js', + 'test/parallel/test-process-getgroups.js', + 'test/parallel/test-process-hrtime.js', + 'test/parallel/test-process-kill-null.js', + 'test/parallel/test-process-kill-pid.js', + // Workers have a different uncaught exception mechanism. + // 'test/parallel/test-process-next-tick.js', + 'test/parallel/test-process-raw-debug.js', + 'test/parallel/test-process-remove-all-signal-listeners.js', + 'test/parallel/test-process-versions.js', + 'test/parallel/test-process-wrap.js' +]); diff --git a/test/workers/test-stream.js b/test/workers/test-stream.js new file mode 100644 index 0000000000..d6769b33b6 --- /dev/null +++ b/test/workers/test-stream.js @@ -0,0 +1,48 @@ +'use strict'; + +require('../common'); + +require('./index.js').runTestsInsideWorker([ + 'test/parallel/test-stream2-base64-single-char-read-end.js', + 'test/parallel/test-stream2-compatibility.js', + 'test/parallel/test-stream2-finish-pipe.js', + 'test/parallel/test-stream2-large-read-stall.js', + 'test/parallel/test-stream2-objects.js', + 'test/parallel/test-stream2-pipe-error-handling.js', + 'test/parallel/test-stream2-pipe-error-once-listener.js', + 'test/parallel/test-stream2-push.js', + 'test/parallel/test-stream2-readable-empty-buffer-no-eof.js', + 'test/parallel/test-stream2-readable-legacy-drain.js', + 'test/parallel/test-stream2-readable-non-empty-end.js', + 'test/parallel/test-stream2-readable-wrap-empty.js', + 'test/parallel/test-stream2-readable-wrap.js', + 'test/parallel/test-stream2-read-sync-stack.js', + 'test/parallel/test-stream2-set-encoding.js', + 'test/parallel/test-stream2-transform.js', + 'test/parallel/test-stream2-unpipe-drain.js', + 'test/parallel/test-stream2-unpipe-leak.js', + 'test/parallel/test-stream3-pause-then-read.js', + 'test/parallel/test-stream-big-packet.js', + 'test/parallel/test-stream-big-push.js', + 'test/parallel/test-stream-duplex.js', + 'test/parallel/test-stream-end-paused.js', + 'test/parallel/test-stream-ispaused.js', + 'test/parallel/test-stream-pipe-after-end.js', + 'test/parallel/test-stream-pipe-cleanup.js', + 'test/parallel/test-stream-pipe-error-handling.js', + 'test/parallel/test-stream-pipe-event.js', + 'test/parallel/test-stream-push-order.js', + 'test/parallel/test-stream-push-strings.js', + 'test/parallel/test-stream-readable-constructor-set-methods.js', + 'test/parallel/test-stream-readable-event.js', + 'test/parallel/test-stream-readable-flow-recursion.js', + 'test/parallel/test-stream-transform-constructor-set-methods.js', + 'test/parallel/test-stream-transform-objectmode-falsey-value.js', + 'test/parallel/test-stream-transform-split-objectmode.js', + 'test/parallel/test-stream-unshift-empty-chunk.js', + 'test/parallel/test-stream-unshift-read-race.js', + 'test/parallel/test-stream-writable-change-default-encoding.js', + 'test/parallel/test-stream-writable-constructor-set-methods.js', + 'test/parallel/test-stream-writable-decoded-encoding.js', + 'test/parallel/test-stream-writev.js' +]); diff --git a/test/workers/test-vm.js b/test/workers/test-vm.js new file mode 100644 index 0000000000..d0da14e870 --- /dev/null +++ b/test/workers/test-vm.js @@ -0,0 +1,22 @@ +'use strict'; +require('../common'); + +require('./index.js').runTestsInsideWorker([ + 'test/parallel/test-vm-basic.js', + 'test/parallel/test-vm-context-async-script.js', + 'test/parallel/test-vm-context.js', + 'test/parallel/test-vm-context-property-forwarding.js', + 'test/parallel/test-vm-create-context-accessors.js', + 'test/parallel/test-vm-create-context-arg.js', + 'test/parallel/test-vm-create-context-circular-reference.js', + 'test/parallel/test-vm-cross-context.js', + 'test/parallel/test-vm-debug-context.js', + 'test/parallel/test-vm-function-declaration.js', + 'test/parallel/test-vm-global-define-property.js', + 'test/parallel/test-vm-global-identity.js', + 'test/parallel/test-vm-is-context.js', + 'test/parallel/test-vm-new-script-new-context.js', + 'test/parallel/test-vm-new-script-this-context.js', + 'test/parallel/test-vm-static-this.js', + 'test/parallel/test-vm-timeout.js' +]); diff --git a/test/workers/test-zlib.js b/test/workers/test-zlib.js new file mode 100644 index 0000000000..81b887c798 --- /dev/null +++ b/test/workers/test-zlib.js @@ -0,0 +1,20 @@ +'use strict'; + +require('../common'); + +require('./index.js').runTestsInsideWorker([ + 'test/parallel/test-zlib-close-after-write.js', + 'test/parallel/test-zlib-convenience-methods.js', + 'test/parallel/test-zlib-dictionary-fail.js', + 'test/parallel/test-zlib-dictionary.js', + 'test/parallel/test-zlib-flush.js', + 'test/parallel/test-zlib-from-gzip.js', + 'test/parallel/test-zlib-from-string.js', + 'test/parallel/test-zlib-invalid-input.js', + 'test/parallel/test-zlib.js', + 'test/parallel/test-zlib-params.js', + 'test/parallel/test-zlib-random-byte-pipes.js', + 'test/parallel/test-zlib-write-after-close.js', + 'test/parallel/test-zlib-write-after-flush.js', + 'test/parallel/test-zlib-zero-byte.js' +]); diff --git a/test/workers/testcfg.py b/test/workers/testcfg.py new file mode 100644 index 0000000000..9b0ca8a043 --- /dev/null +++ b/test/workers/testcfg.py @@ -0,0 +1,6 @@ +import sys, os +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +import testpy + +def GetConfiguration(context, root): + return testpy.SimpleTestConfiguration(context, root, 'workers') diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js index 43af1cb978..b46bc3d4ab 100644 --- a/tools/doc/type-parser.js +++ b/tools/doc/type-parser.js @@ -46,6 +46,8 @@ const typeMap = { 'Handle': 'net.html#net_server_listen_handle_backlog_callback', 'net.Socket': 'net.html#net_class_net_socket', + 'MessagePort': 'worker.html#worker_class_messageport', + 'Stream': 'stream.html#stream_stream', 'stream.Readable': 'stream.html#stream_class_stream_readable', 'stream.Writable': 'stream.html#stream_class_stream_writable',