diff --git a/benchmark/fixtures/echo.worker.js b/benchmark/fixtures/echo.worker.js new file mode 100644 index 00000000000000..167a28ad23e830 --- /dev/null +++ b/benchmark/fixtures/echo.worker.js @@ -0,0 +1,7 @@ +'use strict'; + +const { parentPort } = require('worker_threads'); + +parentPort.on('message', (msg) => { + parentPort.postMessage(msg); +}); diff --git a/benchmark/worker/echo.js b/benchmark/worker/echo.js new file mode 100644 index 00000000000000..32c4bddf7d5f77 --- /dev/null +++ b/benchmark/worker/echo.js @@ -0,0 +1,73 @@ +'use strict'; + +const common = require('../common.js'); +const path = require('path'); +const bench = common.createBenchmark(main, { + workers: [1], + payload: ['string', 'object'], + sendsPerBroadcast: [1, 10], + n: [1e5] +}, { flags: ['--experimental-worker'] }); + +const workerPath = path.resolve(__dirname, '..', 'fixtures', 'echo.worker.js'); + +function main(conf) { + const { Worker } = require('worker_threads'); + + 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/doc/api/_toc.md b/doc/api/_toc.md index 9b487b50a55031..e307d52ae8544e 100644 --- a/doc/api/_toc.md +++ b/doc/api/_toc.md @@ -53,6 +53,7 @@ * [Utilities](util.html) * [V8](v8.html) * [VM](vm.html) +* [Worker Threads](worker_threads.html) * [ZLIB](zlib.html)
diff --git a/doc/api/all.md b/doc/api/all.md index d013f07bd328fc..47216b695d3351 100644 --- a/doc/api/all.md +++ b/doc/api/all.md @@ -46,4 +46,5 @@ @include util @include v8 @include vm +@include worker_threads @include zlib diff --git a/doc/api/async_hooks.md b/doc/api/async_hooks.md index b97bc73304a4d7..f8449ec423740a 100644 --- a/doc/api/async_hooks.md +++ b/doc/api/async_hooks.md @@ -21,6 +21,9 @@ A resource can also be closed before the callback is called. `AsyncHook` does not explicitly distinguish between these different cases but will represent them as the abstract concept that is a resource. +If [`Worker`][]s are used, each thread has an independent `async_hooks` +interface, and each thread will use a new set of async IDs. + ## Public API ### Overview @@ -224,7 +227,7 @@ clearTimeout(setTimeout(() => {}, 10)); ``` Every new resource is assigned an ID that is unique within the scope of the -current process. +current Node.js instance. ###### `type` @@ -733,3 +736,4 @@ never be called. [Hook Callbacks]: #async_hooks_hook_callbacks [PromiseHooks]: https://docs.google.com/document/d/1rda3yKGHimKIhg5YeoAmCOtyURgsbTH_qaYR79FELlk [promise execution tracking]: #async_hooks_promise_execution_tracking +[`Worker`]: worker_threads.html#worker_threads_class_worker diff --git a/doc/api/errors.md b/doc/api/errors.md index fc1bcd3e6e994b..972d0971c4b059 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -629,6 +629,12 @@ An operation outside the bounds of a `Buffer` was attempted. An attempt has been made to create a `Buffer` larger than the maximum allowed size. + +### ERR_CANNOT_TRANSFER_OBJECT + +The value passed to `postMessage()` contained an object that is not supported +for transferring. + ### ERR_CANNOT_WATCH_SIGINT @@ -650,12 +656,23 @@ Used when a child process is being forked without specifying an IPC channel. Used when the main process is trying to read data from the child process's STDERR / STDOUT, and the data's length is longer than the `maxBuffer` option. + +### ERR_CLOSED_MESSAGE_PORT + +There was an attempt to use a `MessagePort` instance in a closed +state, usually after `.close()` has been called. + ### ERR_CONSOLE_WRITABLE_STREAM `Console` was instantiated without `stdout` stream, or `Console` has a non-writable `stdout` or `stderr` stream. + +### ERR_CONSTRUCT_CALL_REQUIRED + +A constructor for a class was called without `new`. + ### ERR_CPU_USAGE @@ -1203,6 +1220,11 @@ urlSearchParams.has.call(buf, 'foo'); // Throws a TypeError with code 'ERR_INVALID_THIS' ``` + +### ERR_INVALID_TRANSFER_OBJECT + +An invalid transfer object was passed to `postMessage()`. + ### ERR_INVALID_TUPLE @@ -1278,6 +1300,12 @@ strict compliance with the API specification (which in some cases may accept `func(undefined)` and `func()` are treated identically, and the [`ERR_INVALID_ARG_TYPE`][] error code may be used instead. + +### ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST + +A `MessagePort` was found in the object passed to a `postMessage()` call, +but not provided in the `transferList` for that call. + ### ERR_MISSING_MODULE @@ -1285,6 +1313,13 @@ strict compliance with the API specification (which in some cases may accept An [ES6 module][] could not be resolved. + +### ERR_MISSING_PLATFORM_FOR_WORKER + +The V8 platform used by this instance of Node.js does not support creating +Workers. This is caused by lack of embedder support for Workers. In particular, +this error will not occur with standard builds of Node.js. + ### ERR_MODULE_RESOLUTION_LEGACY @@ -1694,6 +1729,22 @@ The fulfilled value of a linking promise is not a `vm.Module` object. The current module's status does not allow for this operation. The specific meaning of the error depends on the specific function. + +### ERR_WORKER_NEED_ABSOLUTE_PATH + +The path for the main script of a worker is not an absolute path. + + +### ERR_WORKER_UNSERIALIZABLE_ERROR + +All attempts at serializing an uncaught exception from a worker thread failed. + + +### ERR_WORKER_UNSUPPORTED_EXTENSION + +The pathname used for the main script of a worker has an +unknown file extension. + ### ERR_ZLIB_INITIALIZATION_FAILED diff --git a/doc/api/process.md b/doc/api/process.md index 8205efbd780300..92b39d6da4a97d 100644 --- a/doc/api/process.md +++ b/doc/api/process.md @@ -410,6 +410,8 @@ added: v0.7.0 The `process.abort()` method causes the Node.js process to exit immediately and generate a core file. +This feature is not available in [`Worker`][] threads. + ## process.arch + +> Stability: 1 - Experimental + +The `worker` module provides a way to create multiple environments running +on independent threads, and to create message channels between them. It +can be accessed using: + +```js +const worker = require('worker_threads'); +``` + +Workers are useful for performing CPU-intensive JavaScript operations; do not +use them for I/O, since Node.js’s built-in mechanisms for performing operations +asynchronously already treat it more efficiently than Worker threads can. + +Workers, unlike child processes or when using the `cluster` module, can also +share memory efficiently by transferring `ArrayBuffer` instances or sharing +`SharedArrayBuffer` instances between them. + +## Example + +```js +const { + Worker, isMainThread, parentPort, workerData +} = require('worker_threads'); + +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; + parentPort.postMessage(parse(script)); +} +``` + +Note that this example spawns a Worker thread for each `parse` call. +In practice, it is strongly recommended to use a pool of Workers for these +kinds of tasks, since the overhead of creating Workers would likely exceed the +benefit of handing the work off to it. + +## worker.isMainThread + + +* {boolean} + +Is `true` if this code is not running inside of a [`Worker`][] thread. + +## worker.parentPort + + +* {null|MessagePort} + +If this thread was spawned as a [`Worker`][], this will be a [`MessagePort`][] +allowing communication with the parent thread. Messages sent using +`parentPort.postMessage()` will be available in the parent thread +using `worker.on('message')`, and messages sent from the parent thread +using `worker.postMessage()` will be available in this thread using +`parentPort.on('message')`. + +## worker.threadId + + +* {integer} + +An integer identifier for the current thread. On the corresponding worker object +(if there is any), it is available as [`worker.threadId`][]. + +## worker.workerData + + +An arbitrary JavaScript value that contains a clone of the data passed +to this thread’s `Worker` constructor. + +## Class: MessageChannel + + +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_threads'); + +const { port1, port2 } = new MessageChannel(); +port1.on('message', (message) => console.log('received', message)); +port2.postMessage({ foo: 'bar' }); +// prints: received { foo: 'bar' } from the `port1.on('message')` listener +``` + +## Class: MessagePort + + +* Extends: {EventEmitter} + +Instances of the `worker.MessagePort` class represent one end of an +asynchronous, two-way communications channel. It can be used to transfer +structured data, memory regions and other `MessagePort`s between different +[`Worker`][]s. + +With the exception of `MessagePort`s being [`EventEmitter`][]s rather +than `EventTarget`s, this implementation matches [browser `MessagePort`][]s. + +### Event: 'close' + + +The `'close'` event is emitted once either side of the channel has been +disconnected. + +### Event: 'message' + + +* `value` {any} The transmitted value + +The `'message'` event is emitted for any incoming message, containing the cloned +input of [`port.postMessage()`][]. + +Listeners on this event will receive a clone of the `value` parameter as passed +to `postMessage()` and no further arguments. + +### port.close() + + +Disables further sending of messages on either side of the connection. +This method can be called once you know that no further communication +will happen over this `MessagePort`. + +### port.postMessage(value[, transferList]) + + +* `value` {any} +* `transferList` {Object[]} + +Sends a JavaScript value to the receiving side of this channel. +`value` will be transferred in a way which is compatible with +the [HTML structured clone algorithm][]. In particular, it may contain circular +references and objects like typed arrays that the `JSON` API is not able +to stringify. + +`transferList` may be a list of `ArrayBuffer` 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`). Unlike with +[child processes][], transferring handles such as network sockets is currently +not supported. + +If `value` contains [`SharedArrayBuffer`][] instances, those will be accessible +from either thread. They cannot be listed in `transferList`. + +`value` may still contain `ArrayBuffer` instances that are not in +`transferList`; in that case, the underlying memory is copied rather than moved. + +Because the object cloning uses the structured clone algorithm, +non-enumerable properties, property accessors, and object prototypes are +not preserved. In particular, [`Buffer`][] objects will be read as +plain [`Uint8Array`][]s on the receiving side. + +The message object will be cloned immediately, and can be modified after +posting without having side effects. + +For more information on the serialization and deserialization mechanisms +behind this API, see the [serialization API of the `v8` module][v8.serdes]. + +### port.ref() + + +Opposite of `unref()`. Calling `ref()` on a previously `unref()`ed port will +*not* let the program exit if it's the only active handle left (the default +behavior). If the port is `ref()`ed, calling `ref()` again will have no effect. + +If listeners are attached or removed using `.on('message')`, the port will +be `ref()`ed and `unref()`ed automatically depending on whether +listeners for the event exist. + +### port.start() + + +Starts receiving messages on this `MessagePort`. When using this port +as an event emitter, this will be called automatically once `'message'` +listeners are attached. + +### port.unref() + + +Calling `unref()` on a port will allow the thread to exit if this is the only +active handle in the event system. If the port is already `unref()`ed calling +`unref()` again will have no effect. + +If listeners are attached or removed using `.on('message')`, the port will +be `ref()`ed and `unref()`ed automatically depending on whether +listeners for the event exist. + +## Class: Worker + + +The `Worker` class represents an independent JavaScript execution thread. +Most Node.js APIs are available inside of it. + +Notable differences inside a Worker environment are: + +- The [`process.stdin`][], [`process.stdout`][] and [`process.stderr`][] + may be redirected by the parent thread. +- The [`require('worker_threads').isMainThread`][] property is set to `false`. +- The [`require('worker_threads').parentPort`][] message port is available, +- [`process.exit()`][] does not stop the whole program, just the single thread, + and [`process.abort()`][] is not available. +- [`process.chdir()`][] and `process` methods that set group or user ids + are not available. +- [`process.env`][] is a read-only reference to the environment variables. +- [`process.title`][] cannot be modified. +- Signals will not be delivered through [`process.on('...')`][Signals events]. +- Execution may stop at any point as a result of [`worker.terminate()`][] + being invoked. +- IPC channels from parent processes are not accessible. + +Currently, the following differences also exist until they are addressed: + +- The [`inspector`][] module is not available yet. +- Native addons are not supported yet. + +Creating `Worker` instances inside of other `Worker`s is possible. + +Like [Web Workers][] and the [`cluster` module][], two-way communication can be +achieved through inter-thread message passing. Internally, a `Worker` has a +built-in pair of [`MessagePort`][]s that are already associated with each other +when the `Worker` is created. While the `MessagePort` object on the parent side +is not directly exposed, its functionalities are exposed through +[`worker.postMessage()`][] and the [`worker.on('message')`][] event +on the `Worker` object for the parent thread. + +To create custom messaging channels (which is encouraged over using the default +global channel because it facilitates separation of concerns), users can create +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_threads'); +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_threads').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_threads').workerData`][]. The cloning will + occur as described in the [HTML structured clone algorithm][], and an error + will be thrown if the object cannot be cloned (e.g. because it contains + `function`s). + * stdin {boolean} If this is set to `true`, then `worker.stdin` will + provide a writable stream whose contents will appear as `process.stdin` + inside the Worker. By default, no data is provided. + * stdout {boolean} If this is set to `true`, then `worker.stdout` will + not automatically be piped through to `process.stdout` in the parent. + * stderr {boolean} If this is set to `true`, then `worker.stderr` will + not automatically be piped through to `process.stderr` in the parent. + +### Event: 'error' + + +* `err` {Error} + +The `'error'` event is emitted if the worker thread throws an uncaught +exception. In that case, the worker will be terminated. + +### Event: 'exit' + + +* `exitCode` {integer} + +The `'exit'` event is emitted once the worker has stopped. If the worker +exited by calling [`process.exit()`][], the `exitCode` parameter will be the +passed exit code. If the worker was terminated, the `exitCode` parameter will +be `1`. + +### Event: 'message' + + +* `value` {any} The transmitted value + +The `'message'` event is emitted when the worker thread has invoked +[`require('worker_threads').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_threads').on('workerMessage')`][]. +See [`port.postMessage()`][] for more details. + +### worker.ref() + + +Opposite of `unref()`, calling `ref()` on a previously `unref()`ed worker will +*not* let the program exit if it's the only active handle left (the default +behavior). If the worker is `ref()`ed, calling `ref()` again will have +no effect. + +### worker.stderr + + +* {stream.Readable} + +This is a readable stream which contains data written to [`process.stderr`][] +inside the worker thread. If `stderr: true` was not passed to the +[`Worker`][] constructor, then data will be piped to the parent thread's +[`process.stderr`][] stream. + +### worker.stdin + + +* {null|stream.Writable} + +If `stdin: true` was passed to the [`Worker`][] constructor, this is a +writable stream. The data written to this stream will be made available in +the worker thread as [`process.stdin`][]. + +### worker.stdout + + +* {stream.Readable} + +This is a readable stream which contains data written to [`process.stdout`][] +inside the worker thread. If `stdout: true` was not passed to the +[`Worker`][] constructor, then data will be piped to the parent thread's +[`process.stdout`][] stream. + +### worker.terminate([callback]) + + +* `callback` {Function} + +Stop all JavaScript execution in the worker thread as soon as possible. +`callback` is an optional function that is invoked once this operation is known +to have completed. + +**Warning**: Currently, not all code in the internals of Node.js is prepared to +expect termination at arbitrary points in time and may crash if it encounters +that condition. Consequently, you should currently only call `.terminate()` if +it is known that the Worker thread is not accessing Node.js core modules other +than what is exposed in the `worker` module. + +### worker.threadId + + +* {integer} + +An integer identifier for the referenced thread. Inside the worker thread, +it is available as [`require('worker_threads').threadId`][]. + +### worker.unref() + + +Calling `unref()` on a worker will allow the thread to exit if this is the only +active handle in the event system. If the worker is already `unref()`ed calling +`unref()` again will have no effect. + +[`Buffer`]: buffer.html +[`EventEmitter`]: events.html +[`MessagePort`]: #worker_threads_class_messageport +[`port.postMessage()`]: #worker_threads_port_postmessage_value_transferlist +[`Worker`]: #worker_threads_class_worker +[`worker.terminate()`]: #worker_threads_worker_terminate_callback +[`worker.postMessage()`]: #worker_threads_worker_postmessage_value_transferlist_1 +[`worker.on('message')`]: #worker_threads_event_message_1 +[`worker.threadId`]: #worker_threads_worker_threadid_1 +[`port.on('message')`]: #worker_threads_event_message +[`process.exit()`]: process.html#process_process_exit_code +[`process.abort()`]: process.html#process_process_abort +[`process.chdir()`]: process.html#process_process_chdir_directory +[`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_threads').workerData`]: #worker_threads_worker_workerdata +[`require('worker_threads').on('workerMessage')`]: #worker_threads_event_workermessage +[`require('worker_threads').postMessage()`]: #worker_threads_worker_postmessage_value_transferlist +[`require('worker_threads').isMainThread`]: #worker_threads_worker_ismainthread +[`require('worker_threads').threadId`]: #worker_threads_worker_threadid +[`cluster` module]: cluster.html +[`inspector`]: inspector.html +[v8.serdes]: v8.html#v8_serialization_api +[`SharedArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer +[Signals events]: process.html#process_signal_events +[`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array +[browser `MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort +[child processes]: child_process.html +[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm +[Web Workers]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API diff --git a/lib/inspector.js b/lib/inspector.js index 3285c1040a7132..f4ec71fd6c2105 100644 --- a/lib/inspector.js +++ b/lib/inspector.js @@ -12,7 +12,7 @@ const { const util = require('util'); const { Connection, open, url } = process.binding('inspector'); -if (!Connection) +if (!Connection || !require('internal/worker').isMainThread) throw new ERR_INSPECTOR_NOT_AVAILABLE(); const connectionSymbol = Symbol('connectionProperty'); diff --git a/lib/internal/bootstrap/loaders.js b/lib/internal/bootstrap/loaders.js index ff809a91291bee..4291092532ec94 100644 --- a/lib/internal/bootstrap/loaders.js +++ b/lib/internal/bootstrap/loaders.js @@ -194,7 +194,9 @@ }; NativeModule.isInternal = function(id) { - return id.startsWith('internal/'); + return id.startsWith('internal/') || + (id === 'worker_threads' && + !process.binding('config').experimentalWorker); }; } diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index 6477c2d8282f43..4817ec110a99e5 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -24,6 +24,7 @@ _shouldAbortOnUncaughtToggle }, { internalBinding, NativeModule }) { const exceptionHandlerState = { captureFn: null }; + const isMainThread = internalBinding('worker').threadId === 0; function startup() { const EventEmitter = NativeModule.require('events'); @@ -100,7 +101,9 @@ NativeModule.require('internal/inspector_async_hook').setup(); } - _process.setupChannel(); + if (isMainThread) + _process.setupChannel(); + _process.setupRawDebug(_rawDebug); const browserGlobals = !process._noBrowserGlobals; @@ -175,8 +178,11 @@ // are running from a script and running the REPL - but there are a few // others like the debugger or running --eval arguments. Here we decide // which mode we run in. - - if (NativeModule.exists('_third_party_main')) { + if (internalBinding('worker').getEnvMessagePort() !== undefined) { + // This means we are in a Worker context, and any script execution + // will be directed by the worker module. + NativeModule.require('internal/worker').setupChild(evalScript); + } else if (NativeModule.exists('_third_party_main')) { // To allow people to extend Node in different ways, this hook allows // one to drop a file lib/_third_party_main.js into the build // directory which will be executed instead of Node's normal loading. @@ -542,7 +548,7 @@ return `process.binding('inspector').callAndPauseOnStart(${fn}, {})`; } - function evalScript(name) { + function evalScript(name, body = wrapForBreakOnFirstLine(process._eval)) { const CJSModule = NativeModule.require('internal/modules/cjs/loader'); const path = NativeModule.require('path'); const cwd = tryGetCwd(path); @@ -550,7 +556,6 @@ const module = new CJSModule(name); module.filename = path.join(cwd, name); module.paths = CJSModule._nodeModulePaths(cwd); - const body = wrapForBreakOnFirstLine(process._eval); const script = `global.__filename = ${JSON.stringify(name)};\n` + 'global.exports = exports;\n' + 'global.module = module;\n' + diff --git a/lib/internal/error-serdes.js b/lib/internal/error-serdes.js new file mode 100644 index 00000000000000..9da1a864171607 --- /dev/null +++ b/lib/internal/error-serdes.js @@ -0,0 +1,121 @@ +'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 {} + } + 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 {} + const serialized = serialize({ + constructor: name, + properties: TryGetAllProperties(error) + }); + return Buffer.concat([Buffer.from([kSerializedError]), serialized]); + } + } + } + } catch {} + try { + const serialized = serialize(error); + return Buffer.concat([Buffer.from([kSerializedObject]), serialized]); + } catch {} + 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.subarray(1)); + const ctor = errors[constructor]; + return Object.create(ctor.prototype, properties); + case kSerializedObject: + return deserialize(error.subarray(1)); + case kInspectedError: + const buf = Buffer.from(error.buffer, + error.byteOffset + 1, + error.byteLength - 1); + return buf.toString('utf8'); + } + require('assert').fail('This should not happen'); +} + +module.exports = { serializeError, deserializeError }; diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 89c0139f8b6fde..54201d0d1e7f4c 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -844,4 +844,12 @@ E('ERR_VM_MODULE_NOT_LINKED', E('ERR_VM_MODULE_NOT_MODULE', 'Provided module is not an instance of Module', Error); E('ERR_VM_MODULE_STATUS', 'Module status %s', Error); +E('ERR_WORKER_NEED_ABSOLUTE_PATH', + 'The worker script filename must be an absolute path. Received "%s"', + TypeError); +E('ERR_WORKER_UNSERIALIZABLE_ERROR', + 'Serializing an uncaught exception failed', Error); +E('ERR_WORKER_UNSUPPORTED_EXTENSION', + 'The worker script extension must be ".js" or ".mjs". Received "%s"', + TypeError); E('ERR_ZLIB_INITIALIZATION_FAILED', 'Initialization failed', Error); diff --git a/lib/internal/modules/cjs/helpers.js b/lib/internal/modules/cjs/helpers.js index 60346c5841c7df..5b5199c262ae3b 100644 --- a/lib/internal/modules/cjs/helpers.js +++ b/lib/internal/modules/cjs/helpers.js @@ -105,6 +105,11 @@ const builtinLibs = [ 'v8', 'vm', 'zlib' ]; +if (process.binding('config').experimentalWorker) { + builtinLibs.push('worker_threads'); + builtinLibs.sort(); +} + if (typeof process.binding('inspector').open === 'function') { builtinLibs.push('inspector'); builtinLibs.sort(); diff --git a/lib/internal/process.js b/lib/internal/process.js index 0f0e40d6a0cdbc..f01be32be4b6a3 100644 --- a/lib/internal/process.js +++ b/lib/internal/process.js @@ -16,6 +16,7 @@ const util = require('util'); const constants = process.binding('constants').os.signals; const assert = require('assert').strict; const { deprecate } = require('internal/util'); +const { isMainThread } = require('internal/worker'); process.assert = deprecate( function(x, msg) { @@ -186,6 +187,11 @@ function setupKillAndExit() { function setupSignalHandlers() { + if (!isMainThread) { + // Worker threads don't receive signals. + return; + } + const signalWraps = Object.create(null); let Signal; diff --git a/lib/internal/process/methods.js b/lib/internal/process/methods.js index 91aca398b346d4..9a954f6a9b93cf 100644 --- a/lib/internal/process/methods.js +++ b/lib/internal/process/methods.js @@ -8,11 +8,18 @@ const { validateMode, validateUint32 } = require('internal/validators'); +const { + isMainThread +} = require('internal/worker'); function setupProcessMethods(_chdir, _cpuUsage, _hrtime, _memoryUsage, _rawDebug, _umask, _initgroups, _setegid, _seteuid, _setgid, _setuid, _setgroups) { // Non-POSIX platforms like Windows don't have certain methods. + // Workers also lack these methods since they change process-global state. + if (!isMainThread) + return; + if (_setgid !== undefined) { setupPosixMethods(_initgroups, _setegid, _seteuid, _setgid, _setuid, _setgroups); diff --git a/lib/internal/process/stdio.js b/lib/internal/process/stdio.js index eaba4dfca13a47..e5bb50e8ed81de 100644 --- a/lib/internal/process/stdio.js +++ b/lib/internal/process/stdio.js @@ -6,6 +6,10 @@ const { ERR_UNKNOWN_STDIN_TYPE, ERR_UNKNOWN_STREAM_TYPE } = require('internal/errors').codes; +const { + isMainThread, + workerStdio +} = require('internal/worker'); exports.setup = setupStdio; @@ -16,6 +20,7 @@ function setupStdio() { function getStdout() { if (stdout) return stdout; + if (!isMainThread) return workerStdio.stdout; stdout = createWritableStdioStream(1); stdout.destroySoon = stdout.destroy; stdout._destroy = function(er, cb) { @@ -31,6 +36,7 @@ function setupStdio() { function getStderr() { if (stderr) return stderr; + if (!isMainThread) return workerStdio.stderr; stderr = createWritableStdioStream(2); stderr.destroySoon = stderr.destroy; stderr._destroy = function(er, cb) { @@ -46,6 +52,7 @@ function setupStdio() { function getStdin() { if (stdin) return stdin; + if (!isMainThread) return workerStdio.stdin; const tty_wrap = process.binding('tty_wrap'); const fd = 0; diff --git a/lib/internal/util/inspector.js b/lib/internal/util/inspector.js index 634d3302333584..3dd73415ded862 100644 --- a/lib/internal/util/inspector.js +++ b/lib/internal/util/inspector.js @@ -1,6 +1,8 @@ 'use strict'; -const hasInspector = process.config.variables.v8_enable_inspector === 1; +// TODO(addaleax): Figure out how to integrate the inspector with workers. +const hasInspector = process.config.variables.v8_enable_inspector === 1 && + require('internal/worker').isMainThread; const inspector = hasInspector ? require('inspector') : undefined; let session; diff --git a/lib/internal/worker.js b/lib/internal/worker.js new file mode 100644 index 00000000000000..de00f20d4f0e1d --- /dev/null +++ b/lib/internal/worker.js @@ -0,0 +1,476 @@ +'use strict'; + +const EventEmitter = require('events'); +const assert = require('assert'); +const path = require('path'); +const util = require('util'); +const { Readable, Writable } = require('stream'); +const { + ERR_INVALID_ARG_TYPE, + ERR_WORKER_NEED_ABSOLUTE_PATH, + ERR_WORKER_UNSERIALIZABLE_ERROR, + ERR_WORKER_UNSUPPORTED_EXTENSION, +} = require('internal/errors').codes; + +const { internalBinding } = require('internal/bootstrap/loaders'); +const { MessagePort, MessageChannel } = internalBinding('messaging'); +const { handle_onclose } = internalBinding('symbols'); +const { clearAsyncIdStack } = require('internal/async_hooks'); +const { serializeError, deserializeError } = require('internal/error-serdes'); + +util.inherits(MessagePort, EventEmitter); + +const { + Worker: WorkerImpl, + getEnvMessagePort, + threadId +} = internalBinding('worker'); + +const isMainThread = threadId === 0; + +const kOnMessageListener = Symbol('kOnMessageListener'); +const kHandle = Symbol('kHandle'); +const kName = Symbol('kName'); +const kPort = Symbol('kPort'); +const kPublicPort = Symbol('kPublicPort'); +const kDispose = Symbol('kDispose'); +const kOnExit = Symbol('kOnExit'); +const kOnMessage = Symbol('kOnMessage'); +const kOnCouldNotSerializeErr = Symbol('kOnCouldNotSerializeErr'); +const kOnErrorMessage = Symbol('kOnErrorMessage'); +const kParentSideStdio = Symbol('kParentSideStdio'); +const kWritableCallbacks = Symbol('kWritableCallbacks'); +const kStdioWantsMoreDataCallback = Symbol('kStdioWantsMoreDataCallback'); +const kStartedReading = Symbol('kStartedReading'); +const kWaitingStreams = Symbol('kWaitingStreams'); +const kIncrementsPortRef = Symbol('kIncrementsPortRef'); + +const debug = util.debuglog('worker'); + +// A communication channel consisting of a handle (that wraps around an +// uv_async_t) which can receive information from other threads and emits +// .onmessage events, and a function used for sending data to a MessagePort +// in some other thread. +MessagePort.prototype[kOnMessageListener] = function onmessage(payload) { + debug(`[${threadId}] received message`, payload); + // Emit the deserialized object to userland. + this.emit('message', payload); +}; + +// This is for compatibility with the Web's MessagePort API. It makes sense to +// provide it as an `EventEmitter` in Node.js, but if somebody overrides +// `onmessage`, we'll switch over to the Web API model. +Object.defineProperty(MessagePort.prototype, 'onmessage', { + enumerable: true, + configurable: true, + get() { + return this[kOnMessageListener]; + }, + set(value) { + this[kOnMessageListener] = value; + if (typeof value === 'function') { + this.ref(); + this.start(); + } else { + this.unref(); + this.stop(); + } + } +}); + +// This is called from inside the `MessagePort` constructor. +function oninit() { + setupPortReferencing(this, this, 'message'); +} + +Object.defineProperty(MessagePort.prototype, 'oninit', { + enumerable: true, + writable: false, + value: oninit +}); + +// This is called after the underlying `uv_async_t` has been closed. +function onclose() { + if (typeof this.onclose === 'function') { + // Not part of the Web standard yet, but there aren't many reasonable + // alternatives in a non-EventEmitter usage setting. + // Refs: https://github.com/whatwg/html/issues/1766 + this.onclose(); + } + this.emit('close'); +} + +Object.defineProperty(MessagePort.prototype, handle_onclose, { + enumerable: false, + writable: false, + value: onclose +}); + +const originalClose = MessagePort.prototype.close; +MessagePort.prototype.close = function(cb) { + if (typeof cb === 'function') + this.once('close', cb); + originalClose.call(this); +}; + +const drainMessagePort = MessagePort.prototype.drain; +delete MessagePort.prototype.drain; + +function setupPortReferencing(port, eventEmitter, eventName) { + // Keep track of whether there are any workerMessage listeners: + // If there are some, ref() the channel so it keeps the event loop alive. + // If there are none or all are removed, unref() the channel so the worker + // can shutdown gracefully. + port.unref(); + eventEmitter.on('newListener', (name) => { + if (name === eventName && eventEmitter.listenerCount(eventName) === 0) { + port.ref(); + port.start(); + } + }); + eventEmitter.on('removeListener', (name) => { + if (name === eventName && eventEmitter.listenerCount(eventName) === 0) { + port.stop(); + port.unref(); + } + }); +} + + +class ReadableWorkerStdio extends Readable { + constructor(port, name) { + super(); + this[kPort] = port; + this[kName] = name; + this[kIncrementsPortRef] = true; + this[kStartedReading] = false; + this.on('end', () => { + if (this[kIncrementsPortRef] && --this[kPort][kWaitingStreams] === 0) + this[kPort].unref(); + }); + } + + _read() { + if (!this[kStartedReading] && this[kIncrementsPortRef]) { + this[kStartedReading] = true; + if (this[kPort][kWaitingStreams]++ === 0) + this[kPort].ref(); + } + + this[kPort].postMessage({ + type: 'stdioWantsMoreData', + stream: this[kName] + }); + } +} + +class WritableWorkerStdio extends Writable { + constructor(port, name) { + super({ decodeStrings: false }); + this[kPort] = port; + this[kName] = name; + this[kWritableCallbacks] = []; + } + + _write(chunk, encoding, cb) { + this[kPort].postMessage({ + type: 'stdioPayload', + stream: this[kName], + chunk, + encoding + }); + this[kWritableCallbacks].push(cb); + if (this[kPort][kWaitingStreams]++ === 0) + this[kPort].ref(); + } + + _final(cb) { + this[kPort].postMessage({ + type: 'stdioPayload', + stream: this[kName], + chunk: null + }); + cb(); + } + + [kStdioWantsMoreDataCallback]() { + const cbs = this[kWritableCallbacks]; + this[kWritableCallbacks] = []; + for (const cb of cbs) + cb(); + if ((this[kPort][kWaitingStreams] -= cbs.length) === 0) + this[kPort].unref(); + } +} + +class Worker extends EventEmitter { + constructor(filename, options = {}) { + super(); + debug(`[${threadId}] create new worker`, filename, options); + if (typeof filename !== 'string') { + throw new ERR_INVALID_ARG_TYPE('filename', 'string', filename); + } + + if (!options.eval) { + if (!path.isAbsolute(filename)) { + throw new ERR_WORKER_NEED_ABSOLUTE_PATH(filename); + } + const ext = path.extname(filename); + if (ext !== '.js' && ext !== '.mjs') { + throw new ERR_WORKER_UNSUPPORTED_EXTENSION(ext); + } + } + + // Set up the C++ handle for the worker, as well as some internal wiring. + this[kHandle] = new WorkerImpl(); + this[kHandle].onexit = (code) => this[kOnExit](code); + this[kPort] = this[kHandle].messagePort; + this[kPort].on('message', (data) => this[kOnMessage](data)); + this[kPort].start(); + this[kPort].unref(); + this[kPort][kWaitingStreams] = 0; + debug(`[${threadId}] created Worker with ID ${this.threadId}`); + + let stdin = null; + if (options.stdin) + stdin = new WritableWorkerStdio(this[kPort], 'stdin'); + const stdout = new ReadableWorkerStdio(this[kPort], 'stdout'); + if (!options.stdout) { + stdout[kIncrementsPortRef] = false; + pipeWithoutWarning(stdout, process.stdout); + } + const stderr = new ReadableWorkerStdio(this[kPort], 'stderr'); + if (!options.stderr) { + stderr[kIncrementsPortRef] = false; + pipeWithoutWarning(stderr, process.stderr); + } + + this[kParentSideStdio] = { stdin, stdout, stderr }; + + const { port1, port2 } = new MessageChannel(); + this[kPublicPort] = port1; + this[kPublicPort].on('message', (message) => this.emit('message', message)); + setupPortReferencing(this[kPublicPort], this, 'message'); + this[kPort].postMessage({ + type: 'loadScript', + filename, + doEval: !!options.eval, + workerData: options.workerData, + publicPort: port2, + hasStdin: !!options.stdin + }, [port2]); + // Actually start the new thread now that everything is in place. + this[kHandle].startThread(); + } + + [kOnExit](code) { + debug(`[${threadId}] hears end event for Worker ${this.threadId}`); + drainMessagePort.call(this[kPublicPort]); + this[kDispose](); + this.emit('exit', code); + this.removeAllListeners(); + } + + [kOnCouldNotSerializeErr]() { + this.emit('error', new ERR_WORKER_UNSERIALIZABLE_ERROR()); + } + + [kOnErrorMessage](serialized) { + // This is what is called for uncaught exceptions. + const error = deserializeError(serialized); + this.emit('error', error); + } + + [kOnMessage](message) { + switch (message.type) { + case 'upAndRunning': + return this.emit('online'); + case 'couldNotSerializeError': + return this[kOnCouldNotSerializeErr](); + case 'errorMessage': + return this[kOnErrorMessage](message.error); + case 'stdioPayload': + { + const { stream, chunk, encoding } = message; + return this[kParentSideStdio][stream].push(chunk, encoding); + } + case 'stdioWantsMoreData': + { + const { stream } = message; + return this[kParentSideStdio][stream][kStdioWantsMoreDataCallback](); + } + } + + assert.fail(`Unknown worker message type ${message.type}`); + } + + [kDispose]() { + this[kHandle].onexit = null; + this[kHandle] = null; + this[kPort] = null; + this[kPublicPort] = null; + + const { stdout, stderr } = this[kParentSideStdio]; + this[kParentSideStdio] = null; + + if (!stdout._readableState.ended) { + debug(`[${threadId}] explicitly closes stdout for ${this.threadId}`); + stdout.push(null); + } + if (!stderr._readableState.ended) { + debug(`[${threadId}] explicitly closes stderr for ${this.threadId}`); + stderr.push(null); + } + } + + postMessage(...args) { + this[kPublicPort].postMessage(...args); + } + + terminate(callback) { + if (this[kHandle] === null) return; + + debug(`[${threadId}] terminates Worker with ID ${this.threadId}`); + + if (typeof callback !== 'undefined') + this.once('exit', (exitCode) => callback(null, exitCode)); + + this[kHandle].stopThread(); + } + + ref() { + if (this[kHandle] === null) return; + + this[kHandle].ref(); + this[kPublicPort].ref(); + } + + unref() { + if (this[kHandle] === null) return; + + this[kHandle].unref(); + this[kPublicPort].unref(); + } + + get threadId() { + if (this[kHandle] === null) return -1; + + return this[kHandle].threadId; + } + + get stdin() { + return this[kParentSideStdio].stdin; + } + + get stdout() { + return this[kParentSideStdio].stdout; + } + + get stderr() { + return this[kParentSideStdio].stderr; + } +} + +const workerStdio = {}; +if (!isMainThread) { + const port = getEnvMessagePort(); + port[kWaitingStreams] = 0; + workerStdio.stdin = new ReadableWorkerStdio(port, 'stdin'); + workerStdio.stdout = new WritableWorkerStdio(port, 'stdout'); + workerStdio.stderr = new WritableWorkerStdio(port, 'stderr'); +} + +let originalFatalException; + +function setupChild(evalScript) { + // Called during bootstrap to set up worker script execution. + debug(`[${threadId}] is setting up worker child environment`); + const port = getEnvMessagePort(); + + const publicWorker = require('worker_threads'); + + port.on('message', (message) => { + if (message.type === 'loadScript') { + const { filename, doEval, workerData, publicPort, hasStdin } = message; + publicWorker.parentPort = publicPort; + setupPortReferencing(publicPort, publicPort, 'message'); + publicWorker.workerData = workerData; + + if (!hasStdin) + workerStdio.stdin.push(null); + + debug(`[${threadId}] starts worker script ${filename} ` + + `(eval = ${eval}) at cwd = ${process.cwd()}`); + port.unref(); + port.postMessage({ type: 'upAndRunning' }); + if (doEval) { + evalScript('[worker eval]', filename); + } else { + process.argv[1] = filename; // script filename + require('module').runMain(); + } + return; + } else if (message.type === 'stdioPayload') { + const { stream, chunk, encoding } = message; + workerStdio[stream].push(chunk, encoding); + return; + } else if (message.type === 'stdioWantsMoreData') { + const { stream } = message; + workerStdio[stream][kStdioWantsMoreDataCallback](); + return; + } + + assert.fail(`Unknown worker message type ${message.type}`); + }); + + port.start(); + + originalFatalException = process._fatalException; + process._fatalException = fatalException; + + function fatalException(error) { + debug(`[${threadId}] gets fatal exception`); + let caught = false; + try { + caught = originalFatalException.call(this, error); + } catch (e) { + error = e; + } + debug(`[${threadId}] fatal exception caught = ${caught}`); + + if (!caught) { + let serialized; + try { + serialized = serializeError(error); + } catch {} + debug(`[${threadId}] fatal exception serialized = ${!!serialized}`); + if (serialized) + port.postMessage({ type: 'errorMessage', error: serialized }); + else + port.postMessage({ type: 'couldNotSerializeError' }); + clearAsyncIdStack(); + } + } +} + +function pipeWithoutWarning(source, dest) { + const sourceMaxListeners = source._maxListeners; + const destMaxListeners = dest._maxListeners; + source.setMaxListeners(Infinity); + dest.setMaxListeners(Infinity); + + source.pipe(dest); + + source._maxListeners = sourceMaxListeners; + dest._maxListeners = destMaxListeners; +} + +module.exports = { + MessagePort, + MessageChannel, + threadId, + Worker, + setupChild, + isMainThread, + workerStdio +}; diff --git a/lib/worker_threads.js b/lib/worker_threads.js new file mode 100644 index 00000000000000..0609650cd5731d --- /dev/null +++ b/lib/worker_threads.js @@ -0,0 +1,18 @@ +'use strict'; + +const { + isMainThread, + MessagePort, + MessageChannel, + threadId, + Worker +} = require('internal/worker'); + +module.exports = { + isMainThread, + MessagePort, + MessageChannel, + threadId, + Worker, + parentPort: null +}; diff --git a/node.gyp b/node.gyp index 4b94c1dd6b2ad9..14bf5211e97362 100644 --- a/node.gyp +++ b/node.gyp @@ -78,6 +78,7 @@ 'lib/util.js', 'lib/v8.js', 'lib/vm.js', + 'lib/worker_threads.js', 'lib/zlib.js', 'lib/internal/assert.js', 'lib/internal/async_hooks.js', @@ -101,6 +102,7 @@ 'lib/internal/constants.js', 'lib/internal/encoding.js', 'lib/internal/errors.js', + 'lib/internal/error-serdes.js', 'lib/internal/fixed_queue.js', 'lib/internal/freelist.js', 'lib/internal/fs/promises.js', @@ -156,6 +158,7 @@ 'lib/internal/validators.js', 'lib/internal/stream_base_commons.js', 'lib/internal/vm/module.js', + 'lib/internal/worker.js', 'lib/internal/streams/lazy_transform.js', 'lib/internal/streams/async_iterator.js', 'lib/internal/streams/buffer_list.js', @@ -334,6 +337,7 @@ 'src/node_file.cc', 'src/node_http2.cc', 'src/node_http_parser.cc', + 'src/node_messaging.cc', 'src/node_os.cc', 'src/node_platform.cc', 'src/node_perf.cc', @@ -346,10 +350,12 @@ 'src/node_v8.cc', 'src/node_stat_watcher.cc', 'src/node_watchdog.cc', + 'src/node_worker.cc', 'src/node_zlib.cc', 'src/node_i18n.cc', 'src/pipe_wrap.cc', 'src/process_wrap.cc', + 'src/sharedarraybuffer_metadata.cc', 'src/signal_wrap.cc', 'src/spawn_sync.cc', 'src/string_bytes.cc', @@ -391,6 +397,7 @@ 'src/node_http2_state.h', 'src/node_internals.h', 'src/node_javascript.h', + 'src/node_messaging.h', 'src/node_mutex.h', 'src/node_perf.h', 'src/node_perf_common.h', @@ -402,12 +409,14 @@ 'src/node_wrap.h', 'src/node_revert.h', 'src/node_i18n.h', + 'src/node_worker.h', 'src/pipe_wrap.h', 'src/tty_wrap.h', 'src/tcp_wrap.h', 'src/udp_wrap.h', 'src/req_wrap.h', 'src/req_wrap-inl.h', + 'src/sharedarraybuffer_metadata.h', 'src/string_bytes.h', 'src/string_decoder.h', 'src/string_decoder-inl.h', diff --git a/src/async_wrap-inl.h b/src/async_wrap-inl.h index c9f12333243092..5763b17aa08bc4 100644 --- a/src/async_wrap-inl.h +++ b/src/async_wrap-inl.h @@ -65,6 +65,22 @@ inline v8::MaybeLocal AsyncWrap::MakeCallback( const v8::Local symbol, int argc, v8::Local* argv) { + return MakeCallback(symbol.As(), argc, argv); +} + + +inline v8::MaybeLocal AsyncWrap::MakeCallback( + const v8::Local symbol, + int argc, + v8::Local* argv) { + return MakeCallback(symbol.As(), argc, argv); +} + + +inline v8::MaybeLocal AsyncWrap::MakeCallback( + const v8::Local symbol, + int argc, + v8::Local* argv) { v8::Local cb_v = object()->Get(symbol); CHECK(cb_v->IsFunction()); return MakeCallback(cb_v.As(), argc, argv); diff --git a/src/async_wrap.h b/src/async_wrap.h index 451bcfe12e6717..b2f96477b490e0 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -49,6 +49,7 @@ namespace node { V(HTTP2SETTINGS) \ V(HTTPPARSER) \ V(JSSTREAM) \ + V(MESSAGEPORT) \ V(PIPECONNECTWRAP) \ V(PIPESERVERWRAP) \ V(PIPEWRAP) \ @@ -66,6 +67,7 @@ namespace node { V(TTYWRAP) \ V(UDPSENDWRAP) \ V(UDPWRAP) \ + V(WORKER) \ V(WRITEWRAP) \ V(ZLIB) @@ -158,10 +160,18 @@ class AsyncWrap : public BaseObject { v8::MaybeLocal MakeCallback(const v8::Local cb, int argc, v8::Local* argv); + inline v8::MaybeLocal MakeCallback( + const v8::Local symbol, + int argc, + v8::Local* argv); inline v8::MaybeLocal MakeCallback( const v8::Local symbol, int argc, v8::Local* argv); + inline v8::MaybeLocal MakeCallback( + const v8::Local symbol, + int argc, + v8::Local* argv); inline v8::MaybeLocal MakeCallback(uint32_t index, int argc, v8::Local* argv); diff --git a/src/base_object-inl.h b/src/base_object-inl.h index 3bd854639b2c6d..06a29223973c5d 100644 --- a/src/base_object-inl.h +++ b/src/base_object-inl.h @@ -65,6 +65,14 @@ v8::Local BaseObject::object() { return PersistentToLocal(env_->isolate(), persistent_handle_); } +v8::Local BaseObject::object(v8::Isolate* isolate) { + v8::Local handle = object(); +#ifdef DEBUG + CHECK_EQ(handle->CreationContext()->GetIsolate(), isolate); + CHECK_EQ(env_->isolate(), isolate); +#endif + return handle; +} Environment* BaseObject::env() const { return env_; diff --git a/src/base_object.h b/src/base_object.h index e0b60843401681..38291d598feb1c 100644 --- a/src/base_object.h +++ b/src/base_object.h @@ -43,6 +43,10 @@ class BaseObject { // persistent.IsEmpty() is true. inline v8::Local object(); + // Same as the above, except it additionally verifies that this object + // is associated with the passed Isolate in debug mode. + inline v8::Local object(v8::Isolate* isolate); + inline Persistent& persistent(); inline Environment* env() const; diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc index 6c7c1af3e31cf6..f9db02562d9c8a 100644 --- a/src/bootstrapper.cc +++ b/src/bootstrapper.cc @@ -17,6 +17,7 @@ using v8::Object; using v8::Promise; using v8::PromiseRejectEvent; using v8::PromiseRejectMessage; +using v8::String; using v8::Value; void SetupProcessObject(const FunctionCallbackInfo& args) { @@ -113,15 +114,17 @@ void SetupBootstrapObject(Environment* env, BOOTSTRAP_METHOD(_umask, Umask); #if defined(__POSIX__) && !defined(__ANDROID__) && !defined(__CloudABI__) - BOOTSTRAP_METHOD(_initgroups, InitGroups); - BOOTSTRAP_METHOD(_setegid, SetEGid); - BOOTSTRAP_METHOD(_seteuid, SetEUid); - BOOTSTRAP_METHOD(_setgid, SetGid); - BOOTSTRAP_METHOD(_setuid, SetUid); - BOOTSTRAP_METHOD(_setgroups, SetGroups); + if (env->is_main_thread()) { + BOOTSTRAP_METHOD(_initgroups, InitGroups); + BOOTSTRAP_METHOD(_setegid, SetEGid); + BOOTSTRAP_METHOD(_seteuid, SetEUid); + BOOTSTRAP_METHOD(_setgid, SetGid); + BOOTSTRAP_METHOD(_setuid, SetUid); + BOOTSTRAP_METHOD(_setgroups, SetGroups); + } #endif // __POSIX__ && !defined(__ANDROID__) && !defined(__CloudABI__) - auto should_abort_on_uncaught_toggle = + Local should_abort_on_uncaught_toggle = FIXED_ONE_BYTE_STRING(env->isolate(), "_shouldAbortOnUncaughtToggle"); CHECK(bootstrapper->Set(env->context(), should_abort_on_uncaught_toggle, @@ -130,4 +133,21 @@ void SetupBootstrapObject(Environment* env, } #undef BOOTSTRAP_METHOD +namespace symbols { + +void Initialize(Local target, + Local unused, + Local context) { + Environment* env = Environment::GetCurrent(context); +#define V(PropertyName, StringValue) \ + target->Set(env->context(), \ + env->PropertyName()->Name(), \ + env->PropertyName()).FromJust(); + PER_ISOLATE_SYMBOL_PROPERTIES(V) +#undef V +} + +} // namespace symbols } // namespace node + +NODE_MODULE_CONTEXT_AWARE_INTERNAL(symbols, node::symbols::Initialize) diff --git a/src/callback_scope.cc b/src/callback_scope.cc index 9eac7beb038a26..23e6d5b0632f2c 100644 --- a/src/callback_scope.cc +++ b/src/callback_scope.cc @@ -79,6 +79,11 @@ void InternalCallbackScope::Close() { closed_ = true; HandleScope handle_scope(env_->isolate()); + if (!env_->can_call_into_js()) return; + if (failed_ && !env_->is_main_thread() && env_->is_stopping_worker()) { + env_->async_hooks()->clear_async_id_stack(); + } + if (pushed_ids_) env_->async_hooks()->pop_async_id(async_context_.async_id); diff --git a/src/env-inl.h b/src/env-inl.h index cb8d0c4efe0405..eeb419b4a0fad2 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -322,6 +322,10 @@ inline Environment* Environment::GetThreadLocalEnv() { return static_cast(uv_key_get(&thread_local_env)); } +inline bool Environment::profiler_idle_notifier_started() const { + return profiler_idle_notifier_started_; +} + inline v8::Isolate* Environment::isolate() const { return isolate_; } @@ -578,13 +582,42 @@ void Environment::SetUnrefImmediate(native_immediate_callback cb, } inline bool Environment::can_call_into_js() const { - return can_call_into_js_; + return can_call_into_js_ && (is_main_thread() || !is_stopping_worker()); } inline void Environment::set_can_call_into_js(bool can_call_into_js) { can_call_into_js_ = can_call_into_js; } +inline bool Environment::is_main_thread() const { + return thread_id_ == 0; +} + +inline double Environment::thread_id() const { + return thread_id_; +} + +inline void Environment::set_thread_id(double id) { + thread_id_ = id; +} + +inline worker::Worker* Environment::worker_context() const { + return worker_context_; +} + +inline void Environment::set_worker_context(worker::Worker* context) { + CHECK_EQ(worker_context_, nullptr); // Should be set only once. + worker_context_ = context; +} + +inline void Environment::add_sub_worker_context(worker::Worker* context) { + sub_worker_contexts_.insert(context); +} + +inline void Environment::remove_sub_worker_context(worker::Worker* context) { + sub_worker_contexts_.erase(context); +} + inline performance::performance_state* Environment::performance_state() { return performance_state_.get(); } @@ -706,6 +739,7 @@ bool Environment::CleanupHookCallback::Equal::operator()( } #define VP(PropertyName, StringValue) V(v8::Private, PropertyName) +#define VY(PropertyName, StringValue) V(v8::Symbol, PropertyName) #define VS(PropertyName, StringValue) V(v8::String, PropertyName) #define V(TypeName, PropertyName) \ inline \ @@ -714,21 +748,26 @@ bool Environment::CleanupHookCallback::Equal::operator()( return const_cast(this)->PropertyName ## _.Get(isolate); \ } PER_ISOLATE_PRIVATE_SYMBOL_PROPERTIES(VP) + PER_ISOLATE_SYMBOL_PROPERTIES(VY) PER_ISOLATE_STRING_PROPERTIES(VS) #undef V #undef VS +#undef VY #undef VP #define VP(PropertyName, StringValue) V(v8::Private, PropertyName) +#define VY(PropertyName, StringValue) V(v8::Symbol, PropertyName) #define VS(PropertyName, StringValue) V(v8::String, PropertyName) #define V(TypeName, PropertyName) \ inline v8::Local Environment::PropertyName() const { \ return isolate_data()->PropertyName(isolate()); \ } PER_ISOLATE_PRIVATE_SYMBOL_PROPERTIES(VP) + PER_ISOLATE_SYMBOL_PROPERTIES(VY) PER_ISOLATE_STRING_PROPERTIES(VS) #undef V #undef VS +#undef VY #undef VP #define V(PropertyName, TypeName) \ diff --git a/src/env.cc b/src/env.cc index 7865ba95404df5..8df59d1546dbdd 100644 --- a/src/env.cc +++ b/src/env.cc @@ -4,6 +4,7 @@ #include "node_buffer.h" #include "node_platform.h" #include "node_file.h" +#include "node_worker.h" #include "tracing/agent.h" #include @@ -23,7 +24,9 @@ using v8::Private; using v8::StackFrame; using v8::StackTrace; using v8::String; +using v8::Symbol; using v8::Value; +using worker::Worker; IsolateData::IsolateData(Isolate* isolate, uv_loop_t* event_loop, @@ -59,6 +62,18 @@ IsolateData::IsolateData(Isolate* isolate, sizeof(StringValue) - 1).ToLocalChecked())); PER_ISOLATE_PRIVATE_SYMBOL_PROPERTIES(V) #undef V +#define V(PropertyName, StringValue) \ + PropertyName ## _.Set( \ + isolate, \ + Symbol::New( \ + isolate, \ + String::NewFromOneByte( \ + isolate, \ + reinterpret_cast(StringValue), \ + v8::NewStringType::kInternalized, \ + sizeof(StringValue) - 1).ToLocalChecked())); + PER_ISOLATE_SYMBOL_PROPERTIES(V) +#undef V #define V(PropertyName, StringValue) \ PropertyName ## _.Set( \ isolate, \ @@ -250,6 +265,11 @@ void Environment::CleanupHandles() { } void Environment::StartProfilerIdleNotifier() { + if (profiler_idle_notifier_started_) + return; + + profiler_idle_notifier_started_ = true; + uv_prepare_start(&idle_prepare_handle_, [](uv_prepare_t* handle) { Environment* env = ContainerOf(&Environment::idle_prepare_handle_, handle); env->isolate()->SetIdle(true); @@ -262,6 +282,7 @@ void Environment::StartProfilerIdleNotifier() { } void Environment::StopProfilerIdleNotifier() { + profiler_idle_notifier_started_ = false; uv_prepare_stop(&idle_prepare_handle_); uv_check_stop(&idle_check_handle_); } @@ -425,7 +446,9 @@ void Environment::RunAndClearNativeImmediates() { if (it->refed_) ref_count++; if (UNLIKELY(try_catch.HasCaught())) { - FatalException(isolate(), try_catch); + if (!try_catch.HasTerminated()) + FatalException(isolate(), try_catch); + // Bail out, remove the already executed callbacks from list // and set up a new TryCatch for the other pending callbacks. std::move_backward(it, list.end(), list.begin() + (list.end() - it)); @@ -613,4 +636,25 @@ void Environment::AsyncHooks::grow_async_ids_stack() { uv_key_t Environment::thread_local_env = {}; +void Environment::Exit(int exit_code) { + if (is_main_thread()) + exit(exit_code); + else + worker_context_->Exit(exit_code); +} + +void Environment::stop_sub_worker_contexts() { + while (!sub_worker_contexts_.empty()) { + Worker* w = *sub_worker_contexts_.begin(); + remove_sub_worker_context(w); + w->Exit(1); + w->JoinThread(); + } +} + +bool Environment::is_stopping_worker() const { + CHECK(!is_main_thread()); + return worker_context_->is_stopped(); +} + } // namespace node diff --git a/src/env.h b/src/env.h index 96252fa12a1397..cf6873e5fe7c6a 100644 --- a/src/env.h +++ b/src/env.h @@ -55,6 +55,10 @@ namespace performance { class performance_state; } +namespace worker { +class Worker; +} + namespace loader { class ModuleWrap; @@ -106,6 +110,12 @@ struct PackageConfig { V(decorated_private_symbol, "node:decorated") \ V(napi_env, "node:napi:env") \ V(napi_wrapper, "node:napi:wrapper") \ + V(sab_lifetimepartner_symbol, "node:sharedArrayBufferLifetimePartner") \ + +// Symbols are per-isolate primitives but Environment proxies them +// for the sake of convenience. +#define PER_ISOLATE_SYMBOL_PROPERTIES(V) \ + V(handle_onclose_symbol, "handle_onclose") \ // Strings are per-isolate primitives but Environment proxies them // for the sake of convenience. Strings should be ASCII-only. @@ -127,7 +137,6 @@ struct PackageConfig { V(chunks_sent_since_last_write_string, "chunksSentSinceLastWrite") \ V(constants_string, "constants") \ V(oncertcb_string, "oncertcb") \ - V(onclose_string, "_onclose") \ V(code_string, "code") \ V(cwd_string, "cwd") \ V(dest_string, "dest") \ @@ -188,7 +197,11 @@ struct PackageConfig { V(mac_string, "mac") \ V(main_string, "main") \ V(max_buffer_string, "maxBuffer") \ + V(max_semi_space_size_string, "maxSemiSpaceSize") \ + V(max_old_space_size_string, "maxOldSpaceSize") \ V(message_string, "message") \ + V(message_port_string, "messagePort") \ + V(message_port_constructor_string, "MessagePort") \ V(minttl_string, "minttl") \ V(modulus_string, "modulus") \ V(name_string, "name") \ @@ -208,6 +221,7 @@ struct PackageConfig { V(onhandshakedone_string, "onhandshakedone") \ V(onhandshakestart_string, "onhandshakestart") \ V(onheaders_string, "onheaders") \ + V(oninit_string, "oninit") \ V(onmessage_string, "onmessage") \ V(onnewsession_string, "onnewsession") \ V(onocspresponse_string, "onocspresponse") \ @@ -238,6 +252,8 @@ struct PackageConfig { V(pipe_target_string, "pipeTarget") \ V(pipe_source_string, "pipeSource") \ V(port_string, "port") \ + V(port1_string, "port1") \ + V(port2_string, "port2") \ V(preference_string, "preference") \ V(priority_string, "priority") \ V(promise_string, "promise") \ @@ -271,6 +287,7 @@ struct PackageConfig { V(subject_string, "subject") \ V(subjectaltname_string, "subjectaltname") \ V(syscall_string, "syscall") \ + V(thread_id_string, "threadId") \ V(ticketkeycallback_string, "onticketkeycallback") \ V(timeout_string, "timeout") \ V(tls_ticket_string, "tlsTicket") \ @@ -319,6 +336,8 @@ struct PackageConfig { V(http2stream_constructor_template, v8::ObjectTemplate) \ V(immediate_callback_function, v8::Function) \ V(inspector_console_api_object, v8::Object) \ + V(message_port, v8::Object) \ + V(message_port_constructor_template, v8::FunctionTemplate) \ V(pbkdf2_constructor_template, v8::ObjectTemplate) \ V(pipe_constructor_template, v8::FunctionTemplate) \ V(performance_entry_callback, v8::Function) \ @@ -329,6 +348,7 @@ struct PackageConfig { V(promise_wrap_template, v8::ObjectTemplate) \ V(push_values_to_array_function, v8::Function) \ V(randombytes_constructor_template, v8::ObjectTemplate) \ + V(sab_lifetimepartner_constructor_template, v8::FunctionTemplate) \ V(script_context_constructor_template, v8::FunctionTemplate) \ V(script_data_constructor_function, v8::Function) \ V(secure_context_constructor_template, v8::FunctionTemplate) \ @@ -356,10 +376,12 @@ class IsolateData { inline MultiIsolatePlatform* platform() const; #define VP(PropertyName, StringValue) V(v8::Private, PropertyName) +#define VY(PropertyName, StringValue) V(v8::Symbol, PropertyName) #define VS(PropertyName, StringValue) V(v8::String, PropertyName) #define V(TypeName, PropertyName) \ inline v8::Local PropertyName(v8::Isolate* isolate) const; PER_ISOLATE_PRIVATE_SYMBOL_PROPERTIES(VP) + PER_ISOLATE_SYMBOL_PROPERTIES(VY) PER_ISOLATE_STRING_PROPERTIES(VS) #undef V #undef VS @@ -370,10 +392,12 @@ class IsolateData { private: #define VP(PropertyName, StringValue) V(v8::Private, PropertyName) +#define VY(PropertyName, StringValue) V(v8::Symbol, PropertyName) #define VS(PropertyName, StringValue) V(v8::String, PropertyName) #define V(TypeName, PropertyName) \ v8::Eternal PropertyName ## _; PER_ISOLATE_PRIVATE_SYMBOL_PROPERTIES(VP) + PER_ISOLATE_SYMBOL_PROPERTIES(VY) PER_ISOLATE_STRING_PROPERTIES(VS) #undef V #undef VS @@ -586,6 +610,7 @@ class Environment { void RegisterHandleCleanups(); void CleanupHandles(); + void Exit(int code); // Register clean-up cb to be called on environment destruction. inline void RegisterHandleCleanup(uv_handle_t* handle, @@ -600,6 +625,7 @@ class Environment { void StartProfilerIdleNotifier(); void StopProfilerIdleNotifier(); + inline bool profiler_idle_notifier_started() const; inline v8::Isolate* isolate() const; inline tracing::Agent* tracing_agent() const; @@ -698,6 +724,18 @@ class Environment { inline bool can_call_into_js() const; inline void set_can_call_into_js(bool can_call_into_js); + // TODO(addaleax): This should be inline. + bool is_stopping_worker() const; + + inline bool is_main_thread() const; + inline double thread_id() const; + inline void set_thread_id(double id); + inline worker::Worker* worker_context() const; + inline void set_worker_context(worker::Worker* context); + inline void add_sub_worker_context(worker::Worker* context); + inline void remove_sub_worker_context(worker::Worker* context); + void stop_sub_worker_contexts(); + inline void ThrowError(const char* errmsg); inline void ThrowTypeError(const char* errmsg); inline void ThrowRangeError(const char* errmsg); @@ -737,13 +775,16 @@ class Environment { // Strings and private symbols are shared across shared contexts // The getters simply proxy to the per-isolate primitive. #define VP(PropertyName, StringValue) V(v8::Private, PropertyName) +#define VY(PropertyName, StringValue) V(v8::Symbol, PropertyName) #define VS(PropertyName, StringValue) V(v8::String, PropertyName) #define V(TypeName, PropertyName) \ inline v8::Local PropertyName() const; PER_ISOLATE_PRIVATE_SYMBOL_PROPERTIES(VP) + PER_ISOLATE_SYMBOL_PROPERTIES(VY) PER_ISOLATE_STRING_PROPERTIES(VS) #undef V #undef VS +#undef VY #undef VP #define V(PropertyName, TypeName) \ @@ -822,6 +863,7 @@ class Environment { uv_idle_t immediate_idle_handle_; uv_prepare_t idle_prepare_handle_; uv_check_t idle_check_handle_; + bool profiler_idle_notifier_started_ = false; AsyncHooks async_hooks_; ImmediateInfo immediate_info_; @@ -835,12 +877,15 @@ class Environment { std::vector destroy_async_id_list_; AliasedBuffer should_abort_on_uncaught_toggle_; - int should_not_abort_scope_counter_ = 0; std::unique_ptr performance_state_; std::unordered_map performance_marks_; + bool can_call_into_js_ = true; + double thread_id_ = 0; + std::unordered_set sub_worker_contexts_; + #if HAVE_INSPECTOR std::unique_ptr inspector_agent_; @@ -873,6 +918,8 @@ class Environment { std::vector> file_handle_read_wrap_freelist_; + worker::Worker* worker_context_ = nullptr; + struct ExitCallback { void (*cb_)(void* arg); void* arg_; diff --git a/src/handle_wrap.cc b/src/handle_wrap.cc index 20356b94a5775a..4c2a33aa84459d 100644 --- a/src/handle_wrap.cc +++ b/src/handle_wrap.cc @@ -67,7 +67,7 @@ void HandleWrap::Close(const FunctionCallbackInfo& args) { wrap->Close(args[0]); } -void HandleWrap::Close(v8::Local close_callback) { +void HandleWrap::Close(Local close_callback) { if (state_ != kInitialized) return; @@ -76,9 +76,10 @@ void HandleWrap::Close(v8::Local close_callback) { state_ = kClosing; if (!close_callback.IsEmpty() && close_callback->IsFunction()) { - object()->Set(env()->context(), env()->onclose_string(), close_callback) - .FromJust(); - state_ = kClosingWithCallback; + object()->Set(env()->context(), + env()->handle_onclose_symbol(), + close_callback) + .FromMaybe(false); } } @@ -109,24 +110,23 @@ HandleWrap::HandleWrap(Environment* env, void HandleWrap::OnClose(uv_handle_t* handle) { - HandleWrap* wrap = static_cast(handle->data); + std::unique_ptr wrap { static_cast(handle->data) }; Environment* env = wrap->env(); HandleScope scope(env->isolate()); Context::Scope context_scope(env->context()); // The wrap object should still be there. CHECK_EQ(wrap->persistent().IsEmpty(), false); - CHECK(wrap->state_ >= kClosing && wrap->state_ <= kClosingWithCallback); + CHECK_EQ(wrap->state_, kClosing); - const bool have_close_callback = (wrap->state_ == kClosingWithCallback); wrap->state_ = kClosed; wrap->OnClose(); - if (have_close_callback) - wrap->MakeCallback(env->onclose_string(), 0, nullptr); - - delete wrap; + if (wrap->object()->Has(env->context(), env->handle_onclose_symbol()) + .FromMaybe(false)) { + wrap->MakeCallback(env->handle_onclose_symbol(), 0, nullptr); + } } diff --git a/src/handle_wrap.h b/src/handle_wrap.h index b2b09f5010d1f7..4e177d249f28b5 100644 --- a/src/handle_wrap.h +++ b/src/handle_wrap.h @@ -95,7 +95,7 @@ class HandleWrap : public AsyncWrap { // refer to `doc/guides/node-postmortem-support.md` friend int GenDebugSymbols(); ListNode handle_wrap_queue_; - enum { kInitialized, kClosing, kClosingWithCallback, kClosed } state_; + enum { kInitialized, kClosing, kClosed } state_; uv_handle_t* const handle_; }; diff --git a/src/js_stream.cc b/src/js_stream.cc index c766c322e3017a..e562a62f3d1bb2 100644 --- a/src/js_stream.cc +++ b/src/js_stream.cc @@ -44,7 +44,8 @@ bool JSStream::IsClosing() { TryCatch try_catch(env()->isolate()); Local value; if (!MakeCallback(env()->isclosing_string(), 0, nullptr).ToLocal(&value)) { - FatalException(env()->isolate(), try_catch); + if (!try_catch.HasTerminated()) + FatalException(env()->isolate(), try_catch); return true; } return value->IsTrue(); @@ -59,7 +60,8 @@ int JSStream::ReadStart() { int value_int = UV_EPROTO; if (!MakeCallback(env()->onreadstart_string(), 0, nullptr).ToLocal(&value) || !value->Int32Value(env()->context()).To(&value_int)) { - FatalException(env()->isolate(), try_catch); + if (!try_catch.HasTerminated()) + FatalException(env()->isolate(), try_catch); } return value_int; } @@ -73,7 +75,8 @@ int JSStream::ReadStop() { int value_int = UV_EPROTO; if (!MakeCallback(env()->onreadstop_string(), 0, nullptr).ToLocal(&value) || !value->Int32Value(env()->context()).To(&value_int)) { - FatalException(env()->isolate(), try_catch); + if (!try_catch.HasTerminated()) + FatalException(env()->isolate(), try_catch); } return value_int; } @@ -94,7 +97,8 @@ int JSStream::DoShutdown(ShutdownWrap* req_wrap) { arraysize(argv), argv).ToLocal(&value) || !value->Int32Value(env()->context()).To(&value_int)) { - FatalException(env()->isolate(), try_catch); + if (!try_catch.HasTerminated()) + FatalException(env()->isolate(), try_catch); } return value_int; } @@ -128,7 +132,8 @@ int JSStream::DoWrite(WriteWrap* w, arraysize(argv), argv).ToLocal(&value) || !value->Int32Value(env()->context()).To(&value_int)) { - FatalException(env()->isolate(), try_catch); + if (!try_catch.HasTerminated()) + FatalException(env()->isolate(), try_catch); } return value_int; } diff --git a/src/node.cc b/src/node.cc index bf3aae2d35f773..663e4a222eba91 100644 --- a/src/node.cc +++ b/src/node.cc @@ -253,6 +253,11 @@ bool config_experimental_modules = false; // that is used by lib/vm.js bool config_experimental_vm_modules = false; +// Set in node.cc by ParseArgs when --experimental-worker is used. +// Used in node_config.cc to set a constant on process.binding('config') +// that is used by lib/worker.js +bool config_experimental_worker = false; + // Set in node.cc by ParseArgs when --experimental-repl-await is used. // Used in node_config.cc to set a constant on process.binding('config') // that is used by lib/repl.js. @@ -1016,9 +1021,9 @@ void AppendExceptionLine(Environment* env, } -static void ReportException(Environment* env, - Local er, - Local message) { +void ReportException(Environment* env, + Local er, + Local message) { CHECK(!er.IsEmpty()); HandleScope scope(env->isolate()); @@ -1105,9 +1110,9 @@ static void ReportException(Environment* env, const TryCatch& try_catch) { // Executes a str within the current v8 context. -static Local ExecuteString(Environment* env, - Local source, - Local filename) { +static MaybeLocal ExecuteString(Environment* env, + Local source, + Local filename) { EscapableHandleScope scope(env->isolate()); TryCatch try_catch(env->isolate()); @@ -1120,13 +1125,19 @@ static Local ExecuteString(Environment* env, v8::Script::Compile(env->context(), source, &origin); if (script.IsEmpty()) { ReportException(env, try_catch); - exit(3); + env->Exit(3); + return MaybeLocal(); } MaybeLocal result = script.ToLocalChecked()->Run(env->context()); if (result.IsEmpty()) { + if (try_catch.HasTerminated()) { + env->isolate()->CancelTerminateExecution(); + return MaybeLocal(); + } ReportException(env, try_catch); - exit(4); + env->Exit(4); + return MaybeLocal(); } return scope.Escape(result.ToLocalChecked()); @@ -1225,6 +1236,7 @@ static void Abort(const FunctionCallbackInfo& args) { void Chdir(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + CHECK(env->is_main_thread()); CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsString()); @@ -1406,6 +1418,7 @@ static void GetEGid(const FunctionCallbackInfo& args) { void SetGid(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + CHECK(env->is_main_thread()); CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsUint32() || args[0]->IsString()); @@ -1425,6 +1438,7 @@ void SetGid(const FunctionCallbackInfo& args) { void SetEGid(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + CHECK(env->is_main_thread()); CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsUint32() || args[0]->IsString()); @@ -1444,6 +1458,7 @@ void SetEGid(const FunctionCallbackInfo& args) { void SetUid(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + CHECK(env->is_main_thread()); CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsUint32() || args[0]->IsString()); @@ -1463,6 +1478,7 @@ void SetUid(const FunctionCallbackInfo& args) { void SetEUid(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + CHECK(env->is_main_thread()); CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsUint32() || args[0]->IsString()); @@ -1624,9 +1640,10 @@ static void WaitForInspectorDisconnect(Environment* env) { static void Exit(const FunctionCallbackInfo& args) { - WaitForInspectorDisconnect(Environment::GetCurrent(args)); + Environment* env = Environment::GetCurrent(args); + WaitForInspectorDisconnect(env); v8_platform.StopTracingAgent(); - exit(args[0]->Int32Value()); + env->Exit(args[0]->Int32Value()); } @@ -2035,6 +2052,9 @@ void FatalException(Isolate* isolate, Local caught = fatal_exception_function->Call(process_object, 1, &error); + if (fatal_try_catch.HasTerminated()) + return; + if (fatal_try_catch.HasCaught()) { // The fatal exception function threw, so we must exit ReportException(env, fatal_try_catch); @@ -2048,6 +2068,12 @@ void FatalException(Isolate* isolate, void FatalException(Isolate* isolate, const TryCatch& try_catch) { + // If we try to print out a termination exception, we'd just get 'null', + // so just crashing here with that information seems like a better idea, + // and in particular it seems like we should handle terminations at the call + // site for this function rather than by printing them out somewhere. + CHECK(!try_catch.HasTerminated()); + HandleScope scope(isolate); if (!try_catch.IsVerbose()) { FatalException(isolate, try_catch.Exception(), try_catch.Message()); @@ -2569,11 +2595,12 @@ void SetupProcessObject(Environment* env, Local process = env->process_object(); auto title_string = FIXED_ONE_BYTE_STRING(env->isolate(), "title"); - CHECK(process->SetAccessor(env->context(), - title_string, - ProcessTitleGetter, - ProcessTitleSetter, - env->as_external()).FromJust()); + CHECK(process->SetAccessor( + env->context(), + title_string, + ProcessTitleGetter, + env->is_main_thread() ? ProcessTitleSetter : nullptr, + env->as_external()).FromJust()); // process.version READONLY_PROPERTY(process, @@ -2857,25 +2884,27 @@ void SetupProcessObject(Environment* env, CHECK(process->SetAccessor(env->context(), debug_port_string, DebugPortGetter, - DebugPortSetter, + env->is_main_thread() ? DebugPortSetter : nullptr, env->as_external()).FromJust()); // define various internal methods - env->SetMethod(process, - "_startProfilerIdleNotifier", - StartProfilerIdleNotifier); - env->SetMethod(process, - "_stopProfilerIdleNotifier", - StopProfilerIdleNotifier); + if (env->is_main_thread()) { + env->SetMethod(process, + "_startProfilerIdleNotifier", + StartProfilerIdleNotifier); + env->SetMethod(process, + "_stopProfilerIdleNotifier", + StopProfilerIdleNotifier); + env->SetMethod(process, "abort", Abort); + env->SetMethod(process, "chdir", Chdir); + env->SetMethod(process, "umask", Umask); + } + env->SetMethod(process, "_getActiveRequests", GetActiveRequests); env->SetMethod(process, "_getActiveHandles", GetActiveHandles); env->SetMethod(process, "reallyExit", Exit); - env->SetMethod(process, "abort", Abort); - env->SetMethod(process, "chdir", Chdir); env->SetMethod(process, "cwd", Cwd); - env->SetMethod(process, "umask", Umask); - #if defined(__POSIX__) && !defined(__ANDROID__) && !defined(__CloudABI__) env->SetMethod(process, "getuid", GetUid); env->SetMethod(process, "geteuid", GetEUid); @@ -2885,16 +2914,17 @@ void SetupProcessObject(Environment* env, #endif // __POSIX__ && !defined(__ANDROID__) && !defined(__CloudABI__) env->SetMethod(process, "_kill", Kill); + env->SetMethod(process, "dlopen", DLOpen); - env->SetMethod(process, "_debugProcess", DebugProcess); - env->SetMethod(process, "_debugEnd", DebugEnd); + if (env->is_main_thread()) { + env->SetMethod(process, "_debugProcess", DebugProcess); + env->SetMethod(process, "_debugEnd", DebugEnd); + } env->SetMethod(process, "hrtime", Hrtime); env->SetMethod(process, "cpuUsage", CPUUsage); - env->SetMethod(process, "dlopen", DLOpen); - env->SetMethod(process, "uptime", Uptime); env->SetMethod(process, "memoryUsage", MemoryUsage); } @@ -2930,8 +2960,10 @@ void RawDebug(const FunctionCallbackInfo& args) { } -static Local GetBootstrapper(Environment* env, Local source, - Local script_name) { +static MaybeLocal GetBootstrapper( + Environment* env, + Local source, + Local script_name) { EscapableHandleScope scope(env->isolate()); TryCatch try_catch(env->isolate()); @@ -2942,16 +2974,17 @@ static Local GetBootstrapper(Environment* env, Local source, try_catch.SetVerbose(false); // Execute the bootstrapper javascript file - Local bootstrapper_v = ExecuteString(env, source, script_name); + MaybeLocal bootstrapper_v = ExecuteString(env, source, script_name); + if (bootstrapper_v.IsEmpty()) // This happens when execution was interrupted. + return MaybeLocal(); + if (try_catch.HasCaught()) { ReportException(env, try_catch); exit(10); } - CHECK(bootstrapper_v->IsFunction()); - Local bootstrapper = Local::Cast(bootstrapper_v); - - return scope.Escape(bootstrapper); + CHECK(bootstrapper_v.ToLocalChecked()->IsFunction()); + return scope.Escape(bootstrapper_v.ToLocalChecked().As()); } static bool ExecuteBootstrapper(Environment* env, Local bootstrapper, @@ -2990,13 +3023,18 @@ void LoadEnvironment(Environment* env) { // node_js2c. Local loaders_name = FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/loaders.js"); - Local loaders_bootstrapper = + MaybeLocal loaders_bootstrapper = GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name); Local node_name = FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/node.js"); - Local node_bootstrapper = + MaybeLocal node_bootstrapper = GetBootstrapper(env, NodeBootstrapperSource(env), node_name); + if (loaders_bootstrapper.IsEmpty() || node_bootstrapper.IsEmpty()) { + // Execution was interrupted. + return; + } + // Add a reference to the global object Local global = env->context()->Global(); @@ -3044,7 +3082,7 @@ void LoadEnvironment(Environment* env) { // Bootstrap internal loaders Local bootstrapped_loaders; - if (!ExecuteBootstrapper(env, loaders_bootstrapper, + if (!ExecuteBootstrapper(env, loaders_bootstrapper.ToLocalChecked(), arraysize(loaders_bootstrapper_args), loaders_bootstrapper_args, &bootstrapped_loaders)) { @@ -3060,7 +3098,7 @@ void LoadEnvironment(Environment* env) { bootstrapper, bootstrapped_loaders }; - if (!ExecuteBootstrapper(env, node_bootstrapper, + if (!ExecuteBootstrapper(env, node_bootstrapper.ToLocalChecked(), arraysize(node_bootstrapper_args), node_bootstrapper_args, &bootstrapped_node)) { @@ -3094,6 +3132,7 @@ static void PrintHelp() { " --experimental-vm-modules experimental ES Module support\n" " in vm module\n" #endif // defined(NODE_HAVE_I18N_SUPPORT) + " --experimental-worker experimental threaded Worker support\n" #if HAVE_OPENSSL && NODE_FIPS_MODE " --force-fips force FIPS crypto (cannot be disabled)\n" #endif // HAVE_OPENSSL && NODE_FIPS_MODE @@ -3257,6 +3296,7 @@ static void CheckIfAllowedInEnv(const char* exe, bool is_env, "--experimental-modules", "--experimental-repl-await", "--experimental-vm-modules", + "--experimental-worker", "--force-fips", "--icu-data-dir", "--inspect", @@ -3454,6 +3494,8 @@ static void ParseArgs(int* argc, new_v8_argc += 1; } else if (strcmp(arg, "--experimental-vm-modules") == 0) { config_experimental_vm_modules = true; + } else if (strcmp(arg, "--experimental-worker") == 0) { + config_experimental_worker = true; } else if (strcmp(arg, "--experimental-repl-await") == 0) { config_experimental_repl_await = true; } else if (strcmp(arg, "--loader") == 0) { @@ -4270,6 +4312,7 @@ inline int Start(Isolate* isolate, IsolateData* isolate_data, WaitForInspectorDisconnect(&env); env.set_can_call_into_js(false); + env.stop_sub_worker_contexts(); env.RunCleanup(); RunAtExit(&env); diff --git a/src/node_config.cc b/src/node_config.cc index 603d55491a259b..dd5ee666486874 100644 --- a/src/node_config.cc +++ b/src/node_config.cc @@ -91,6 +91,9 @@ static void Initialize(Local target, if (config_experimental_vm_modules) READONLY_BOOLEAN_PROPERTY("experimentalVMModules"); + if (config_experimental_worker) + READONLY_BOOLEAN_PROPERTY("experimentalWorker"); + if (config_experimental_repl_await) READONLY_BOOLEAN_PROPERTY("experimentalREPLAwait"); diff --git a/src/node_errors.h b/src/node_errors.h index b2f2b256c4c120..2c97088cc553b4 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -23,15 +23,22 @@ namespace node { #define ERRORS_WITH_CODE(V) \ V(ERR_BUFFER_OUT_OF_BOUNDS, RangeError) \ V(ERR_BUFFER_TOO_LARGE, Error) \ + V(ERR_CANNOT_TRANSFER_OBJECT, TypeError) \ + V(ERR_CLOSED_MESSAGE_PORT, Error) \ + V(ERR_CONSTRUCT_CALL_REQUIRED, Error) \ V(ERR_INDEX_OUT_OF_RANGE, RangeError) \ V(ERR_INVALID_ARG_VALUE, TypeError) \ V(ERR_INVALID_ARG_TYPE, TypeError) \ + V(ERR_INVALID_TRANSFER_OBJECT, TypeError) \ V(ERR_MEMORY_ALLOCATION_FAILED, Error) \ V(ERR_MISSING_ARGS, TypeError) \ + V(ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST, TypeError) \ V(ERR_MISSING_MODULE, Error) \ + V(ERR_MISSING_PLATFORM_FOR_WORKER, Error) \ V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \ V(ERR_SCRIPT_EXECUTION_TIMEOUT, Error) \ V(ERR_STRING_TOO_LONG, Error) \ + V(ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER, TypeError) \ #define V(code, type) \ inline v8::Local code(v8::Isolate* isolate, \ @@ -54,10 +61,21 @@ namespace node { // Errors with predefined static messages #define PREDEFINED_ERROR_MESSAGES(V) \ + V(ERR_CANNOT_TRANSFER_OBJECT, "Cannot transfer object of unsupported type")\ + V(ERR_CLOSED_MESSAGE_PORT, "Cannot send data on closed MessagePort") \ + V(ERR_CONSTRUCT_CALL_REQUIRED, "Cannot call constructor without `new`") \ V(ERR_INDEX_OUT_OF_RANGE, "Index out of range") \ + V(ERR_INVALID_TRANSFER_OBJECT, "Found invalid object in transferList") \ V(ERR_MEMORY_ALLOCATION_FAILED, "Failed to allocate memory") \ + V(ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST, \ + "MessagePort was found in message but not listed in transferList") \ + V(ERR_MISSING_PLATFORM_FOR_WORKER, \ + "The V8 platform used by this instance of Node does not support " \ + "creating Workers") \ V(ERR_SCRIPT_EXECUTION_INTERRUPTED, \ - "Script execution was interrupted by `SIGINT`") + "Script execution was interrupted by `SIGINT`") \ + V(ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER, \ + "Cannot serialize externalized SharedArrayBuffer") \ #define V(code, message) \ inline v8::Local code(v8::Isolate* isolate) { \ diff --git a/src/node_internals.h b/src/node_internals.h index 3014a0e5f7a442..7760eb26c6c15c 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -114,6 +114,7 @@ struct sockaddr; V(http_parser) \ V(inspector) \ V(js_stream) \ + V(messaging) \ V(module_wrap) \ V(os) \ V(performance) \ @@ -125,6 +126,7 @@ struct sockaddr; V(stream_pipe) \ V(stream_wrap) \ V(string_decoder) \ + V(symbols) \ V(tcp_wrap) \ V(timer_wrap) \ V(trace_events) \ @@ -135,6 +137,7 @@ struct sockaddr; V(util) \ V(uv) \ V(v8) \ + V(worker) \ V(zlib) #define NODE_BUILTIN_MODULES(V) \ @@ -188,6 +191,11 @@ extern bool config_experimental_modules; // that is used by lib/vm.js extern bool config_experimental_vm_modules; +// Set in node.cc by ParseArgs when --experimental-vm-modules is used. +// Used in node_config.cc to set a constant on process.binding('config') +// that is used by lib/vm.js +extern bool config_experimental_worker; + // Set in node.cc by ParseArgs when --experimental-repl-await is used. // Used in node_config.cc to set a constant on process.binding('config') // that is used by lib/repl.js. @@ -307,6 +315,10 @@ class FatalTryCatch : public v8::TryCatch { Environment* env_; }; +void ReportException(Environment* env, + v8::Local er, + v8::Local message); + v8::Maybe ProcessEmitWarning(Environment* env, const char* fmt, ...); v8::Maybe ProcessEmitDeprecationWarning(Environment* env, const char* warning, diff --git a/src/node_messaging.cc b/src/node_messaging.cc new file mode 100644 index 00000000000000..352749ea48f483 --- /dev/null +++ b/src/node_messaging.cc @@ -0,0 +1,690 @@ +#include "node_messaging.h" +#include "node_internals.h" +#include "node_buffer.h" +#include "node_errors.h" +#include "util.h" +#include "util-inl.h" +#include "async_wrap.h" +#include "async_wrap-inl.h" + +using v8::Array; +using v8::ArrayBuffer; +using v8::ArrayBufferCreationMode; +using v8::Context; +using v8::EscapableHandleScope; +using v8::Exception; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Isolate; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Nothing; +using v8::Object; +using v8::SharedArrayBuffer; +using v8::String; +using v8::Value; +using v8::ValueDeserializer; +using v8::ValueSerializer; + +namespace node { +namespace worker { + +Message::Message(MallocedBuffer&& buffer) + : main_message_buf_(std::move(buffer)) {} + +namespace { + +// This is used to tell V8 how to read transferred host objects, like other +// `MessagePort`s and `SharedArrayBuffer`s, and make new JS objects out of them. +class DeserializerDelegate : public ValueDeserializer::Delegate { + public: + DeserializerDelegate(Message* m, + Environment* env, + const std::vector& message_ports, + const std::vector>& + shared_array_buffers) + : message_ports_(message_ports), + shared_array_buffers_(shared_array_buffers) {} + + MaybeLocal ReadHostObject(Isolate* isolate) override { + // Currently, only MessagePort hosts objects are supported, so identifying + // by the index in the message's MessagePort array is sufficient. + uint32_t id; + if (!deserializer->ReadUint32(&id)) + return MaybeLocal(); + CHECK_LE(id, message_ports_.size()); + return message_ports_[id]->object(isolate); + }; + + MaybeLocal GetSharedArrayBufferFromId( + Isolate* isolate, uint32_t clone_id) override { + CHECK_LE(clone_id, shared_array_buffers_.size()); + return shared_array_buffers_[clone_id]; + } + + ValueDeserializer* deserializer = nullptr; + + private: + const std::vector& message_ports_; + const std::vector>& shared_array_buffers_; +}; + +} // anonymous namespace + +MaybeLocal Message::Deserialize(Environment* env, + Local context) { + EscapableHandleScope handle_scope(env->isolate()); + Context::Scope context_scope(context); + + // Create all necessary MessagePort handles. + std::vector ports(message_ports_.size()); + for (uint32_t i = 0; i < message_ports_.size(); ++i) { + ports[i] = MessagePort::New(env, + context, + std::move(message_ports_[i])); + if (ports[i] == nullptr) { + for (MessagePort* port : ports) { + // This will eventually release the MessagePort object itself. + port->Close(); + } + return MaybeLocal(); + } + } + message_ports_.clear(); + + std::vector> shared_array_buffers; + // Attach all transfered SharedArrayBuffers to their new Isolate. + for (uint32_t i = 0; i < shared_array_buffers_.size(); ++i) { + Local sab; + if (!shared_array_buffers_[i]->GetSharedArrayBuffer(env, context) + .ToLocal(&sab)) + return MaybeLocal(); + shared_array_buffers.push_back(sab); + } + shared_array_buffers_.clear(); + + DeserializerDelegate delegate(this, env, ports, shared_array_buffers); + ValueDeserializer deserializer( + env->isolate(), + reinterpret_cast(main_message_buf_.data), + main_message_buf_.size, + &delegate); + delegate.deserializer = &deserializer; + + // Attach all transfered ArrayBuffers to their new Isolate. + for (uint32_t i = 0; i < array_buffer_contents_.size(); ++i) { + Local ab = + ArrayBuffer::New(env->isolate(), + array_buffer_contents_[i].release(), + array_buffer_contents_[i].size, + ArrayBufferCreationMode::kInternalized); + deserializer.TransferArrayBuffer(i, ab); + } + array_buffer_contents_.clear(); + + if (deserializer.ReadHeader(context).IsNothing()) + return MaybeLocal(); + return handle_scope.Escape( + deserializer.ReadValue(context).FromMaybe(Local())); +} + +void Message::AddSharedArrayBuffer( + SharedArrayBufferMetadataReference reference) { + shared_array_buffers_.push_back(reference); +} + +void Message::AddMessagePort(std::unique_ptr&& data) { + message_ports_.emplace_back(std::move(data)); +} + +namespace { + +// This tells V8 how to serialize objects that it does not understand +// (e.g. C++ objects) into the output buffer, in a way that our own +// DeserializerDelegate understands how to unpack. +class SerializerDelegate : public ValueSerializer::Delegate { + public: + SerializerDelegate(Environment* env, Local context, Message* m) + : env_(env), context_(context), msg_(m) {} + + void ThrowDataCloneError(Local message) override { + env_->isolate()->ThrowException(Exception::Error(message)); + } + + Maybe WriteHostObject(Isolate* isolate, Local object) override { + if (env_->message_port_constructor_template()->HasInstance(object)) { + return WriteMessagePort(Unwrap(object)); + } + + THROW_ERR_CANNOT_TRANSFER_OBJECT(env_); + return Nothing(); + } + + Maybe GetSharedArrayBufferId( + Isolate* isolate, + Local shared_array_buffer) override { + uint32_t i; + for (i = 0; i < seen_shared_array_buffers_.size(); ++i) { + if (seen_shared_array_buffers_[i] == shared_array_buffer) + return Just(i); + } + + auto reference = SharedArrayBufferMetadata::ForSharedArrayBuffer( + env_, + context_, + shared_array_buffer); + if (!reference) { + return Nothing(); + } + seen_shared_array_buffers_.push_back(shared_array_buffer); + msg_->AddSharedArrayBuffer(reference); + return Just(i); + } + + void Finish() { + // Only close the MessagePort handles and actually transfer them + // once we know that serialization succeeded. + for (MessagePort* port : ports_) { + port->Close(); + msg_->AddMessagePort(port->Detach()); + } + } + + ValueSerializer* serializer = nullptr; + + private: + Maybe WriteMessagePort(MessagePort* port) { + for (uint32_t i = 0; i < ports_.size(); i++) { + if (ports_[i] == port) { + serializer->WriteUint32(i); + return Just(true); + } + } + + THROW_ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST(env_); + return Nothing(); + } + + Environment* env_; + Local context_; + Message* msg_; + std::vector> seen_shared_array_buffers_; + std::vector ports_; + + friend class worker::Message; +}; + +} // anynomous namespace + +Maybe Message::Serialize(Environment* env, + Local context, + Local input, + Local transfer_list_v) { + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(context); + + // Verify that we're not silently overwriting an existing message. + CHECK(main_message_buf_.is_empty()); + + SerializerDelegate delegate(env, context, this); + ValueSerializer serializer(env->isolate(), &delegate); + delegate.serializer = &serializer; + + std::vector> array_buffers; + if (transfer_list_v->IsArray()) { + Local transfer_list = transfer_list_v.As(); + uint32_t length = transfer_list->Length(); + for (uint32_t i = 0; i < length; ++i) { + Local entry; + if (!transfer_list->Get(context, i).ToLocal(&entry)) + return Nothing(); + // Currently, we support ArrayBuffers and MessagePorts. + if (entry->IsArrayBuffer()) { + Local ab = entry.As(); + // If we cannot render the ArrayBuffer unusable in this Isolate and + // take ownership of its memory, copying the buffer will have to do. + if (!ab->IsNeuterable() || ab->IsExternal()) + continue; + // We simply use the array index in the `array_buffers` list as the + // ID that we write into the serialized buffer. + uint32_t id = array_buffers.size(); + array_buffers.push_back(ab); + serializer.TransferArrayBuffer(id, ab); + continue; + } else if (env->message_port_constructor_template() + ->HasInstance(entry)) { + MessagePort* port = Unwrap(entry.As()); + CHECK_NE(port, nullptr); + delegate.ports_.push_back(port); + continue; + } + + THROW_ERR_INVALID_TRANSFER_OBJECT(env); + return Nothing(); + } + } + + serializer.WriteHeader(); + if (serializer.WriteValue(context, input).IsNothing()) { + return Nothing(); + } + + for (Local ab : array_buffers) { + // If serialization succeeded, we want to take ownership of + // (a.k.a. externalize) the underlying memory region and render + // it inaccessible in this Isolate. + ArrayBuffer::Contents contents = ab->Externalize(); + ab->Neuter(); + array_buffer_contents_.push_back( + MallocedBuffer { static_cast(contents.Data()), + contents.ByteLength() }); + } + + delegate.Finish(); + + // The serializer gave us a buffer allocated using `malloc()`. + std::pair data = serializer.Release(); + main_message_buf_ = + MallocedBuffer(reinterpret_cast(data.first), data.second); + return Just(true); +} + +MessagePortData::MessagePortData(MessagePort* owner) : owner_(owner) { } + +MessagePortData::~MessagePortData() { + CHECK_EQ(owner_, nullptr); + Disentangle(); +} + +void MessagePortData::AddToIncomingQueue(Message&& message) { + // This function will be called by other threads. + Mutex::ScopedLock lock(mutex_); + incoming_messages_.emplace_back(std::move(message)); + + if (owner_ != nullptr) + owner_->TriggerAsync(); +} + +bool MessagePortData::IsSiblingClosed() const { + Mutex::ScopedLock lock(*sibling_mutex_); + return sibling_ == nullptr; +} + +void MessagePortData::Entangle(MessagePortData* a, MessagePortData* b) { + CHECK_EQ(a->sibling_, nullptr); + CHECK_EQ(b->sibling_, nullptr); + a->sibling_ = b; + b->sibling_ = a; + a->sibling_mutex_ = b->sibling_mutex_; +} + +void MessagePortData::PingOwnerAfterDisentanglement() { + Mutex::ScopedLock lock(mutex_); + if (owner_ != nullptr) + owner_->TriggerAsync(); +} + +void MessagePortData::Disentangle() { + // Grab a copy of the sibling mutex, then replace it so that each sibling + // has its own sibling_mutex_ now. + std::shared_ptr sibling_mutex = sibling_mutex_; + Mutex::ScopedLock sibling_lock(*sibling_mutex); + sibling_mutex_ = std::make_shared(); + + MessagePortData* sibling = sibling_; + if (sibling_ != nullptr) { + sibling_->sibling_ = nullptr; + sibling_ = nullptr; + } + + // We close MessagePorts after disentanglement, so we trigger the + // corresponding uv_async_t to let them know that this happened. + PingOwnerAfterDisentanglement(); + if (sibling != nullptr) { + sibling->PingOwnerAfterDisentanglement(); + } +} + +MessagePort::~MessagePort() { + if (data_) + data_->owner_ = nullptr; +} + +MessagePort::MessagePort(Environment* env, + Local context, + Local wrap) + : HandleWrap(env, + wrap, + reinterpret_cast(new uv_async_t()), + AsyncWrap::PROVIDER_MESSAGEPORT), + data_(new MessagePortData(this)) { + auto onmessage = [](uv_async_t* handle) { + // Called when data has been put into the queue. + MessagePort* channel = static_cast(handle->data); + channel->OnMessage(); + }; + CHECK_EQ(uv_async_init(env->event_loop(), + async(), + onmessage), 0); + async()->data = static_cast(this); + + Local fn; + if (!wrap->Get(context, env->oninit_string()).ToLocal(&fn)) + return; + + if (fn->IsFunction()) { + Local init = fn.As(); + USE(init->Call(context, wrap, 0, nullptr)); + } +} + +void MessagePort::AddToIncomingQueue(Message&& message) { + data_->AddToIncomingQueue(std::move(message)); +} + +uv_async_t* MessagePort::async() { + return reinterpret_cast(GetHandle()); +} + +void MessagePort::TriggerAsync() { + CHECK_EQ(uv_async_send(async()), 0); +} + +void MessagePort::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + if (!args.IsConstructCall()) { + THROW_ERR_CONSTRUCT_CALL_REQUIRED(env); + return; + } + + Local context = args.This()->CreationContext(); + Context::Scope context_scope(context); + + new MessagePort(env, context, args.This()); +} + +MessagePort* MessagePort::New( + Environment* env, + Local context, + std::unique_ptr data) { + Context::Scope context_scope(context); + Local ctor; + if (!GetMessagePortConstructor(env, context).ToLocal(&ctor)) + return nullptr; + MessagePort* port = nullptr; + + // Construct a new instance, then assign the listener instance and possibly + // the MessagePortData to it. + Local instance; + if (!ctor->NewInstance(context).ToLocal(&instance)) + return nullptr; + ASSIGN_OR_RETURN_UNWRAP(&port, instance, nullptr); + if (data) { + port->Detach(); + port->data_ = std::move(data); + port->data_->owner_ = port; + // If the existing MessagePortData object had pending messages, this is + // the easiest way to run that queue. + port->TriggerAsync(); + } + return port; +} + +void MessagePort::OnMessage() { + HandleScope handle_scope(env()->isolate()); + Local context = object(env()->isolate())->CreationContext(); + + // data_ can only ever be modified by the owner thread, so no need to lock. + // However, the message port may be transferred while it is processing + // messages, so we need to check that this handle still owns its `data_` field + // on every iteration. + while (data_) { + Message received; + { + // Get the head of the message queue. + Mutex::ScopedLock lock(data_->mutex_); + + if (stop_event_loop_) { + CHECK(!data_->receiving_messages_); + uv_stop(env()->event_loop()); + break; + } + + if (!data_->receiving_messages_) + break; + if (data_->incoming_messages_.empty()) + break; + received = std::move(data_->incoming_messages_.front()); + data_->incoming_messages_.pop_front(); + } + + if (!env()->can_call_into_js()) { + // In this case there is nothing to do but to drain the current queue. + continue; + } + + { + // Call the JS .onmessage() callback. + HandleScope handle_scope(env()->isolate()); + Context::Scope context_scope(context); + Local args[] = { + received.Deserialize(env(), context).FromMaybe(Local()) + }; + + if (args[0].IsEmpty() || + !object()->Has(context, env()->onmessage_string()).FromMaybe(false) || + MakeCallback(env()->onmessage_string(), 1, args).IsEmpty()) { + // Re-schedule OnMessage() execution in case of failure. + if (data_) + TriggerAsync(); + return; + } + } + } + + if (data_ && data_->IsSiblingClosed()) { + Close(); + } +} + +bool MessagePort::IsSiblingClosed() const { + CHECK(data_); + return data_->IsSiblingClosed(); +} + +void MessagePort::OnClose() { + if (data_) { + data_->owner_ = nullptr; + data_->Disentangle(); + } + data_.reset(); + delete async(); +} + +std::unique_ptr MessagePort::Detach() { + Mutex::ScopedLock lock(data_->mutex_); + data_->owner_ = nullptr; + return std::move(data_); +} + + +void MessagePort::Send(Message&& message) { + Mutex::ScopedLock lock(*data_->sibling_mutex_); + if (data_->sibling_ == nullptr) + return; + data_->sibling_->AddToIncomingQueue(std::move(message)); +} + +void MessagePort::Send(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Local context = object(env->isolate())->CreationContext(); + Message msg; + if (msg.Serialize(env, context, args[0], args[1]) + .IsNothing()) { + return; + } + Send(std::move(msg)); +} + +void MessagePort::PostMessage(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + MessagePort* port; + ASSIGN_OR_RETURN_UNWRAP(&port, args.This()); + if (!port->data_) { + return THROW_ERR_CLOSED_MESSAGE_PORT(env); + } + if (args.Length() == 0) { + return THROW_ERR_MISSING_ARGS(env, "Not enough arguments to " + "MessagePort.postMessage"); + } + port->Send(args); +} + +void MessagePort::Start() { + Mutex::ScopedLock lock(data_->mutex_); + data_->receiving_messages_ = true; + if (!data_->incoming_messages_.empty()) + TriggerAsync(); +} + +void MessagePort::Stop() { + Mutex::ScopedLock lock(data_->mutex_); + data_->receiving_messages_ = false; +} + +void MessagePort::StopEventLoop() { + Mutex::ScopedLock lock(data_->mutex_); + data_->receiving_messages_ = false; + stop_event_loop_ = true; + + TriggerAsync(); +} + +void MessagePort::Start(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + MessagePort* port; + ASSIGN_OR_RETURN_UNWRAP(&port, args.This()); + if (!port->data_) { + THROW_ERR_CLOSED_MESSAGE_PORT(env); + return; + } + port->Start(); +} + +void MessagePort::Stop(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + MessagePort* port; + ASSIGN_OR_RETURN_UNWRAP(&port, args.This()); + if (!port->data_) { + THROW_ERR_CLOSED_MESSAGE_PORT(env); + return; + } + port->Stop(); +} + +void MessagePort::Drain(const FunctionCallbackInfo& args) { + MessagePort* port; + ASSIGN_OR_RETURN_UNWRAP(&port, args.This()); + port->OnMessage(); +} + +size_t MessagePort::self_size() const { + Mutex::ScopedLock lock(data_->mutex_); + size_t sz = sizeof(*this) + sizeof(*data_); + for (const Message& msg : data_->incoming_messages_) + sz += sizeof(msg) + msg.main_message_buf_.size; + return sz; +} + +void MessagePort::Entangle(MessagePort* a, MessagePort* b) { + Entangle(a, b->data_.get()); +} + +void MessagePort::Entangle(MessagePort* a, MessagePortData* b) { + MessagePortData::Entangle(a->data_.get(), b); +} + +MaybeLocal GetMessagePortConstructor( + Environment* env, Local context) { + // Factor generating the MessagePort JS constructor into its own piece + // of code, because it is needed early on in the child environment setup. + Local templ = env->message_port_constructor_template(); + if (!templ.IsEmpty()) + return templ->GetFunction(context); + + { + Local m = env->NewFunctionTemplate(MessagePort::New); + m->SetClassName(env->message_port_constructor_string()); + m->InstanceTemplate()->SetInternalFieldCount(1); + + AsyncWrap::AddWrapMethods(env, m); + + env->SetProtoMethod(m, "postMessage", MessagePort::PostMessage); + env->SetProtoMethod(m, "start", MessagePort::Start); + env->SetProtoMethod(m, "stop", MessagePort::Stop); + env->SetProtoMethod(m, "drain", MessagePort::Drain); + env->SetProtoMethod(m, "close", HandleWrap::Close); + env->SetProtoMethod(m, "unref", HandleWrap::Unref); + env->SetProtoMethod(m, "ref", HandleWrap::Ref); + env->SetProtoMethod(m, "hasRef", HandleWrap::HasRef); + + env->set_message_port_constructor_template(m); + } + + return GetMessagePortConstructor(env, context); +} + +namespace { + +static void MessageChannel(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + if (!args.IsConstructCall()) { + THROW_ERR_CONSTRUCT_CALL_REQUIRED(env); + return; + } + + Local context = args.This()->CreationContext(); + Context::Scope context_scope(context); + + MessagePort* port1 = MessagePort::New(env, context); + MessagePort* port2 = MessagePort::New(env, context); + MessagePort::Entangle(port1, port2); + + args.This()->Set(env->context(), env->port1_string(), port1->object()) + .FromJust(); + args.This()->Set(env->context(), env->port2_string(), port2->object()) + .FromJust(); +} + +static void InitMessaging(Local target, + Local unused, + Local context, + void* priv) { + Environment* env = Environment::GetCurrent(context); + + { + Local message_channel_string = + FIXED_ONE_BYTE_STRING(env->isolate(), "MessageChannel"); + Local templ = env->NewFunctionTemplate(MessageChannel); + templ->SetClassName(message_channel_string); + target->Set(env->context(), + message_channel_string, + templ->GetFunction(context).ToLocalChecked()).FromJust(); + } + + target->Set(context, + env->message_port_constructor_string(), + GetMessagePortConstructor(env, context).ToLocalChecked()) + .FromJust(); +} + +} // anonymous namespace + +} // namespace worker +} // namespace node + +NODE_MODULE_CONTEXT_AWARE_INTERNAL(messaging, node::worker::InitMessaging) diff --git a/src/node_messaging.h b/src/node_messaging.h new file mode 100644 index 00000000000000..9a13437d19a331 --- /dev/null +++ b/src/node_messaging.h @@ -0,0 +1,181 @@ +#ifndef SRC_NODE_MESSAGING_H_ +#define SRC_NODE_MESSAGING_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "env.h" +#include "node_mutex.h" +#include "sharedarraybuffer_metadata.h" +#include + +namespace node { +namespace worker { + +class MessagePortData; +class MessagePort; + +// Represents a single communication message. +class Message { + public: + explicit Message(MallocedBuffer&& payload = MallocedBuffer()); + + Message(Message&& other) = default; + Message& operator=(Message&& other) = default; + Message& operator=(const Message&) = delete; + Message(const Message&) = delete; + + // Deserialize the contained JS value. May only be called once, and only + // after Serialize() has been called (e.g. by another thread). + v8::MaybeLocal Deserialize(Environment* env, + v8::Local context); + + // Serialize a JS value, and optionally transfer objects, into this message. + // The Message object retains ownership of all transferred objects until + // deserialization. + v8::Maybe Serialize(Environment* env, + v8::Local context, + v8::Local input, + v8::Local transfer_list); + + // Internal method of Message that is called when a new SharedArrayBuffer + // object is encountered in the incoming value's structure. + void AddSharedArrayBuffer(SharedArrayBufferMetadataReference ref); + // Internal method of Message that is called once serialization finishes + // and that transfers ownership of `data` to this message. + void AddMessagePort(std::unique_ptr&& data); + + private: + MallocedBuffer main_message_buf_; + std::vector> array_buffer_contents_; + std::vector shared_array_buffers_; + std::vector> message_ports_; + + friend class MessagePort; +}; + +// This contains all data for a `MessagePort` instance that is not tied to +// a specific Environment/Isolate/event loop, for easier transfer between those. +class MessagePortData { + public: + explicit MessagePortData(MessagePort* owner); + ~MessagePortData(); + + MessagePortData(MessagePortData&& other) = delete; + MessagePortData& operator=(MessagePortData&& other) = delete; + MessagePortData(const MessagePortData& other) = delete; + MessagePortData& operator=(const MessagePortData& other) = delete; + + // Add a message to the incoming queue and notify the receiver. + // This may be called from any thread. + void AddToIncomingQueue(Message&& message); + + // Returns true if and only this MessagePort is currently not entangled + // with another message port. + bool IsSiblingClosed() const; + + // Turns `a` and `b` into siblings, i.e. connects the sending side of one + // to the receiving side of the other. This is not thread-safe. + static void Entangle(MessagePortData* a, MessagePortData* b); + + // Removes any possible sibling. This is thread-safe (it acquires both + // `sibling_mutex_` and `mutex_`), and has to be because it is called once + // the corresponding JS handle handle wants to close + // which can happen on either side of a worker. + void Disentangle(); + + private: + // After disentangling this message port, the owner handle (if any) + // is asynchronously triggered, so that it can close down naturally. + void PingOwnerAfterDisentanglement(); + + // This mutex protects all fields below it, with the exception of + // sibling_. + mutable Mutex mutex_; + bool receiving_messages_ = false; + std::list incoming_messages_; + MessagePort* owner_ = nullptr; + // This mutex protects the sibling_ field and is shared between two entangled + // MessagePorts. If both mutexes are acquired, this one needs to be + // acquired first. + std::shared_ptr sibling_mutex_ = std::make_shared(); + MessagePortData* sibling_ = nullptr; + + friend class MessagePort; +}; + +// A message port that receives messages from other threads, including +// the uv_async_t handle that is used to notify the current event loop of +// new incoming messages. +class MessagePort : public HandleWrap { + public: + // Create a new MessagePort. The `context` argument specifies the Context + // instance that is used for creating the values emitted from this port. + MessagePort(Environment* env, + v8::Local context, + v8::Local wrap); + ~MessagePort(); + + // Create a new message port instance, optionally over an existing + // `MessagePortData` object. + static MessagePort* New(Environment* env, + v8::Local context, + std::unique_ptr data = nullptr); + + // Send a message, i.e. deliver it into the sibling's incoming queue. + // If there is no sibling, i.e. this port is closed, + // this message is silently discarded. + void Send(Message&& message); + void Send(const v8::FunctionCallbackInfo& args); + // Deliver a single message into this port's incoming queue. + void AddToIncomingQueue(Message&& message); + + // Start processing messages on this port as a receiving end. + void Start(); + // Stop processing messages on this port as a receiving end. + void Stop(); + // Stop processing messages on this port as a receiving end, + // and stop the event loop that this port is associated with. + void StopEventLoop(); + + static void New(const v8::FunctionCallbackInfo& args); + static void PostMessage(const v8::FunctionCallbackInfo& args); + static void Start(const v8::FunctionCallbackInfo& args); + static void Stop(const v8::FunctionCallbackInfo& args); + static void Drain(const v8::FunctionCallbackInfo& args); + + // Turns `a` and `b` into siblings, i.e. connects the sending side of one + // to the receiving side of the other. This is not thread-safe. + static void Entangle(MessagePort* a, MessagePort* b); + static void Entangle(MessagePort* a, MessagePortData* b); + + // Detach this port's data for transferring. After this, the MessagePortData + // is no longer associated with this handle, although it can still receive + // messages. + std::unique_ptr Detach(); + + bool IsSiblingClosed() const; + + size_t self_size() const override; + + private: + void OnClose() override; + void OnMessage(); + void TriggerAsync(); + inline uv_async_t* async(); + + std::unique_ptr data_ = nullptr; + bool stop_event_loop_ = false; + + friend class MessagePortData; +}; + +v8::MaybeLocal GetMessagePortConstructor( + Environment* env, v8::Local context); + +} // namespace worker +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + + +#endif // SRC_NODE_MESSAGING_H_ diff --git a/src/node_platform.cc b/src/node_platform.cc index 2885c72ed71213..f2407f5be2d3c8 100644 --- a/src/node_platform.cc +++ b/src/node_platform.cc @@ -65,7 +65,7 @@ size_t BackgroundTaskRunner::NumberOfAvailableBackgroundThreads() const { PerIsolatePlatformData::PerIsolatePlatformData( v8::Isolate* isolate, uv_loop_t* loop) - : isolate_(isolate), loop_(loop) { + : loop_(loop) { flush_tasks_ = new uv_async_t(); CHECK_EQ(0, uv_async_init(loop, flush_tasks_, FlushTasks)); flush_tasks_->data = static_cast(this); @@ -82,12 +82,14 @@ void PerIsolatePlatformData::PostIdleTask(std::unique_ptr task) { } void PerIsolatePlatformData::PostTask(std::unique_ptr task) { + CHECK_NE(flush_tasks_, nullptr); foreground_tasks_.Push(std::move(task)); uv_async_send(flush_tasks_); } void PerIsolatePlatformData::PostDelayedTask( std::unique_ptr task, double delay_in_seconds) { + CHECK_NE(flush_tasks_, nullptr); std::unique_ptr delayed(new DelayedTask()); delayed->task = std::move(task); delayed->platform_data = shared_from_this(); @@ -97,6 +99,13 @@ void PerIsolatePlatformData::PostDelayedTask( } PerIsolatePlatformData::~PerIsolatePlatformData() { + Shutdown(); +} + +void PerIsolatePlatformData::Shutdown() { + if (flush_tasks_ == nullptr) + return; + while (FlushForegroundTasksInternal()) {} CancelPendingDelayedTasks(); @@ -104,6 +113,7 @@ PerIsolatePlatformData::~PerIsolatePlatformData() { [](uv_handle_t* handle) { delete reinterpret_cast(handle); }); + flush_tasks_ = nullptr; } void PerIsolatePlatformData::ref() { @@ -144,6 +154,7 @@ void NodePlatform::UnregisterIsolate(IsolateData* isolate_data) { std::shared_ptr existing = per_isolate_[isolate]; CHECK(existing); if (existing->unref() == 0) { + existing->Shutdown(); per_isolate_.erase(isolate); } } diff --git a/src/node_platform.h b/src/node_platform.h index 8f6ff89f491fe3..cf0809ad1f673b 100644 --- a/src/node_platform.h +++ b/src/node_platform.h @@ -80,7 +80,6 @@ class PerIsolatePlatformData : static void RunForegroundTask(uv_timer_t* timer); int ref_count_ = 1; - v8::Isolate* isolate_; uv_loop_t* const loop_; uv_async_t* flush_tasks_ = nullptr; TaskQueue foreground_tasks_; diff --git a/src/node_worker.cc b/src/node_worker.cc new file mode 100644 index 00000000000000..366dca353d345c --- /dev/null +++ b/src/node_worker.cc @@ -0,0 +1,428 @@ +#include "node_worker.h" +#include "node_errors.h" +#include "node_internals.h" +#include "node_buffer.h" +#include "node_perf.h" +#include "util.h" +#include "util-inl.h" +#include "async_wrap.h" +#include "async_wrap-inl.h" + +using v8::ArrayBuffer; +using v8::Context; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Integer; +using v8::Isolate; +using v8::Local; +using v8::Locker; +using v8::Number; +using v8::Object; +using v8::SealHandleScope; +using v8::String; +using v8::Value; + +namespace node { +namespace worker { + +namespace { + +double next_thread_id = 1; +Mutex next_thread_id_mutex; + +} // anonymous namespace + +Worker::Worker(Environment* env, Local wrap) + : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_WORKER) { + // Generate a new thread id. + { + Mutex::ScopedLock next_thread_id_lock(next_thread_id_mutex); + thread_id_ = next_thread_id++; + } + wrap->Set(env->context(), + env->thread_id_string(), + Number::New(env->isolate(), thread_id_)).FromJust(); + + // Set up everything that needs to be set up in the parent environment. + parent_port_ = MessagePort::New(env, env->context()); + if (parent_port_ == nullptr) { + // This can happen e.g. because execution is terminating. + return; + } + + child_port_data_.reset(new MessagePortData(nullptr)); + MessagePort::Entangle(parent_port_, child_port_data_.get()); + + object()->Set(env->context(), + env->message_port_string(), + parent_port_->object()).FromJust(); + + array_buffer_allocator_.reset(CreateArrayBufferAllocator()); + + isolate_ = NewIsolate(array_buffer_allocator_.get()); + CHECK_NE(isolate_, nullptr); + CHECK_EQ(uv_loop_init(&loop_), 0); + + thread_exit_async_.reset(new uv_async_t); + thread_exit_async_->data = this; + CHECK_EQ(uv_async_init(env->event_loop(), + thread_exit_async_.get(), + [](uv_async_t* handle) { + static_cast(handle->data)->OnThreadStopped(); + }), 0); + + { + // Enter an environment capable of executing code in the child Isolate + // (and only in it). + Locker locker(isolate_); + Isolate::Scope isolate_scope(isolate_); + HandleScope handle_scope(isolate_); + + isolate_data_.reset(CreateIsolateData(isolate_, + &loop_, + env->isolate_data()->platform(), + array_buffer_allocator_.get())); + CHECK(isolate_data_); + + Local context = NewContext(isolate_); + Context::Scope context_scope(context); + + // TODO(addaleax): Use CreateEnvironment(), or generally another public API. + env_.reset(new Environment(isolate_data_.get(), + context, + nullptr)); + CHECK_NE(env_, nullptr); + env_->set_abort_on_uncaught_exception(false); + env_->set_worker_context(this); + env_->set_thread_id(thread_id_); + + env_->Start(0, nullptr, 0, nullptr, env->profiler_idle_notifier_started()); + } + + // The new isolate won't be bothered on this thread again. + isolate_->DiscardThreadSpecificMetadata(); +} + +bool Worker::is_stopped() const { + Mutex::ScopedLock stopped_lock(stopped_mutex_); + return stopped_; +} + +void Worker::Run() { + MultiIsolatePlatform* platform = isolate_data_->platform(); + CHECK_NE(platform, nullptr); + + { + Locker locker(isolate_); + Isolate::Scope isolate_scope(isolate_); + SealHandleScope outer_seal(isolate_); + + { + Context::Scope context_scope(env_->context()); + HandleScope handle_scope(isolate_); + + { + HandleScope handle_scope(isolate_); + Mutex::ScopedLock lock(mutex_); + // Set up the message channel for receiving messages in the child. + child_port_ = MessagePort::New(env_.get(), + env_->context(), + std::move(child_port_data_)); + // MessagePort::New() may return nullptr if execution is terminated + // within it. + if (child_port_ != nullptr) + env_->set_message_port(child_port_->object(isolate_)); + } + + if (!is_stopped()) { + HandleScope handle_scope(isolate_); + Environment::AsyncCallbackScope callback_scope(env_.get()); + env_->async_hooks()->push_async_ids(1, 0); + // This loads the Node bootstrapping code. + LoadEnvironment(env_.get()); + env_->async_hooks()->pop_async_id(1); + } + + { + SealHandleScope seal(isolate_); + bool more; + env_->performance_state()->Mark( + node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_START); + do { + if (is_stopped()) break; + uv_run(&loop_, UV_RUN_DEFAULT); + if (is_stopped()) break; + + platform->DrainBackgroundTasks(isolate_); + + more = uv_loop_alive(&loop_); + if (more && !is_stopped()) + continue; + + EmitBeforeExit(env_.get()); + + // Emit `beforeExit` if the loop became alive either after emitting + // event, or after running some callbacks. + more = uv_loop_alive(&loop_); + } while (more == true); + env_->performance_state()->Mark( + node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT); + } + } + + { + int exit_code; + bool stopped = is_stopped(); + if (!stopped) + exit_code = EmitExit(env_.get()); + Mutex::ScopedLock lock(mutex_); + if (exit_code_ == 0 && !stopped) + exit_code_ = exit_code; + } + + env_->set_can_call_into_js(false); + Isolate::DisallowJavascriptExecutionScope disallow_js(isolate_, + Isolate::DisallowJavascriptExecutionScope::THROW_ON_FAILURE); + + // Grab the parent-to-child channel and render is unusable. + MessagePort* child_port; + { + Mutex::ScopedLock lock(mutex_); + child_port = child_port_; + child_port_ = nullptr; + } + + { + Context::Scope context_scope(env_->context()); + child_port->Close(); + env_->stop_sub_worker_contexts(); + env_->RunCleanup(); + RunAtExit(env_.get()); + + { + Mutex::ScopedLock stopped_lock(stopped_mutex_); + stopped_ = true; + } + + env_->RunCleanup(); + + // This call needs to be made while the `Environment` is still alive + // because we assume that it is available for async tracking in the + // NodePlatform implementation. + platform->DrainBackgroundTasks(isolate_); + } + + env_.reset(); + } + + DisposeIsolate(); + + // Need to run the loop one more time to close the platform's uv_async_t + uv_run(&loop_, UV_RUN_ONCE); + + { + Mutex::ScopedLock lock(mutex_); + CHECK(thread_exit_async_); + scheduled_on_thread_stopped_ = true; + uv_async_send(thread_exit_async_.get()); + } +} + +void Worker::DisposeIsolate() { + if (isolate_ == nullptr) + return; + + CHECK(isolate_data_); + MultiIsolatePlatform* platform = isolate_data_->platform(); + platform->CancelPendingDelayedTasks(isolate_); + + isolate_data_.reset(); + + isolate_->Dispose(); + isolate_ = nullptr; +} + +void Worker::JoinThread() { + if (thread_joined_) + return; + CHECK_EQ(uv_thread_join(&tid_), 0); + thread_joined_ = true; + + env()->remove_sub_worker_context(this); + + if (thread_exit_async_) { + env()->CloseHandle(thread_exit_async_.release(), [](uv_async_t* async) { + delete async; + }); + + if (scheduled_on_thread_stopped_) + OnThreadStopped(); + } +} + +void Worker::OnThreadStopped() { + Mutex::ScopedLock lock(mutex_); + scheduled_on_thread_stopped_ = false; + + { + Mutex::ScopedLock stopped_lock(stopped_mutex_); + CHECK(stopped_); + } + + CHECK_EQ(child_port_, nullptr); + parent_port_ = nullptr; + + // It's okay to join the thread while holding the mutex because + // OnThreadStopped means it's no longer doing any work that might grab it + // and really just silently exiting. + JoinThread(); + + { + HandleScope handle_scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + + // Reset the parent port as we're closing it now anyway. + object()->Set(env()->context(), + env()->message_port_string(), + Undefined(env()->isolate())).FromJust(); + + Local code = Integer::New(env()->isolate(), exit_code_); + MakeCallback(env()->onexit_string(), 1, &code); + } + + // JoinThread() cleared all libuv handles bound to this Worker, + // the C++ object is no longer needed for anything now. + MakeWeak(); +} + +Worker::~Worker() { + Mutex::ScopedLock lock(mutex_); + JoinThread(); + + CHECK(stopped_); + CHECK(thread_joined_); + CHECK_EQ(child_port_, nullptr); + CHECK_EQ(uv_loop_close(&loop_), 0); + + // This has most likely already happened within the worker thread -- this + // is just in case Worker creation failed early. + DisposeIsolate(); +} + +void Worker::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + CHECK(args.IsConstructCall()); + + if (env->isolate_data()->platform() == nullptr) { + THROW_ERR_MISSING_PLATFORM_FOR_WORKER(env); + return; + } + + new Worker(env, args.This()); +} + +void Worker::StartThread(const FunctionCallbackInfo& args) { + Worker* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.This()); + Mutex::ScopedLock lock(w->mutex_); + + w->env()->add_sub_worker_context(w); + w->stopped_ = false; + CHECK_EQ(uv_thread_create(&w->tid_, [](void* arg) { + static_cast(arg)->Run(); + }, static_cast(w)), 0); + w->thread_joined_ = false; +} + +void Worker::StopThread(const FunctionCallbackInfo& args) { + Worker* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.This()); + + w->Exit(1); + w->JoinThread(); +} + +void Worker::Ref(const FunctionCallbackInfo& args) { + Worker* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.This()); + if (w->thread_exit_async_) + uv_ref(reinterpret_cast(w->thread_exit_async_.get())); +} + +void Worker::Unref(const FunctionCallbackInfo& args) { + Worker* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.This()); + if (w->thread_exit_async_) + uv_unref(reinterpret_cast(w->thread_exit_async_.get())); +} + +void Worker::Exit(int code) { + Mutex::ScopedLock lock(mutex_); + Mutex::ScopedLock stopped_lock(stopped_mutex_); + if (!stopped_) { + CHECK_NE(env_, nullptr); + stopped_ = true; + exit_code_ = code; + if (child_port_ != nullptr) + child_port_->StopEventLoop(); + isolate_->TerminateExecution(); + } +} + +size_t Worker::self_size() const { + return sizeof(*this); +} + +namespace { + +// Return the MessagePort that is global for this Environment and communicates +// with the internal [kPort] port of the JS Worker class in the parent thread. +void GetEnvMessagePort(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Local port = env->message_port(); + if (!port.IsEmpty()) { + CHECK_EQ(port->CreationContext()->GetIsolate(), args.GetIsolate()); + args.GetReturnValue().Set(port); + } +} + +void InitWorker(Local target, + Local unused, + Local context, + void* priv) { + Environment* env = Environment::GetCurrent(context); + + { + Local w = env->NewFunctionTemplate(Worker::New); + + w->InstanceTemplate()->SetInternalFieldCount(1); + + AsyncWrap::AddWrapMethods(env, w); + env->SetProtoMethod(w, "startThread", Worker::StartThread); + env->SetProtoMethod(w, "stopThread", Worker::StopThread); + env->SetProtoMethod(w, "ref", Worker::Ref); + env->SetProtoMethod(w, "unref", Worker::Unref); + + Local workerString = + FIXED_ONE_BYTE_STRING(env->isolate(), "Worker"); + w->SetClassName(workerString); + target->Set(workerString, w->GetFunction()); + } + + env->SetMethod(target, "getEnvMessagePort", GetEnvMessagePort); + + auto thread_id_string = FIXED_ONE_BYTE_STRING(env->isolate(), "threadId"); + target->Set(env->context(), + thread_id_string, + Number::New(env->isolate(), env->thread_id())).FromJust(); +} + +} // anonymous namespace + +} // namespace worker +} // namespace node + +NODE_MODULE_CONTEXT_AWARE_INTERNAL(worker, node::worker::InitWorker) diff --git a/src/node_worker.h b/src/node_worker.h new file mode 100644 index 00000000000000..0a98d2f11ef00f --- /dev/null +++ b/src/node_worker.h @@ -0,0 +1,83 @@ +#ifndef SRC_NODE_WORKER_H_ +#define SRC_NODE_WORKER_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "node_messaging.h" +#include + +namespace node { +namespace worker { + +// A worker thread, as represented in its parent thread. +class Worker : public AsyncWrap { + public: + Worker(Environment* env, v8::Local wrap); + ~Worker(); + + // Run the worker. This is only called from the worker thread. + void Run(); + + // Forcibly exit the thread with a specified exit code. This may be called + // from any thread. + void Exit(int code); + + // Wait for the worker thread to stop (in a blocking manner). + void JoinThread(); + + size_t self_size() const override; + bool is_stopped() const; + + static void New(const v8::FunctionCallbackInfo& args); + static void StartThread(const v8::FunctionCallbackInfo& args); + static void StopThread(const v8::FunctionCallbackInfo& args); + static void GetMessagePort(const v8::FunctionCallbackInfo& args); + static void Ref(const v8::FunctionCallbackInfo& args); + static void Unref(const v8::FunctionCallbackInfo& args); + + private: + void OnThreadStopped(); + void DisposeIsolate(); + + uv_loop_t loop_; + DeleteFnPtr isolate_data_; + DeleteFnPtr env_; + v8::Isolate* isolate_ = nullptr; + DeleteFnPtr + array_buffer_allocator_; + uv_thread_t tid_; + + // This mutex protects access to all variables listed below it. + mutable Mutex mutex_; + + // Currently only used for telling the parent thread that the child + // thread exited. + std::unique_ptr thread_exit_async_; + bool scheduled_on_thread_stopped_ = false; + + // This mutex only protects stopped_. If both locks are acquired, this needs + // to be the latter one. + mutable Mutex stopped_mutex_; + bool stopped_ = true; + + bool thread_joined_ = true; + int exit_code_ = 0; + double thread_id_ = -1; + + std::unique_ptr child_port_data_; + + // The child port is always kept alive by the child Environment's persistent + // handle to it. + MessagePort* child_port_ = nullptr; + // This is always kept alive because the JS object associated with the Worker + // instance refers to it via its [kPort] property. + MessagePort* parent_port_ = nullptr; +}; + +} // namespace worker +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + + +#endif // SRC_NODE_WORKER_H_ diff --git a/src/sharedarraybuffer_metadata.cc b/src/sharedarraybuffer_metadata.cc new file mode 100644 index 00000000000000..86476a9f12c38b --- /dev/null +++ b/src/sharedarraybuffer_metadata.cc @@ -0,0 +1,129 @@ +#include "sharedarraybuffer_metadata.h" +#include "base_object.h" +#include "base_object-inl.h" +#include "node_errors.h" + +using v8::Context; +using v8::Function; +using v8::FunctionTemplate; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Nothing; +using v8::Object; +using v8::SharedArrayBuffer; +using v8::Value; + +namespace node { +namespace worker { + +namespace { + +// Yield a JS constructor for SABLifetimePartner objects in the form of a +// standard API object, that has a single field for containing the raw +// SABLiftimePartner* pointer. +Local GetSABLifetimePartnerConstructor( + Environment* env, Local context) { + Local templ; + templ = env->sab_lifetimepartner_constructor_template(); + if (!templ.IsEmpty()) + return templ->GetFunction(context).ToLocalChecked(); + + templ = BaseObject::MakeLazilyInitializedJSTemplate(env); + templ->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), + "SABLifetimePartner")); + env->set_sab_lifetimepartner_constructor_template(templ); + + return GetSABLifetimePartnerConstructor(env, context); +} + +class SABLifetimePartner : public BaseObject { + public: + SABLifetimePartner(Environment* env, + Local obj, + SharedArrayBufferMetadataReference r) + : BaseObject(env, obj), + reference(r) { + MakeWeak(); + } + + SharedArrayBufferMetadataReference reference; +}; + +} // anonymous namespace + +SharedArrayBufferMetadataReference +SharedArrayBufferMetadata::ForSharedArrayBuffer( + Environment* env, + Local context, + Local source) { + Local lifetime_partner; + + if (!source->GetPrivate(context, + env->sab_lifetimepartner_symbol()) + .ToLocal(&lifetime_partner)) { + return nullptr; + } + + if (lifetime_partner->IsObject() && + env->sab_lifetimepartner_constructor_template() + ->HasInstance(lifetime_partner)) { + CHECK(source->IsExternal()); + SABLifetimePartner* partner = + Unwrap(lifetime_partner.As()); + CHECK_NE(partner, nullptr); + return partner->reference; + } + + if (source->IsExternal()) { + // If this is an external SharedArrayBuffer but we do not see a lifetime + // partner object, it was not us who externalized it. In that case, there + // is no way to serialize it, because it's unclear how the memory + // is actually owned. + THROW_ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER(env); + return nullptr; + } + + SharedArrayBuffer::Contents contents = source->Externalize(); + SharedArrayBufferMetadataReference r(new SharedArrayBufferMetadata( + contents.Data(), contents.ByteLength())); + if (r->AssignToSharedArrayBuffer(env, context, source).IsNothing()) + return nullptr; + return r; +} + +Maybe SharedArrayBufferMetadata::AssignToSharedArrayBuffer( + Environment* env, Local context, + Local target) { + CHECK(target->IsExternal()); + Local ctor = GetSABLifetimePartnerConstructor(env, context); + Local obj; + if (!ctor->NewInstance(context).ToLocal(&obj)) + return Nothing(); + + new SABLifetimePartner(env, obj, shared_from_this()); + return target->SetPrivate(context, + env->sab_lifetimepartner_symbol(), + obj); +} + +SharedArrayBufferMetadata::SharedArrayBufferMetadata(void* data, size_t size) + : data(data), size(size) { } + +SharedArrayBufferMetadata::~SharedArrayBufferMetadata() { + free(data); +} + +MaybeLocal SharedArrayBufferMetadata::GetSharedArrayBuffer( + Environment* env, Local context) { + Local obj = + SharedArrayBuffer::New(env->isolate(), data, size); + + if (AssignToSharedArrayBuffer(env, context, obj).IsNothing()) + return MaybeLocal(); + + return obj; +} + +} // namespace worker +} // namespace node diff --git a/src/sharedarraybuffer_metadata.h b/src/sharedarraybuffer_metadata.h new file mode 100644 index 00000000000000..84bfd224fabcf8 --- /dev/null +++ b/src/sharedarraybuffer_metadata.h @@ -0,0 +1,67 @@ +#ifndef SRC_SHAREDARRAYBUFFER_METADATA_H_ +#define SRC_SHAREDARRAYBUFFER_METADATA_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "node.h" +#include + +namespace node { +namespace worker { + +class SharedArrayBufferMetadata; + +// This is an object associated with a SharedArrayBuffer, which keeps track +// of a cross-thread reference count. Once a SharedArrayBuffer is transferred +// for the first time (or is attempted to be transferred), one of these objects +// is created, and the SharedArrayBuffer is moved from internalized mode into +// externalized mode (i.e. the JS engine no longer frees the memory on its own). +// +// This will always be referred to using a std::shared_ptr, since it keeps +// a reference count and is guaranteed to be thread-safe. +typedef std::shared_ptr + SharedArrayBufferMetadataReference; + +class SharedArrayBufferMetadata + : public std::enable_shared_from_this { + public: + static SharedArrayBufferMetadataReference ForSharedArrayBuffer( + Environment* env, + v8::Local context, + v8::Local source); + ~SharedArrayBufferMetadata(); + + // Create a SharedArrayBuffer object for a specific Environment and Context. + // The created SharedArrayBuffer will be in externalized mode and has + // a hidden object attached to it, during whose lifetime the reference + // count is increased by 1. + v8::MaybeLocal GetSharedArrayBuffer( + Environment* env, v8::Local context); + + SharedArrayBufferMetadata(SharedArrayBufferMetadata&& other) = delete; + SharedArrayBufferMetadata& operator=( + SharedArrayBufferMetadata&& other) = delete; + SharedArrayBufferMetadata& operator=( + const SharedArrayBufferMetadata&) = delete; + SharedArrayBufferMetadata(const SharedArrayBufferMetadata&) = delete; + + private: + explicit SharedArrayBufferMetadata(void* data, size_t size); + + // Attach a lifetime tracker object with a reference count to `target`. + v8::Maybe AssignToSharedArrayBuffer( + Environment* env, + v8::Local context, + v8::Local target); + + void* data = nullptr; + size_t size = 0; +}; + +} // namespace worker +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + + +#endif // SRC_SHAREDARRAYBUFFER_METADATA_H_ diff --git a/src/util.h b/src/util.h index e272286d3e4b96..fade27458f3e16 100644 --- a/src/util.h +++ b/src/util.h @@ -436,8 +436,11 @@ struct MallocedBuffer { return ret; } + inline bool is_empty() const { return data == nullptr; } + MallocedBuffer() : data(nullptr) {} explicit MallocedBuffer(size_t size) : data(Malloc(size)), size(size) {} + MallocedBuffer(char* data, size_t size) : data(data), size(size) {} MallocedBuffer(MallocedBuffer&& other) : data(other.data), size(other.size) { other.data = nullptr; } diff --git a/test/async-hooks/init-hooks.js b/test/async-hooks/init-hooks.js index 509f443b29a671..14969d8e753b68 100644 --- a/test/async-hooks/init-hooks.js +++ b/test/async-hooks/init-hooks.js @@ -1,7 +1,7 @@ 'use strict'; // Flags: --expose-gc -require('../common'); +const common = require('../common'); const assert = require('assert'); const async_hooks = require('async_hooks'); const util = require('util'); @@ -162,6 +162,10 @@ class ActivityCollector { const stub = { uid, type: 'Unknown', handleIsObject: true }; this._activities.set(uid, stub); return stub; + } else if (!common.isMainThread) { + // Worker threads start main script execution inside of an AsyncWrap + // callback, so we don't yield errors for these. + return null; } else { const err = new Error(`Found a handle whose ${hook}` + ' hook was invoked but not its init hook'); diff --git a/test/async-hooks/test-crypto-pbkdf2.js b/test/async-hooks/test-crypto-pbkdf2.js index 2a0b44db8ce7a8..ed7270d191e903 100644 --- a/test/async-hooks/test-crypto-pbkdf2.js +++ b/test/async-hooks/test-crypto-pbkdf2.js @@ -3,6 +3,8 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (!common.isMainThread) + common.skip('Worker bootstrapping works differently -> different async IDs'); const assert = require('assert'); const tick = require('./tick'); diff --git a/test/async-hooks/test-crypto-randomBytes.js b/test/async-hooks/test-crypto-randomBytes.js index 76f8f7759503c4..67f5221a5e0b30 100644 --- a/test/async-hooks/test-crypto-randomBytes.js +++ b/test/async-hooks/test-crypto-randomBytes.js @@ -3,6 +3,8 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (!common.isMainThread) + common.skip('Worker bootstrapping works differently -> different async IDs'); const assert = require('assert'); const tick = require('./tick'); diff --git a/test/async-hooks/test-emit-before-after.js b/test/async-hooks/test-emit-before-after.js index e7744eb4b7bdf0..6a9ceaeefb8c0a 100644 --- a/test/async-hooks/test-emit-before-after.js +++ b/test/async-hooks/test-emit-before-after.js @@ -7,6 +7,9 @@ const spawnSync = require('child_process').spawnSync; const async_hooks = require('internal/async_hooks'); const initHooks = require('./init-hooks'); +if (!common.isMainThread) + common.skip('Worker bootstrapping works differently -> different async IDs'); + switch (process.argv[2]) { case 'test_invalid_async_id': async_hooks.emitBefore(-2, 1); diff --git a/test/async-hooks/test-enable-disable.js b/test/async-hooks/test-enable-disable.js index 27e13efdd8b3fd..df093da608c29c 100644 --- a/test/async-hooks/test-enable-disable.js +++ b/test/async-hooks/test-enable-disable.js @@ -88,6 +88,10 @@ const assert = require('assert'); const tick = require('./tick'); const initHooks = require('./init-hooks'); const { checkInvocations } = require('./hook-checks'); + +if (!common.isMainThread) + common.skip('Worker bootstrapping works differently -> different timing'); + // Include "Unknown"s because hook2 will not be able to identify // the type of the first Immediate since it will miss its `init` invocation. const types = [ 'Immediate', 'Unknown' ]; diff --git a/test/async-hooks/test-fseventwrap.js b/test/async-hooks/test-fseventwrap.js index 2d9e697501efb6..56ada05b760eb6 100644 --- a/test/async-hooks/test-fseventwrap.js +++ b/test/async-hooks/test-fseventwrap.js @@ -1,5 +1,5 @@ 'use strict'; -require('../common'); +const common = require('../common'); const assert = require('assert'); const initHooks = require('./init-hooks'); @@ -7,6 +7,9 @@ const tick = require('./tick'); const { checkInvocations } = require('./hook-checks'); const fs = require('fs'); +if (!common.isMainThread) + common.skip('Worker bootstrapping works differently -> different async IDs'); + const hooks = initHooks(); hooks.enable(); diff --git a/test/async-hooks/test-fsreqwrap-readFile.js b/test/async-hooks/test-fsreqwrap-readFile.js index a40b9a35fd4b0b..064cd80a48279e 100644 --- a/test/async-hooks/test-fsreqwrap-readFile.js +++ b/test/async-hooks/test-fsreqwrap-readFile.js @@ -7,6 +7,9 @@ const initHooks = require('./init-hooks'); const { checkInvocations } = require('./hook-checks'); const fs = require('fs'); +if (!common.isMainThread) + common.skip('Worker bootstrapping works differently -> different async IDs'); + const hooks = initHooks(); hooks.enable(); diff --git a/test/async-hooks/test-getaddrinforeqwrap.js b/test/async-hooks/test-getaddrinforeqwrap.js index 787ea26fd344c8..298c4931cce74e 100644 --- a/test/async-hooks/test-getaddrinforeqwrap.js +++ b/test/async-hooks/test-getaddrinforeqwrap.js @@ -7,6 +7,9 @@ const initHooks = require('./init-hooks'); const { checkInvocations } = require('./hook-checks'); const dns = require('dns'); +if (!common.isMainThread) + common.skip('Worker bootstrapping works differently -> different async IDs'); + const hooks = initHooks(); hooks.enable(); diff --git a/test/async-hooks/test-getnameinforeqwrap.js b/test/async-hooks/test-getnameinforeqwrap.js index 071462dddc9b10..a32814e5b5df4a 100644 --- a/test/async-hooks/test-getnameinforeqwrap.js +++ b/test/async-hooks/test-getnameinforeqwrap.js @@ -7,6 +7,9 @@ const initHooks = require('./init-hooks'); const { checkInvocations } = require('./hook-checks'); const dns = require('dns'); +if (!common.isMainThread) + common.skip('Worker bootstrapping works differently -> different async IDs'); + const hooks = initHooks(); hooks.enable(); diff --git a/test/async-hooks/test-graph.signal.js b/test/async-hooks/test-graph.signal.js index 770fe00da4b4a6..4c5b857fc2e08f 100644 --- a/test/async-hooks/test-graph.signal.js +++ b/test/async-hooks/test-graph.signal.js @@ -1,9 +1,10 @@ 'use strict'; const common = require('../common'); -if (common.isWindows) { +if (common.isWindows) common.skip('no signals on Windows'); -} +if (!common.isMainThread) + common.skip('No signal handling available in Workers'); const initHooks = require('./init-hooks'); const verifyGraph = require('./verify-graph'); diff --git a/test/async-hooks/test-no-assert-when-disabled.js b/test/async-hooks/test-no-assert-when-disabled.js index 12742bd5d4f014..70114d1e1140f8 100644 --- a/test/async-hooks/test-no-assert-when-disabled.js +++ b/test/async-hooks/test-no-assert-when-disabled.js @@ -1,6 +1,9 @@ 'use strict'; // Flags: --no-force-async-hooks-checks --expose-internals -require('../common'); +const common = require('../common'); + +if (!common.isMainThread) + common.skip('Workers don\'t inherit per-env state like the check flag'); const async_hooks = require('internal/async_hooks'); diff --git a/test/async-hooks/test-pipewrap.js b/test/async-hooks/test-pipewrap.js index 066458841fabab..f2b5bf985225b9 100644 --- a/test/async-hooks/test-pipewrap.js +++ b/test/async-hooks/test-pipewrap.js @@ -10,6 +10,9 @@ const initHooks = require('./init-hooks'); const { checkInvocations } = require('./hook-checks'); const spawn = require('child_process').spawn; +if (!common.isMainThread) + common.skip('Worker bootstrapping works differently -> different async IDs'); + const hooks = initHooks(); hooks.enable(); diff --git a/test/async-hooks/test-promise.chain-promise-before-init-hooks.js b/test/async-hooks/test-promise.chain-promise-before-init-hooks.js index 00b53300c4d727..873fd272cf2d29 100644 --- a/test/async-hooks/test-promise.chain-promise-before-init-hooks.js +++ b/test/async-hooks/test-promise.chain-promise-before-init-hooks.js @@ -4,6 +4,10 @@ const common = require('../common'); const assert = require('assert'); const initHooks = require('./init-hooks'); const { checkInvocations } = require('./hook-checks'); + +if (!common.isMainThread) + common.skip('Worker bootstrapping works differently -> different async IDs'); + common.crashOnUnhandledRejection(); const p = new Promise(common.mustCall(function executor(resolve, reject) { diff --git a/test/async-hooks/test-promise.js b/test/async-hooks/test-promise.js index 2ee64962131005..d3070b7cbd594d 100644 --- a/test/async-hooks/test-promise.js +++ b/test/async-hooks/test-promise.js @@ -6,6 +6,9 @@ const assert = require('assert'); const initHooks = require('./init-hooks'); const { checkInvocations } = require('./hook-checks'); +if (!common.isMainThread) + common.skip('Worker bootstrapping works differently -> different async IDs'); + common.crashOnUnhandledRejection(); const hooks = initHooks(); diff --git a/test/async-hooks/test-signalwrap.js b/test/async-hooks/test-signalwrap.js index ae4f41c6a413ba..c94d763450ab24 100644 --- a/test/async-hooks/test-signalwrap.js +++ b/test/async-hooks/test-signalwrap.js @@ -3,6 +3,8 @@ const common = require('../common'); if (common.isWindows) common.skip('no signals in Windows'); +if (!common.isMainThread) + common.skip('No signal handling available in Workers'); const assert = require('assert'); const initHooks = require('./init-hooks'); diff --git a/test/async-hooks/test-statwatcher.js b/test/async-hooks/test-statwatcher.js index 52d146306405f8..8085ebf51b3b1d 100644 --- a/test/async-hooks/test-statwatcher.js +++ b/test/async-hooks/test-statwatcher.js @@ -1,12 +1,15 @@ 'use strict'; -require('../common'); +const common = require('../common'); const commonPath = require.resolve('../common'); const assert = require('assert'); const initHooks = require('./init-hooks'); const { checkInvocations } = require('./hook-checks'); const fs = require('fs'); +if (!common.isMainThread) + common.skip('Worker bootstrapping works differently -> different async IDs'); + const hooks = initHooks(); hooks.enable(); diff --git a/test/common/index.js b/test/common/index.js index f76e1cd38cf460..6437b9952da0e2 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -46,6 +46,14 @@ Object.defineProperty(exports, 'PORT', { enumerable: true }); +exports.isMainThread = (() => { + try { + return require('worker_threads').isMainThread; + } catch { + // Worker module not enabled → only a single main thread exists. + return true; + } +})(); exports.isWindows = process.platform === 'win32'; exports.isWOW64 = exports.isWindows && @@ -746,6 +754,10 @@ exports.skipIfInspectorDisabled = function skipIfInspectorDisabled() { if (process.config.variables.v8_enable_inspector === 0) { exports.skip('V8 inspector is disabled'); } + if (!exports.isMainThread) { + // TODO(addaleax): Fix me. + exports.skip('V8 inspector is not available in Workers'); + } }; exports.skipIf32Bits = function skipIf32Bits() { diff --git a/test/fixtures/worker-script.mjs b/test/fixtures/worker-script.mjs new file mode 100644 index 00000000000000..b92905e06c0b69 --- /dev/null +++ b/test/fixtures/worker-script.mjs @@ -0,0 +1,3 @@ +import worker from 'worker_threads'; + +worker.parentPort.postMessage('Hello, world!'); diff --git a/test/known_issues/test-stdin-is-always-net.socket.js b/test/known_issues/test-stdin-is-always-net.socket.js index 9a4c524c4aecc7..8db33bd7a5ce37 100644 --- a/test/known_issues/test-stdin-is-always-net.socket.js +++ b/test/known_issues/test-stdin-is-always-net.socket.js @@ -19,5 +19,5 @@ const proc = spawn( // To double-check this test, set stdio to 'pipe' and uncomment the line below. // proc.stderr.pipe(process.stderr); proc.on('exit', common.mustCall(function(exitCode) { - process.exitCode = exitCode; + assert.strictEqual(exitCode, 0); })); diff --git a/test/parallel/test-async-hooks-disable-during-promise.js b/test/parallel/test-async-hooks-disable-during-promise.js index d6566661294734..ace9bca6799a13 100644 --- a/test/parallel/test-async-hooks-disable-during-promise.js +++ b/test/parallel/test-async-hooks-disable-during-promise.js @@ -3,6 +3,9 @@ const common = require('../common'); const async_hooks = require('async_hooks'); common.crashOnUnhandledRejection(); +if (!common.isMainThread) + common.skip('Worker bootstrapping works differently -> different AsyncWraps'); + const hook = async_hooks.createHook({ init: common.mustCall(2), before: common.mustCall(1), diff --git a/test/parallel/test-async-hooks-disable-gc-tracking.js b/test/parallel/test-async-hooks-disable-gc-tracking.js index a34739a9bb2b95..84c5043aad3335 100644 --- a/test/parallel/test-async-hooks-disable-gc-tracking.js +++ b/test/parallel/test-async-hooks-disable-gc-tracking.js @@ -8,7 +8,7 @@ const common = require('../common'); const async_hooks = require('async_hooks'); const hook = async_hooks.createHook({ - destroy: common.mustCall(1) // only 1 immediate is destroyed + destroy: common.mustCallAtLeast(1) // only 1 immediate is destroyed }).enable(); new async_hooks.AsyncResource('foobar', { requireManualDestroy: true }); diff --git a/test/parallel/test-async-hooks-enable-during-promise.js b/test/parallel/test-async-hooks-enable-during-promise.js index 17b3c884bb9284..29d25de9805de6 100644 --- a/test/parallel/test-async-hooks-enable-during-promise.js +++ b/test/parallel/test-async-hooks-enable-during-promise.js @@ -7,8 +7,8 @@ common.crashOnUnhandledRejection(); Promise.resolve(1).then(common.mustCall(() => { async_hooks.createHook({ init: common.mustCall(), - before: common.mustCall(), - after: common.mustCall(2) + before: common.mustCallAtLeast(), + after: common.mustCallAtLeast(2) }).enable(); process.nextTick(common.mustCall()); diff --git a/test/parallel/test-async-hooks-prevent-double-destroy.js b/test/parallel/test-async-hooks-prevent-double-destroy.js index 5cd9c5e9a017cb..689dc399f9d2f2 100644 --- a/test/parallel/test-async-hooks-prevent-double-destroy.js +++ b/test/parallel/test-async-hooks-prevent-double-destroy.js @@ -8,7 +8,7 @@ const common = require('../common'); const async_hooks = require('async_hooks'); const hook = async_hooks.createHook({ - destroy: common.mustCall(2) // 1 immediate + manual destroy + destroy: common.mustCallAtLeast(2) // 1 immediate + manual destroy }).enable(); { diff --git a/test/parallel/test-async-hooks-promise-triggerid.js b/test/parallel/test-async-hooks-promise-triggerid.js index 7afd005855fb3e..507a8a4ada2c7e 100644 --- a/test/parallel/test-async-hooks-promise-triggerid.js +++ b/test/parallel/test-async-hooks-promise-triggerid.js @@ -3,6 +3,9 @@ const common = require('../common'); const assert = require('assert'); const async_hooks = require('async_hooks'); +if (!common.isMainThread) + common.skip('Worker bootstrapping works differently -> different async IDs'); + common.crashOnUnhandledRejection(); const promiseAsyncIds = []; diff --git a/test/parallel/test-async-hooks-promise.js b/test/parallel/test-async-hooks-promise.js index 4b36f6026b36c6..be1509c7fc23f6 100644 --- a/test/parallel/test-async-hooks-promise.js +++ b/test/parallel/test-async-hooks-promise.js @@ -3,6 +3,9 @@ const common = require('../common'); const assert = require('assert'); const async_hooks = require('async_hooks'); +if (!common.isMainThread) + common.skip('Worker bootstrapping works differently -> different async IDs'); + const initCalls = []; const resolveCalls = []; diff --git a/test/parallel/test-async-hooks-top-level-clearimmediate.js b/test/parallel/test-async-hooks-top-level-clearimmediate.js index e7a5d8f5606118..cc5fcf48eb50b3 100644 --- a/test/parallel/test-async-hooks-top-level-clearimmediate.js +++ b/test/parallel/test-async-hooks-top-level-clearimmediate.js @@ -6,6 +6,9 @@ const common = require('../common'); const assert = require('assert'); const async_hooks = require('async_hooks'); +if (!common.isMainThread) + common.skip('Worker bootstrapping works differently -> different async IDs'); + let seenId, seenResource; async_hooks.createHook({ diff --git a/test/parallel/test-async-wrap-promise-after-enabled.js b/test/parallel/test-async-wrap-promise-after-enabled.js index 9535c080f246b7..5df8f13c008e19 100644 --- a/test/parallel/test-async-wrap-promise-after-enabled.js +++ b/test/parallel/test-async-wrap-promise-after-enabled.js @@ -5,6 +5,9 @@ const common = require('../common'); const assert = require('assert'); +if (!common.isMainThread) + common.skip('Worker bootstrapping works differently -> different timing'); + const async_hooks = require('async_hooks'); const seenEvents = []; diff --git a/test/parallel/test-child-process-custom-fds.js b/test/parallel/test-child-process-custom-fds.js index fbfc8776a37777..c3146564764295 100644 --- a/test/parallel/test-child-process-custom-fds.js +++ b/test/parallel/test-child-process-custom-fds.js @@ -5,6 +5,9 @@ const assert = require('assert'); const internalCp = require('internal/child_process'); const oldSpawnSync = internalCp.spawnSync; +if (!common.isMainThread) + common.skip('stdio is not associated with file descriptors in Workers'); + // Verify that customFds is used if stdio is not provided. { const msg = 'child_process: options.customFds option is deprecated. ' + diff --git a/test/parallel/test-child-process-http-socket-leak.js b/test/parallel/test-child-process-http-socket-leak.js index 30d8601b840626..553a3277532b04 100644 --- a/test/parallel/test-child-process-http-socket-leak.js +++ b/test/parallel/test-child-process-http-socket-leak.js @@ -6,7 +6,6 @@ const common = require('../common'); const assert = require('assert'); const { fork } = require('child_process'); const http = require('http'); -const { kTimeout } = require('internal/timers'); if (process.argv[2] === 'child') { process.once('message', (req, socket) => { @@ -19,6 +18,8 @@ if (process.argv[2] === 'child') { return; } +const { kTimeout } = require('internal/timers'); + let child; let socket; diff --git a/test/parallel/test-child-process-validate-stdio.js b/test/parallel/test-child-process-validate-stdio.js index 4c4f137110b67e..26e4c297672ef6 100644 --- a/test/parallel/test-child-process-validate-stdio.js +++ b/test/parallel/test-child-process-validate-stdio.js @@ -30,7 +30,8 @@ common.expectsError(() => _validateStdio(stdio2, true), { code: 'ERR_IPC_SYNC_FORK', type: Error } ); -{ + +if (common.isMainThread) { const stdio3 = [process.stdin, process.stdout, process.stderr]; const result = _validateStdio(stdio3, false); assert.deepStrictEqual(result, { @@ -42,4 +43,7 @@ common.expectsError(() => _validateStdio(stdio2, true), ipc: undefined, ipcFd: undefined }); +} else { + common.printSkipMessage( + 'stdio is not associated with file descriptors in Workers'); } diff --git a/test/parallel/test-cli-eval.js b/test/parallel/test-cli-eval.js index 201d5a6a49739b..7a2c7a82aa797f 100644 --- a/test/parallel/test-cli-eval.js +++ b/test/parallel/test-cli-eval.js @@ -34,6 +34,9 @@ const path = require('path'); const fixtures = require('../common/fixtures'); const nodejs = `"${process.execPath}"`; +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + if (process.argv.length > 2) { console.log(process.argv.slice(2).join(' ')); process.exit(0); diff --git a/test/parallel/test-cli-node-options-disallowed.js b/test/parallel/test-cli-node-options-disallowed.js index e4ae2d1aea28f2..0351f83c52b50b 100644 --- a/test/parallel/test-cli-node-options-disallowed.js +++ b/test/parallel/test-cli-node-options-disallowed.js @@ -2,6 +2,8 @@ const common = require('../common'); if (process.config.variables.node_without_node_options) common.skip('missing NODE_OPTIONS support'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); // Test options specified by env variable. diff --git a/test/parallel/test-cli-node-options.js b/test/parallel/test-cli-node-options.js index 2383935f4bb7a3..d851f225fcbc35 100644 --- a/test/parallel/test-cli-node-options.js +++ b/test/parallel/test-cli-node-options.js @@ -2,6 +2,8 @@ const common = require('../common'); if (process.config.variables.node_without_node_options) common.skip('missing NODE_OPTIONS support'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); // Test options specified by env variable. diff --git a/test/parallel/test-cluster-net-listen-relative-path.js b/test/parallel/test-cluster-net-listen-relative-path.js index ce9ead9e2eb958..7e61cf83da7882 100644 --- a/test/parallel/test-cluster-net-listen-relative-path.js +++ b/test/parallel/test-cluster-net-listen-relative-path.js @@ -11,6 +11,8 @@ const tmpdir = require('../common/tmpdir'); if (common.isWindows) common.skip('On Windows named pipes live in their own ' + 'filesystem and don\'t have a ~100 byte limit'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); // Choose a socket name such that the absolute path would exceed 100 bytes. const socketDir = './unix-socket-dir'; diff --git a/test/parallel/test-console.js b/test/parallel/test-console.js index 9a0fa778c1ff65..9e2e28b04e714a 100644 --- a/test/parallel/test-console.js +++ b/test/parallel/test-console.js @@ -27,9 +27,10 @@ const util = require('util'); assert.ok(process.stdout.writable); assert.ok(process.stderr.writable); // Support legacy API -assert.strictEqual(typeof process.stdout.fd, 'number'); -assert.strictEqual(typeof process.stderr.fd, 'number'); - +if (common.isMainThread) { + assert.strictEqual(typeof process.stdout.fd, 'number'); + assert.strictEqual(typeof process.stderr.fd, 'number'); +} process.once('warning', common.mustCall((warning) => { assert(/no such label/.test(warning.message)); })); diff --git a/test/parallel/test-cwd-enoent-preload.js b/test/parallel/test-cwd-enoent-preload.js index b83ff6ff883a05..2077d9c1478335 100644 --- a/test/parallel/test-cwd-enoent-preload.js +++ b/test/parallel/test-cwd-enoent-preload.js @@ -3,6 +3,8 @@ const common = require('../common'); // Fails with EINVAL on SmartOS, EBUSY on Windows, EBUSY on AIX. if (common.isSunOS || common.isWindows || common.isAIX) common.skip('cannot rmdir current working directory'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); const assert = require('assert'); const fs = require('fs'); diff --git a/test/parallel/test-cwd-enoent-repl.js b/test/parallel/test-cwd-enoent-repl.js index d42679d8688e06..5ea8abc7e42b52 100644 --- a/test/parallel/test-cwd-enoent-repl.js +++ b/test/parallel/test-cwd-enoent-repl.js @@ -3,6 +3,8 @@ const common = require('../common'); // Fails with EINVAL on SmartOS, EBUSY on Windows, EBUSY on AIX. if (common.isSunOS || common.isWindows || common.isAIX) common.skip('cannot rmdir current working directory'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); const assert = require('assert'); const fs = require('fs'); diff --git a/test/parallel/test-cwd-enoent.js b/test/parallel/test-cwd-enoent.js index e5d93f46ce60c0..8beb1e3fbe0a05 100644 --- a/test/parallel/test-cwd-enoent.js +++ b/test/parallel/test-cwd-enoent.js @@ -3,6 +3,8 @@ const common = require('../common'); // Fails with EINVAL on SmartOS, EBUSY on Windows, EBUSY on AIX. if (common.isSunOS || common.isWindows || common.isAIX) common.skip('cannot rmdir current working directory'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); const assert = require('assert'); const fs = require('fs'); diff --git a/test/parallel/test-debug-args.js b/test/parallel/test-debug-args.js index a4ed4f9f1ee719..bb7ec5582401a6 100644 --- a/test/parallel/test-debug-args.js +++ b/test/parallel/test-debug-args.js @@ -1,7 +1,10 @@ 'use strict'; // Flags: --debug-code -require('../common'); +const common = require('../common'); const assert = require('assert'); +if (!common.isMainThread) + common.skip('execArgv does not affect Workers'); + assert(process.execArgv.includes('--debug-code')); diff --git a/test/parallel/test-error-serdes.js b/test/parallel/test-error-serdes.js new file mode 100644 index 00000000000000..e9d91e5736bcac --- /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 { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes; +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 ERR_INVALID_ARG_TYPE('object', 'Object', 42); + 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-fs-realpath.js b/test/parallel/test-fs-realpath.js index d7efc748bc9662..599616f5224eb7 100644 --- a/test/parallel/test-fs-realpath.js +++ b/test/parallel/test-fs-realpath.js @@ -24,6 +24,9 @@ const common = require('../common'); const fixtures = require('../common/fixtures'); const tmpdir = require('../common/tmpdir'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + const assert = require('assert'); const fs = require('fs'); const path = require('path'); diff --git a/test/parallel/test-fs-write-file-sync.js b/test/parallel/test-fs-write-file-sync.js index 9a19b9f6e93439..2964ac368b5de4 100644 --- a/test/parallel/test-fs-write-file-sync.js +++ b/test/parallel/test-fs-write-file-sync.js @@ -28,6 +28,9 @@ let openCount = 0; let mode; let content; +if (!common.isMainThread) + common.skip('process.umask is not available in Workers'); + // Need to hijack fs.open/close to make sure that things // get closed once they're opened. fs._openSync = fs.openSync; diff --git a/test/parallel/test-message-channel-sharedarraybuffer.js b/test/parallel/test-message-channel-sharedarraybuffer.js new file mode 100644 index 00000000000000..86335fb38b1516 --- /dev/null +++ b/test/parallel/test-message-channel-sharedarraybuffer.js @@ -0,0 +1,28 @@ +// Flags: --expose-gc --experimental-worker +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +{ + const sharedArrayBuffer = new SharedArrayBuffer(12); + const local = Buffer.from(sharedArrayBuffer); + + const w = new Worker(` + const { parentPort } = require('worker_threads'); + parentPort.on('message', ({ sharedArrayBuffer }) => { + const local = Buffer.from(sharedArrayBuffer); + local.write('world!', 6); + parentPort.postMessage('written!'); + }); + `, { eval: true }); + w.on('message', common.mustCall(() => { + assert.strictEqual(local.toString(), 'Hello world!'); + global.gc(); + w.terminate(); + })); + w.postMessage({ sharedArrayBuffer }); + // This would be a race condition if the memory regions were overlapping + local.write('Hello '); +} diff --git a/test/parallel/test-message-channel.js b/test/parallel/test-message-channel.js new file mode 100644 index 00000000000000..03cc3549a82ff9 --- /dev/null +++ b/test/parallel/test-message-channel.js @@ -0,0 +1,46 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { MessageChannel, MessagePort, Worker } = require('worker_threads'); + +{ + 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(); +} + +{ + const channel = new MessageChannel(); + + const w = new Worker(` + const { MessagePort } = require('worker_threads'); + const assert = require('assert'); + require('worker_threads').parentPort.on('message', ({ port }) => { + assert(port instanceof MessagePort); + port.postMessage('works'); + }); + `, { eval: true }); + w.postMessage({ port: channel.port2 }, [ channel.port2 ]); + assert(channel.port1 instanceof MessagePort); + assert(channel.port2 instanceof MessagePort); + channel.port1.on('message', common.mustCall((message) => { + assert.strictEqual(message, 'works'); + w.terminate(); + })); +} diff --git a/test/parallel/test-message-port-arraybuffer.js b/test/parallel/test-message-port-arraybuffer.js new file mode 100644 index 00000000000000..85b5d07c6362b4 --- /dev/null +++ b/test/parallel/test-message-port-arraybuffer.js @@ -0,0 +1,20 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { MessageChannel } = require('worker_threads'); + +{ + const { port1, port2 } = new MessageChannel(); + + const arrayBuffer = new ArrayBuffer(40); + const typedArray = new Uint32Array(arrayBuffer); + typedArray[0] = 0x12345678; + + port1.postMessage(typedArray, [ arrayBuffer ]); + port2.on('message', common.mustCall((received) => { + assert.strictEqual(received[0], 0x12345678); + port2.close(common.mustCall()); + })); +} diff --git a/test/parallel/test-message-port-message-port-transferring.js b/test/parallel/test-message-port-message-port-transferring.js new file mode 100644 index 00000000000000..32d997b9dddab8 --- /dev/null +++ b/test/parallel/test-message-port-message-port-transferring.js @@ -0,0 +1,23 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { MessageChannel } = require('worker_threads'); + +{ + const { port1: basePort1, port2: basePort2 } = new MessageChannel(); + const { + port1: transferredPort1, port2: transferredPort2 + } = new MessageChannel(); + + basePort1.postMessage({ transferredPort1 }, [ transferredPort1 ]); + basePort2.on('message', common.mustCall(({ transferredPort1 }) => { + transferredPort1.postMessage('foobar'); + transferredPort2.on('message', common.mustCall((msg) => { + assert.strictEqual(msg, 'foobar'); + transferredPort1.close(common.mustCall()); + basePort1.close(common.mustCall()); + })); + })); +} diff --git a/test/parallel/test-message-port.js b/test/parallel/test-message-port.js new file mode 100644 index 00000000000000..fe88126d044c2b --- /dev/null +++ b/test/parallel/test-message-port.js @@ -0,0 +1,56 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { MessageChannel, MessagePort } = require('worker_threads'); + +{ + const { port1, port2 } = new MessageChannel(); + assert(port1 instanceof MessagePort); + assert(port2 instanceof MessagePort); + + const input = { a: 1 }; + port1.postMessage(input); + port2.on('message', common.mustCall((received) => { + assert.deepStrictEqual(received, input); + port2.close(common.mustCall()); + })); +} + +{ + const { port1, port2 } = new MessageChannel(); + + const input = { a: 1 }; + port1.postMessage(input); + // Check that the message still gets delivered if `port2` has its + // `on('message')` handler attached at a later point in time. + setImmediate(() => { + port2.on('message', common.mustCall((received) => { + assert.deepStrictEqual(received, input); + port2.close(common.mustCall()); + })); + }); +} + +{ + const { port1, port2 } = new MessageChannel(); + + const input = { a: 1 }; + + const dummy = common.mustNotCall(); + // Check that the message still gets delivered if `port2` has its + // `on('message')` handler attached at a later point in time, even if a + // listener was removed previously. + port2.addListener('message', dummy); + setImmediate(() => { + port2.removeListener('message', dummy); + port1.postMessage(input); + setImmediate(() => { + port2.on('message', common.mustCall((received) => { + assert.deepStrictEqual(received, input); + port2.close(common.mustCall()); + })); + }); + }); +} diff --git a/test/parallel/test-module-cjs-helpers.js b/test/parallel/test-module-cjs-helpers.js index f6e80af032a2cd..8e50ecd314dc17 100644 --- a/test/parallel/test-module-cjs-helpers.js +++ b/test/parallel/test-module-cjs-helpers.js @@ -1,5 +1,5 @@ 'use strict'; -// Flags: --expose-internals +// Flags: --expose-internals --experimental-worker require('../common'); const assert = require('assert'); @@ -7,5 +7,5 @@ const { builtinLibs } = require('internal/modules/cjs/helpers'); const hasInspector = process.config.variables.v8_enable_inspector === 1; -const expectedLibs = hasInspector ? 33 : 32; +const expectedLibs = hasInspector ? 34 : 33; assert.strictEqual(builtinLibs.length, expectedLibs); diff --git a/test/parallel/test-preload.js b/test/parallel/test-preload.js index 60d52dbcf1c0cb..5941c0b96bc602 100644 --- a/test/parallel/test-preload.js +++ b/test/parallel/test-preload.js @@ -5,6 +5,8 @@ const fixtures = require('../common/fixtures'); // Refs: https://github.com/nodejs/node/pull/2253 if (common.isSunOS) common.skip('unreliable on SunOS'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); const assert = require('assert'); const childProcess = require('child_process'); diff --git a/test/parallel/test-process-chdir-errormessage.js b/test/parallel/test-process-chdir-errormessage.js index e511688cc76bd9..0475d7940ce16a 100644 --- a/test/parallel/test-process-chdir-errormessage.js +++ b/test/parallel/test-process-chdir-errormessage.js @@ -1,8 +1,10 @@ 'use strict'; -const { expectsError } = require('../common'); +const common = require('../common'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); -expectsError( +common.expectsError( () => { process.chdir('does-not-exist'); }, diff --git a/test/parallel/test-process-chdir.js b/test/parallel/test-process-chdir.js index 998147dd43dfc8..e66d366fb7874f 100644 --- a/test/parallel/test-process-chdir.js +++ b/test/parallel/test-process-chdir.js @@ -5,6 +5,9 @@ const assert = require('assert'); const fs = require('fs'); const path = require('path'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + const tmpdir = require('../common/tmpdir'); process.chdir('..'); diff --git a/test/parallel/test-process-euid-egid.js b/test/parallel/test-process-euid-egid.js index 84fbd03e327b05..5639163bf31c13 100644 --- a/test/parallel/test-process-euid-egid.js +++ b/test/parallel/test-process-euid-egid.js @@ -3,9 +3,11 @@ const common = require('../common'); const assert = require('assert'); -if (common.isWindows) { - assert.strictEqual(process.geteuid, undefined); - assert.strictEqual(process.getegid, undefined); +if (common.isWindows || !common.isMainThread) { + if (common.isMainThread) { + assert.strictEqual(process.geteuid, undefined); + assert.strictEqual(process.getegid, undefined); + } assert.strictEqual(process.seteuid, undefined); assert.strictEqual(process.setegid, undefined); return; diff --git a/test/parallel/test-process-exit-handler.js b/test/parallel/test-process-exit-handler.js index 22d84f34349b73..d74e320fe63082 100644 --- a/test/parallel/test-process-exit-handler.js +++ b/test/parallel/test-process-exit-handler.js @@ -1,11 +1,14 @@ 'use strict'; -require('../common'); +const common = require('../common'); + +if (!common.isMainThread) + common.skip('execArgv does not affect Workers'); // This test ensures that no asynchronous operations are performed in the 'exit' // handler. // https://github.com/nodejs/node/issues/12322 process.on('exit', () => { - setTimeout(process.abort, 0); // Should not run. + setTimeout(() => process.abort(), 0); // Should not run. for (const start = Date.now(); Date.now() - start < 10;); }); diff --git a/test/parallel/test-process-fatal-exception-tick.js b/test/parallel/test-process-fatal-exception-tick.js index 605f961f639228..f22273e01c9db8 100644 --- a/test/parallel/test-process-fatal-exception-tick.js +++ b/test/parallel/test-process-fatal-exception-tick.js @@ -3,6 +3,9 @@ const common = require('../common'); const assert = require('assert'); +if (!common.isMainThread) + common.skip('Error handling timing is different in Workers'); + // If a process encounters an uncaughtException, it should schedule // processing of nextTicks on the next Immediates cycle but not // before all Immediates are handled diff --git a/test/parallel/test-process-uid-gid.js b/test/parallel/test-process-uid-gid.js index 24751943092761..456cba7f4dc027 100644 --- a/test/parallel/test-process-uid-gid.js +++ b/test/parallel/test-process-uid-gid.js @@ -24,11 +24,13 @@ const common = require('../common'); const assert = require('assert'); -if (common.isWindows) { - // uid/gid functions are POSIX only - assert.strictEqual(process.getuid, undefined); +if (common.isWindows || !common.isMainThread) { + // uid/gid functions are POSIX only, setters are main-thread only. + if (common.isMainThread) { + assert.strictEqual(process.getuid, undefined); + assert.strictEqual(process.getgid, undefined); + } assert.strictEqual(process.setuid, undefined); - assert.strictEqual(process.getgid, undefined); assert.strictEqual(process.setgid, undefined); return; } diff --git a/test/parallel/test-process-umask-mask.js b/test/parallel/test-process-umask-mask.js index 8ec8fc0074ac1b..26ea5b1d9d21de 100644 --- a/test/parallel/test-process-umask-mask.js +++ b/test/parallel/test-process-umask-mask.js @@ -6,6 +6,9 @@ const common = require('../common'); const assert = require('assert'); +if (!common.isMainThread) + common.skip('process.umask is not available in Workers'); + let mask; if (common.isWindows) { diff --git a/test/parallel/test-process-umask.js b/test/parallel/test-process-umask.js index 1d496f05ef5027..b1e0c08bcbd352 100644 --- a/test/parallel/test-process-umask.js +++ b/test/parallel/test-process-umask.js @@ -22,6 +22,8 @@ 'use strict'; const common = require('../common'); const assert = require('assert'); +if (!common.isMainThread) + common.skip('process.umask is not available in Workers'); // Note in Windows one can only set the "user" bits. let mask; diff --git a/test/parallel/test-repl-require.js b/test/parallel/test-repl-require.js index 3d1af7f2f8c15e..ecab4ba698911b 100644 --- a/test/parallel/test-repl-require.js +++ b/test/parallel/test-repl-require.js @@ -5,6 +5,9 @@ const fixtures = require('../common/fixtures'); const assert = require('assert'); const net = require('net'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + process.chdir(fixtures.fixturesDir); const repl = require('repl'); diff --git a/test/parallel/test-repl-sigint-nested-eval.js b/test/parallel/test-repl-sigint-nested-eval.js index ea07393527b828..28e4d44b235cde 100644 --- a/test/parallel/test-repl-sigint-nested-eval.js +++ b/test/parallel/test-repl-sigint-nested-eval.js @@ -4,6 +4,8 @@ if (common.isWindows) { // No way to send CTRL_C_EVENT to processes from JS right now. common.skip('platform not supported'); } +if (!common.isMainThread) + common.skip('No signal handling available in Workers'); const assert = require('assert'); const spawn = require('child_process').spawn; diff --git a/test/parallel/test-repl-sigint.js b/test/parallel/test-repl-sigint.js index 14cafd0463709f..8ad0b2f5c2c853 100644 --- a/test/parallel/test-repl-sigint.js +++ b/test/parallel/test-repl-sigint.js @@ -4,6 +4,8 @@ if (common.isWindows) { // No way to send CTRL_C_EVENT to processes from JS right now. common.skip('platform not supported'); } +if (!common.isMainThread) + common.skip('No signal handling available in Workers'); const assert = require('assert'); const spawn = require('child_process').spawn; diff --git a/test/parallel/test-repl-tab-complete.js b/test/parallel/test-repl-tab-complete.js index 6485f8bd17f58b..57c1615e6ffe2b 100644 --- a/test/parallel/test-repl-tab-complete.js +++ b/test/parallel/test-repl-tab-complete.js @@ -26,6 +26,9 @@ const assert = require('assert'); const fixtures = require('../common/fixtures'); const hasInspector = process.config.variables.v8_enable_inspector === 1; +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + // We have to change the directory to ../fixtures before requiring repl // in order to make the tests for completion of node_modules work properly // since repl modifies module.paths. diff --git a/test/parallel/test-require-symlink.js b/test/parallel/test-require-symlink.js index d245c21dd1fdb4..7aad3ee0a7e7e9 100644 --- a/test/parallel/test-require-symlink.js +++ b/test/parallel/test-require-symlink.js @@ -4,6 +4,8 @@ const common = require('../common'); if (!common.canCreateSymLink()) common.skip('insufficient privileges'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); const assert = require('assert'); const { spawn } = require('child_process'); diff --git a/test/parallel/test-setproctitle.js b/test/parallel/test-setproctitle.js index 1ab6bff6a30848..e5f6858f786ba3 100644 --- a/test/parallel/test-setproctitle.js +++ b/test/parallel/test-setproctitle.js @@ -5,6 +5,8 @@ const common = require('../common'); // FIXME add sunos support if (common.isSunOS) common.skip(`Unsupported platform [${process.platform}]`); +if (!common.isMainThread) + common.skip('Setting the process title from Workers is not supported'); const assert = require('assert'); const exec = require('child_process').exec; diff --git a/test/parallel/test-signal-args.js b/test/parallel/test-signal-args.js index d9fa6df347ddaa..7b72ed6dcb92d5 100644 --- a/test/parallel/test-signal-args.js +++ b/test/parallel/test-signal-args.js @@ -3,9 +3,10 @@ const common = require('../common'); const assert = require('assert'); -if (common.isWindows) { +if (common.isWindows) common.skip('Sending signals with process.kill is not supported on Windows'); -} +if (!common.isMainThread) + common.skip('No signal handling available in Workers'); process.once('SIGINT', common.mustCall((signal) => { assert.strictEqual(signal, 'SIGINT'); diff --git a/test/parallel/test-signal-handler.js b/test/parallel/test-signal-handler.js index a5c900695a54cb..05ec4e7f73faf5 100644 --- a/test/parallel/test-signal-handler.js +++ b/test/parallel/test-signal-handler.js @@ -25,6 +25,8 @@ const common = require('../common'); if (common.isWindows) common.skip('SIGUSR1 and SIGHUP signals are not supported'); +if (!common.isMainThread) + common.skip('Signal handling in Workers is not supported'); console.log(`process.pid: ${process.pid}`); diff --git a/test/parallel/test-stdio-pipe-access.js b/test/parallel/test-stdio-pipe-access.js index ef84bb83803b26..d32c5f0ba9c2e7 100644 --- a/test/parallel/test-stdio-pipe-access.js +++ b/test/parallel/test-stdio-pipe-access.js @@ -1,5 +1,7 @@ 'use strict'; -require('../common'); +const common = require('../common'); +if (!common.isMainThread) + common.skip('Workers don’t have process-like stdio'); // Test if Node handles acessing process.stdin if it is a redirected // pipe without deadlocking diff --git a/test/parallel/test-stdio-pipe-redirect.js b/test/parallel/test-stdio-pipe-redirect.js index b47f5b9cf44af5..60f16b5cb2f6df 100644 --- a/test/parallel/test-stdio-pipe-redirect.js +++ b/test/parallel/test-stdio-pipe-redirect.js @@ -1,5 +1,7 @@ 'use strict'; -require('../common'); +const common = require('../common'); +if (!common.isMainThread) + common.skip('Workers don’t have process-like stdio'); // Test if Node handles redirecting one child process stdout to another // process stdin without crashing. diff --git a/test/parallel/test-timers-immediate-unref-nested-once.js b/test/parallel/test-timers-immediate-unref-nested-once.js new file mode 100644 index 00000000000000..00efce9bcbc8d3 --- /dev/null +++ b/test/parallel/test-timers-immediate-unref-nested-once.js @@ -0,0 +1,9 @@ +'use strict'; + +const common = require('../common'); + +// This immediate should not execute as it was unrefed +// and nothing else is keeping the event loop alive +setImmediate(() => { + setImmediate(common.mustNotCall()).unref(); +}); diff --git a/test/parallel/test-timers-immediate-unref-simple.js b/test/parallel/test-timers-immediate-unref-simple.js index 68497460328c32..369894fcdebbae 100644 --- a/test/parallel/test-timers-immediate-unref-simple.js +++ b/test/parallel/test-timers-immediate-unref-simple.js @@ -2,6 +2,11 @@ const common = require('../common'); +if (!common.isMainThread) { + // Note that test-timers-immediate-unref-nested-once works instead. + common.skip('Worker bootstrapping works differently -> different timing'); +} + // This immediate should not execute as it was unrefed // and nothing else is keeping the event loop alive setImmediate(common.mustNotCall()).unref(); diff --git a/test/parallel/test-trace-events-all.js b/test/parallel/test-trace-events-all.js index 51d0f3c5a265f1..585d4acd18e6d8 100644 --- a/test/parallel/test-trace-events-all.js +++ b/test/parallel/test-trace-events-all.js @@ -4,6 +4,9 @@ const assert = require('assert'); const cp = require('child_process'); const fs = require('fs'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + const CODE = 'setTimeout(() => { for (var i = 0; i < 100000; i++) { "test" + i } }, 1)'; const FILE_NAME = 'node_trace.1.log'; diff --git a/test/parallel/test-trace-events-api.js b/test/parallel/test-trace-events-api.js index a68a2850a37c45..5dd4c623c022bb 100644 --- a/test/parallel/test-trace-events-api.js +++ b/test/parallel/test-trace-events-api.js @@ -5,6 +5,8 @@ const common = require('../common'); if (!common.hasTracing) common.skip('missing trace events'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); const assert = require('assert'); const cp = require('child_process'); diff --git a/test/parallel/test-trace-events-async-hooks.js b/test/parallel/test-trace-events-async-hooks.js index e0b5e0625bea6f..9dd71eda4c6d73 100644 --- a/test/parallel/test-trace-events-async-hooks.js +++ b/test/parallel/test-trace-events-async-hooks.js @@ -5,6 +5,9 @@ const cp = require('child_process'); const fs = require('fs'); const util = require('util'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + const CODE = 'setTimeout(() => { for (var i = 0; i < 100000; i++) { "test" + i } }, 1)'; const FILE_NAME = 'node_trace.1.log'; diff --git a/test/parallel/test-trace-events-binding.js b/test/parallel/test-trace-events-binding.js index e52f1c769f5283..f50c1321840980 100644 --- a/test/parallel/test-trace-events-binding.js +++ b/test/parallel/test-trace-events-binding.js @@ -4,6 +4,9 @@ const assert = require('assert'); const cp = require('child_process'); const fs = require('fs'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + const CODE = ` process.binding("trace_events").emit( 'b'.charCodeAt(0), 'custom', diff --git a/test/parallel/test-trace-events-bootstrap.js b/test/parallel/test-trace-events-bootstrap.js index 6f8c76564a8ac4..c0f8a49c855b92 100644 --- a/test/parallel/test-trace-events-bootstrap.js +++ b/test/parallel/test-trace-events-bootstrap.js @@ -6,6 +6,9 @@ const path = require('path'); const fs = require('fs'); const tmpdir = require('../common/tmpdir'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + const names = [ 'environment', 'nodeStart', diff --git a/test/parallel/test-trace-events-category-used.js b/test/parallel/test-trace-events-category-used.js index a98cb350371da9..d5aff4e1361ce6 100644 --- a/test/parallel/test-trace-events-category-used.js +++ b/test/parallel/test-trace-events-category-used.js @@ -3,6 +3,9 @@ const common = require('../common'); const assert = require('assert'); const cp = require('child_process'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + const CODE = `console.log( process.binding("trace_events").categoryGroupEnabled("custom") );`; diff --git a/test/parallel/test-trace-events-file-pattern.js b/test/parallel/test-trace-events-file-pattern.js index 46059ad31d58b2..c8098a43979bb4 100644 --- a/test/parallel/test-trace-events-file-pattern.js +++ b/test/parallel/test-trace-events-file-pattern.js @@ -5,6 +5,9 @@ const assert = require('assert'); const cp = require('child_process'); const fs = require('fs'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + tmpdir.refresh(); process.chdir(tmpdir.path); diff --git a/test/parallel/test-trace-events-fs-sync.js b/test/parallel/test-trace-events-fs-sync.js index 60ab5efa3a1d3d..491679c9c03f22 100644 --- a/test/parallel/test-trace-events-fs-sync.js +++ b/test/parallel/test-trace-events-fs-sync.js @@ -4,6 +4,9 @@ const assert = require('assert'); const cp = require('child_process'); const fs = require('fs'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + const tests = new Array(); const traceFile = 'node_trace.1.log'; diff --git a/test/parallel/test-trace-events-metadata.js b/test/parallel/test-trace-events-metadata.js index 8b6e97ce35ff3c..440aa00a9c216c 100644 --- a/test/parallel/test-trace-events-metadata.js +++ b/test/parallel/test-trace-events-metadata.js @@ -4,6 +4,9 @@ const assert = require('assert'); const cp = require('child_process'); const fs = require('fs'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + const CODE = 'setTimeout(() => { for (var i = 0; i < 100000; i++) { "test" + i } }, 1)'; const FILE_NAME = 'node_trace.1.log'; diff --git a/test/parallel/test-trace-events-none.js b/test/parallel/test-trace-events-none.js index a3f0338f28af35..e6beea3b975355 100644 --- a/test/parallel/test-trace-events-none.js +++ b/test/parallel/test-trace-events-none.js @@ -4,6 +4,9 @@ const assert = require('assert'); const cp = require('child_process'); const fs = require('fs'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + const CODE = 'setTimeout(() => { for (var i = 0; i < 100000; i++) { "test" + i } }, 1)'; const FILE_NAME = 'node_trace.1.log'; diff --git a/test/parallel/test-trace-events-perf.js b/test/parallel/test-trace-events-perf.js index 57ac0e3142f66d..8ca5a41eac230e 100644 --- a/test/parallel/test-trace-events-perf.js +++ b/test/parallel/test-trace-events-perf.js @@ -6,6 +6,9 @@ const path = require('path'); const fs = require('fs'); const tmpdir = require('../common/tmpdir'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + if (process.argv[2] === 'child') { const { performance } = require('perf_hooks'); diff --git a/test/parallel/test-trace-events-process-exit.js b/test/parallel/test-trace-events-process-exit.js index 9f164ee6279720..aeb9fc19c2982d 100644 --- a/test/parallel/test-trace-events-process-exit.js +++ b/test/parallel/test-trace-events-process-exit.js @@ -4,6 +4,9 @@ const assert = require('assert'); const cp = require('child_process'); const fs = require('fs'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + const tmpdir = require('../common/tmpdir'); const FILE_NAME = 'node_trace.1.log'; diff --git a/test/parallel/test-trace-events-v8.js b/test/parallel/test-trace-events-v8.js index 49c34b8f17bbb2..325789e96865a8 100644 --- a/test/parallel/test-trace-events-v8.js +++ b/test/parallel/test-trace-events-v8.js @@ -4,6 +4,9 @@ const assert = require('assert'); const cp = require('child_process'); const fs = require('fs'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + const CODE = 'setTimeout(() => { for (var i = 0; i < 100000; i++) { "test" + i } }, 1)'; const FILE_NAME = 'node_trace.1.log'; diff --git a/test/parallel/test-trace-events-vm.js b/test/parallel/test-trace-events-vm.js index 3dc6e263e1b6a1..45724e3e4c88dc 100644 --- a/test/parallel/test-trace-events-vm.js +++ b/test/parallel/test-trace-events-vm.js @@ -6,6 +6,9 @@ const path = require('path'); const fs = require('fs'); const tmpdir = require('../common/tmpdir'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + const names = [ 'ContextifyScript::New', 'RunInThisContext', diff --git a/test/parallel/test-worker-cleanup-handles.js b/test/parallel/test-worker-cleanup-handles.js new file mode 100644 index 00000000000000..2fd11398da07b7 --- /dev/null +++ b/test/parallel/test-worker-cleanup-handles.js @@ -0,0 +1,30 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker, isMainThread, parentPort } = require('worker_threads'); +const { Server } = require('net'); +const fs = require('fs'); + +if (isMainThread) { + const w = new Worker(__filename); + let fd = null; + w.on('message', common.mustCall((fd_) => { + assert.strictEqual(typeof fd_, 'number'); + fd = fd_; + })); + w.on('exit', common.mustCall((code) => { + if (fd === -1) { + // This happens when server sockets don’t have file descriptors, + // i.e. on Windows. + return; + } + common.expectsError(() => fs.fstatSync(fd), + { code: 'EBADF' }); + })); +} else { + const server = new Server(); + server.listen(0); + parentPort.postMessage(server._handle.fd); + server.unref(); +} diff --git a/test/parallel/test-worker-dns-terminate.js b/test/parallel/test-worker-dns-terminate.js new file mode 100644 index 00000000000000..8526f739873e74 --- /dev/null +++ b/test/parallel/test-worker-dns-terminate.js @@ -0,0 +1,15 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const { Worker } = require('worker_threads'); + +const w = new Worker(` +const dns = require('dns'); +dns.lookup('nonexistent.org', () => {}); +require('worker_threads').parentPort.postMessage('0'); +`, { eval: true }); + +w.on('message', common.mustCall(() => { + // This should not crash the worker during a DNS request. + w.terminate(common.mustCall()); +})); diff --git a/test/parallel/test-worker-esmodule.js b/test/parallel/test-worker-esmodule.js new file mode 100644 index 00000000000000..de4657452c9c7f --- /dev/null +++ b/test/parallel/test-worker-esmodule.js @@ -0,0 +1,11 @@ +// Flags: --experimental-worker --experimental-modules +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +const w = new Worker(fixtures.path('worker-script.mjs')); +w.on('message', common.mustCall((message) => { + assert.strictEqual(message, 'Hello, world!'); +})); diff --git a/test/parallel/test-worker-memory.js b/test/parallel/test-worker-memory.js new file mode 100644 index 00000000000000..3c7627b02cbe43 --- /dev/null +++ b/test/parallel/test-worker-memory.js @@ -0,0 +1,41 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const util = require('util'); +const { Worker } = require('worker_threads'); + +const numWorkers = +process.env.JOBS || require('os').cpus().length; + +// Verify that a Worker's memory isn't kept in memory after the thread finishes. + +function run(n, done) { + if (n <= 0) + return done(); + const worker = new Worker( + 'require(\'worker_threads\').parentPort.postMessage(2 + 2)', + { eval: true }); + worker.on('message', common.mustCall((value) => { + assert.strictEqual(value, 4); + })); + worker.on('exit', common.mustCall(() => { + run(n - 1, done); + })); +} + +const startStats = process.memoryUsage(); +let finished = 0; +for (let i = 0; i < numWorkers; ++i) { + run(60 / numWorkers, () => { + if (++finished === numWorkers) { + const finishStats = process.memoryUsage(); + // A typical value for this ratio would be ~1.15. + // 5 as a upper limit is generous, but the main point is that we + // don't have the memory of 50 Isolates/Node.js environments just lying + // around somewhere. + assert.ok(finishStats.rss / startStats.rss < 5, + 'Unexpected memory overhead: ' + + util.inspect([startStats, finishStats])); + } + }); +} diff --git a/test/parallel/test-worker-nexttick-terminate.js b/test/parallel/test-worker-nexttick-terminate.js new file mode 100644 index 00000000000000..804285710b4bf4 --- /dev/null +++ b/test/parallel/test-worker-nexttick-terminate.js @@ -0,0 +1,20 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const { Worker } = require('worker_threads'); + +// Checks that terminating in the middle of `process.nextTick()` does not +// Crash the process. + +const w = new Worker(` +require('worker_threads').parentPort.postMessage('0'); +process.nextTick(() => { + while(1); +}); +`, { eval: true }); + +w.on('message', common.mustCall(() => { + setTimeout(() => { + w.terminate(common.mustCall()); + }, 1); +})); diff --git a/test/parallel/test-worker-stdio.js b/test/parallel/test-worker-stdio.js new file mode 100644 index 00000000000000..366f9e694dd2fa --- /dev/null +++ b/test/parallel/test-worker-stdio.js @@ -0,0 +1,43 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const util = require('util'); +const { Writable } = require('stream'); +const { Worker, isMainThread } = require('worker_threads'); + +class BufferingWritable extends Writable { + constructor() { + super(); + this.chunks = []; + } + + _write(chunk, enc, cb) { + this.chunks.push(chunk); + cb(); + } + + get buffer() { + return Buffer.concat(this.chunks); + } +} + +if (isMainThread) { + const original = new BufferingWritable(); + const passed = new BufferingWritable(); + + const w = new Worker(__filename, { stdin: true, stdout: true }); + const source = fs.createReadStream(process.execPath); + source.pipe(w.stdin); + source.pipe(original); + w.stdout.pipe(passed); + + passed.on('finish', common.mustCall(() => { + assert.strictEqual(original.buffer.compare(passed.buffer), 0, + `Original: ${util.inspect(original.buffer)}, ` + + `Actual: ${util.inspect(passed.buffer)}`); + })); +} else { + process.stdin.pipe(process.stdout); +} diff --git a/test/parallel/test-worker-syntax-error-file.js b/test/parallel/test-worker-syntax-error-file.js new file mode 100644 index 00000000000000..b1f8fd5f681343 --- /dev/null +++ b/test/parallel/test-worker-syntax-error-file.js @@ -0,0 +1,18 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +// Do not use isMainThread so that this test itself can be run inside a Worker. +if (!process.env.HAS_STARTED_WORKER) { + process.env.HAS_STARTED_WORKER = 1; + const w = new Worker(fixtures.path('syntax', 'bad_syntax.js')); + w.on('message', common.mustNotCall()); + w.on('error', common.mustCall((err) => { + assert(/SyntaxError/.test(err)); + })); +} else { + throw new Error('foo'); +} diff --git a/test/parallel/test-worker-syntax-error.js b/test/parallel/test-worker-syntax-error.js new file mode 100644 index 00000000000000..d54888053900da --- /dev/null +++ b/test/parallel/test-worker-syntax-error.js @@ -0,0 +1,17 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +// Do not use isMainThread so that this test itself can be run inside a Worker. +if (!process.env.HAS_STARTED_WORKER) { + process.env.HAS_STARTED_WORKER = 1; + const w = new Worker('abc)', { eval: true }); + w.on('message', common.mustNotCall()); + w.on('error', common.mustCall((err) => { + assert(/SyntaxError/.test(err)); + })); +} else { + throw new Error('foo'); +} diff --git a/test/parallel/test-worker-uncaught-exception-async.js b/test/parallel/test-worker-uncaught-exception-async.js new file mode 100644 index 00000000000000..c3f0c8dec59f09 --- /dev/null +++ b/test/parallel/test-worker-uncaught-exception-async.js @@ -0,0 +1,19 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +// Do not use isMainThread so that this test itself can be run inside a Worker. +if (!process.env.HAS_STARTED_WORKER) { + process.env.HAS_STARTED_WORKER = 1; + const w = new Worker(__filename); + w.on('message', common.mustNotCall()); + w.on('error', common.mustCall((err) => { + 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 00000000000000..b7d9f8a2928ec1 --- /dev/null +++ b/test/parallel/test-worker-uncaught-exception.js @@ -0,0 +1,17 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +// Do not use isMainThread so that this test itself can be run inside a Worker. +if (!process.env.HAS_STARTED_WORKER) { + process.env.HAS_STARTED_WORKER = 1; + const w = new Worker(__filename); + w.on('message', common.mustNotCall()); + w.on('error', common.mustCall((err) => { + assert(/^Error: foo$/.test(err)); + })); +} else { + throw new Error('foo'); +} diff --git a/test/parallel/test-worker-unsupported-path.js b/test/parallel/test-worker-unsupported-path.js new file mode 100644 index 00000000000000..b4de6fd1976013 --- /dev/null +++ b/test/parallel/test-worker-unsupported-path.js @@ -0,0 +1,27 @@ +// Flags: --experimental-worker +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +{ + 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 00000000000000..7946e9cf1c138b --- /dev/null +++ b/test/parallel/test-worker-unsupported-things.js @@ -0,0 +1,41 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker, isMainThread, parentPort } = require('worker_threads'); + +if (isMainThread) { + const w = new Worker(__filename); + w.on('message', common.mustCall((message) => { + assert.strictEqual(message, true); + })); +} else { + { + const before = process.title; + process.title += ' in worker'; + assert.strictEqual(process.title, before); + } + + { + const before = process.debugPort; + process.debugPort++; + assert.strictEqual(process.debugPort, before); + } + + assert.strictEqual('abort' in process, false); + assert.strictEqual('chdir' in process, false); + assert.strictEqual('setuid' in process, false); + assert.strictEqual('seteuid' in process, false); + assert.strictEqual('setgid' in process, false); + assert.strictEqual('setegid' in process, false); + assert.strictEqual('setgroups' in process, false); + assert.strictEqual('initgroups' in process, false); + + assert.strictEqual('_startProfilerIdleNotifier' in process, false); + assert.strictEqual('_stopProfilerIdleNotifier' in process, false); + assert.strictEqual('_debugProcess' in process, false); + assert.strictEqual('_debugPause' in process, false); + assert.strictEqual('_debugEnd' in process, false); + + parentPort.postMessage(true); +} diff --git a/test/parallel/test-worker.js b/test/parallel/test-worker.js new file mode 100644 index 00000000000000..6cd64fce9b4eb8 --- /dev/null +++ b/test/parallel/test-worker.js @@ -0,0 +1,18 @@ +// Flags: --experimental-worker +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker, isMainThread, parentPort } = require('worker_threads'); + +if (isMainThread) { + const w = new Worker(__filename); + w.on('message', common.mustCall((message) => { + assert.strictEqual(message, 'Hello, world!'); + })); +} else { + setImmediate(() => { + process.nextTick(() => { + parentPort.postMessage('Hello, world!'); + }); + }); +} diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index 971296915ceecb..c16624a79e6c83 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -35,7 +35,12 @@ common.crashOnUnhandledRejection(); delete providers.HTTP2STREAM; delete providers.HTTP2PING; delete providers.HTTP2SETTINGS; + // TODO(addaleax): Test for these delete providers.STREAMPIPE; + delete providers.MESSAGEPORT; + delete providers.WORKER; + if (!common.isMainThread) + delete providers.INSPECTORJSBINDING; const objKeys = Object.keys(providers); if (objKeys.length > 0) @@ -278,7 +283,8 @@ if (common.hasCrypto) { // eslint-disable-line node-core/crypto-check testInitialized(req, 'SendWrap'); } -if (process.config.variables.v8_enable_inspector !== 0) { +if (process.config.variables.v8_enable_inspector !== 0 && + common.isMainThread) { const binding = process.binding('inspector'); const handle = new binding.Connection(() => {}); testInitialized(handle, 'Connection'); diff --git a/test/sequential/test-buffer-creation-regression.js b/test/sequential/test-buffer-creation-regression.js index 8c3a09848c9fa9..07deb1db0fe377 100644 --- a/test/sequential/test-buffer-creation-regression.js +++ b/test/sequential/test-buffer-creation-regression.js @@ -30,7 +30,6 @@ try { } catch (e) { if (e instanceof RangeError && acceptableOOMErrors.includes(e.message)) common.skip(`Unable to allocate ${size} bytes for ArrayBuffer`); - throw e; } diff --git a/test/sequential/test-fs-watch.js b/test/sequential/test-fs-watch.js index 3c8ae0eba7d278..1326e62bc10da4 100644 --- a/test/sequential/test-fs-watch.js +++ b/test/sequential/test-fs-watch.js @@ -28,6 +28,9 @@ const path = require('path'); const tmpdir = require('../common/tmpdir'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + const expectFilePath = common.isWindows || common.isLinux || common.isOSX || diff --git a/test/sequential/test-init.js b/test/sequential/test-init.js index 5dd8d9ab14ea4f..1b1b09ee833191 100644 --- a/test/sequential/test-init.js +++ b/test/sequential/test-init.js @@ -25,6 +25,9 @@ const assert = require('assert'); const child = require('child_process'); const fixtures = require('../common/fixtures'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + if (process.env.TEST_INIT) { return process.stdout.write('Loaded successfully!'); } diff --git a/test/sequential/test-inspector-overwrite-config.js b/test/sequential/test-inspector-overwrite-config.js index 8b641a0048484a..46cb922402f360 100644 --- a/test/sequential/test-inspector-overwrite-config.js +++ b/test/sequential/test-inspector-overwrite-config.js @@ -13,6 +13,10 @@ const common = require('../common'); const assert = require('assert'); + +if (!common.isMainThread) + common.skip('--require does not work with Workers'); + const inspector = require('inspector'); const msg = 'Test inspector logging'; let asserted = false; diff --git a/test/sequential/test-performance.js b/test/sequential/test-performance.js index c5065227a6a410..fdadb0be14ae5d 100644 --- a/test/sequential/test-performance.js +++ b/test/sequential/test-performance.js @@ -4,6 +4,9 @@ const common = require('../common'); const assert = require('assert'); const { performance } = require('perf_hooks'); +if (!common.isMainThread) + common.skip('bootstrapping workers works differently'); + assert(performance); assert(performance.nodeTiming); assert.strictEqual(typeof performance.timeOrigin, 'number'); diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js index e7c7aa69da5e95..be72893832373a 100644 --- a/tools/doc/type-parser.js +++ b/tools/doc/type-parser.js @@ -117,7 +117,9 @@ const customTypesMap = { 'Tracing': 'tracing.html#tracing_tracing_object', 'URL': 'url.html#url_the_whatwg_url_api', - 'URLSearchParams': 'url.html#url_class_urlsearchparams' + 'URLSearchParams': 'url.html#url_class_urlsearchparams', + + 'MessagePort': 'worker.html#worker_class_messageport' }; const arrayPart = /(?:\[])+$/; diff --git a/tools/run-worker.js b/tools/run-worker.js new file mode 100644 index 00000000000000..7590e460a404ae --- /dev/null +++ b/tools/run-worker.js @@ -0,0 +1,11 @@ +'use strict'; +if (typeof require === 'undefined') { + console.log('1..0 # Skipped: Not being run as CommonJS'); + process.exit(0); +} + +const path = require('path'); +const { Worker } = require('worker_threads'); + +new Worker(path.resolve(process.cwd(), process.argv[2])) + .on('exit', (code) => process.exitCode = code); diff --git a/tools/test.py b/tools/test.py index e5581e8da41c44..27a27224389e41 100755 --- a/tools/test.py +++ b/tools/test.py @@ -1375,6 +1375,8 @@ def BuildOptions(): help="Expect test cases to fail", default=False, action="store_true") result.add_option("--valgrind", help="Run tests through valgrind", default=False, action="store_true") + result.add_option("--worker", help="Run parallel tests inside a worker context", + default=False, action="store_true") result.add_option("--check-deopts", help="Check tests for permanent deoptimizations", default=False, action="store_true") result.add_option("--cat", help="Print the source of the tests", @@ -1617,6 +1619,11 @@ def Main(): options.node_args.append("--always-opt") options.progress = "deopts" + if options.worker: + run_worker = join(workspace, "tools", "run-worker.js") + options.node_args.append('--experimental-worker') + options.node_args.append(run_worker) + shell = abspath(options.shell) buildspace = dirname(shell)