worker: enable stdio
Provide `stdin`, `stdout` and `stderr` options for the `Worker` constructor, and make these available to the worker thread under their usual names. The default for `stdin` is an empty stream, the default for `stdout` and `stderr` is redirecting to the parent thread’s corresponding stdio streams. PR-URL: https://github.com/nodejs/node/pull/20876 Reviewed-By: Gireesh Punathil <gpunathi@in.ibm.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Shingo Inoue <leko.noor@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com> Reviewed-By: John-David Dalton <john.david.dalton@gmail.com> Reviewed-By: Gus Caplan <me@gus.host>
This commit is contained in:
parent
147ea5e3d7
commit
ddefa0f2c5
@ -240,7 +240,7 @@ Most Node.js APIs are available inside of it.
|
|||||||
Notable differences inside a Worker environment are:
|
Notable differences inside a Worker environment are:
|
||||||
|
|
||||||
- The [`process.stdin`][], [`process.stdout`][] and [`process.stderr`][]
|
- The [`process.stdin`][], [`process.stdout`][] and [`process.stderr`][]
|
||||||
properties are set to `null`.
|
may be redirected by the parent thread.
|
||||||
- The [`require('worker').isMainThread`][] property is set to `false`.
|
- The [`require('worker').isMainThread`][] property is set to `false`.
|
||||||
- The [`require('worker').parentPort`][] message port is available,
|
- The [`require('worker').parentPort`][] message port is available,
|
||||||
- [`process.exit()`][] does not stop the whole program, just the single thread,
|
- [`process.exit()`][] does not stop the whole program, just the single thread,
|
||||||
@ -313,6 +313,13 @@ if (isMainThread) {
|
|||||||
described in the [HTML structured clone algorithm][], and an error will be
|
described in the [HTML structured clone algorithm][], and an error will be
|
||||||
thrown if the object cannot be cloned (e.g. because it contains
|
thrown if the object cannot be cloned (e.g. because it contains
|
||||||
`function`s).
|
`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'
|
### Event: 'error'
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
@ -377,6 +384,41 @@ Opposite of `unref()`, calling `ref()` on a previously `unref()`ed worker will
|
|||||||
behavior). If the worker is `ref()`ed, calling `ref()` again will have
|
behavior). If the worker is `ref()`ed, calling `ref()` again will have
|
||||||
no effect.
|
no effect.
|
||||||
|
|
||||||
|
### worker.stderr
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
* {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
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
* {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
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
* {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])
|
### worker.terminate([callback])
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: REPLACEME
|
added: REPLACEME
|
||||||
|
@ -6,7 +6,10 @@ const {
|
|||||||
ERR_UNKNOWN_STDIN_TYPE,
|
ERR_UNKNOWN_STDIN_TYPE,
|
||||||
ERR_UNKNOWN_STREAM_TYPE
|
ERR_UNKNOWN_STREAM_TYPE
|
||||||
} = require('internal/errors').codes;
|
} = require('internal/errors').codes;
|
||||||
const { isMainThread } = require('internal/worker');
|
const {
|
||||||
|
isMainThread,
|
||||||
|
workerStdio
|
||||||
|
} = require('internal/worker');
|
||||||
|
|
||||||
exports.setup = setupStdio;
|
exports.setup = setupStdio;
|
||||||
|
|
||||||
@ -17,8 +20,7 @@ function setupStdio() {
|
|||||||
|
|
||||||
function getStdout() {
|
function getStdout() {
|
||||||
if (stdout) return stdout;
|
if (stdout) return stdout;
|
||||||
if (!isMainThread)
|
if (!isMainThread) return workerStdio.stdout;
|
||||||
return new (require('stream').Writable)({ write(b, e, cb) { cb(); } });
|
|
||||||
stdout = createWritableStdioStream(1);
|
stdout = createWritableStdioStream(1);
|
||||||
stdout.destroySoon = stdout.destroy;
|
stdout.destroySoon = stdout.destroy;
|
||||||
stdout._destroy = function(er, cb) {
|
stdout._destroy = function(er, cb) {
|
||||||
@ -34,8 +36,7 @@ function setupStdio() {
|
|||||||
|
|
||||||
function getStderr() {
|
function getStderr() {
|
||||||
if (stderr) return stderr;
|
if (stderr) return stderr;
|
||||||
if (!isMainThread)
|
if (!isMainThread) return workerStdio.stderr;
|
||||||
return new (require('stream').Writable)({ write(b, e, cb) { cb(); } });
|
|
||||||
stderr = createWritableStdioStream(2);
|
stderr = createWritableStdioStream(2);
|
||||||
stderr.destroySoon = stderr.destroy;
|
stderr.destroySoon = stderr.destroy;
|
||||||
stderr._destroy = function(er, cb) {
|
stderr._destroy = function(er, cb) {
|
||||||
@ -51,8 +52,7 @@ function setupStdio() {
|
|||||||
|
|
||||||
function getStdin() {
|
function getStdin() {
|
||||||
if (stdin) return stdin;
|
if (stdin) return stdin;
|
||||||
if (!isMainThread)
|
if (!isMainThread) return workerStdio.stdin;
|
||||||
return new (require('stream').Readable)({ read() { this.push(null); } });
|
|
||||||
|
|
||||||
const tty_wrap = process.binding('tty_wrap');
|
const tty_wrap = process.binding('tty_wrap');
|
||||||
const fd = 0;
|
const fd = 0;
|
||||||
|
@ -5,6 +5,7 @@ const EventEmitter = require('events');
|
|||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
|
const { Readable, Writable } = require('stream');
|
||||||
const {
|
const {
|
||||||
ERR_INVALID_ARG_TYPE,
|
ERR_INVALID_ARG_TYPE,
|
||||||
ERR_WORKER_NEED_ABSOLUTE_PATH,
|
ERR_WORKER_NEED_ABSOLUTE_PATH,
|
||||||
@ -29,6 +30,7 @@ const isMainThread = threadId === 0;
|
|||||||
|
|
||||||
const kOnMessageListener = Symbol('kOnMessageListener');
|
const kOnMessageListener = Symbol('kOnMessageListener');
|
||||||
const kHandle = Symbol('kHandle');
|
const kHandle = Symbol('kHandle');
|
||||||
|
const kName = Symbol('kName');
|
||||||
const kPort = Symbol('kPort');
|
const kPort = Symbol('kPort');
|
||||||
const kPublicPort = Symbol('kPublicPort');
|
const kPublicPort = Symbol('kPublicPort');
|
||||||
const kDispose = Symbol('kDispose');
|
const kDispose = Symbol('kDispose');
|
||||||
@ -36,6 +38,12 @@ const kOnExit = Symbol('kOnExit');
|
|||||||
const kOnMessage = Symbol('kOnMessage');
|
const kOnMessage = Symbol('kOnMessage');
|
||||||
const kOnCouldNotSerializeErr = Symbol('kOnCouldNotSerializeErr');
|
const kOnCouldNotSerializeErr = Symbol('kOnCouldNotSerializeErr');
|
||||||
const kOnErrorMessage = Symbol('kOnErrorMessage');
|
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');
|
const debug = util.debuglog('worker');
|
||||||
|
|
||||||
@ -129,6 +137,72 @@ function setupPortReferencing(port, eventEmitter, eventName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 {
|
class Worker extends EventEmitter {
|
||||||
constructor(filename, options = {}) {
|
constructor(filename, options = {}) {
|
||||||
super();
|
super();
|
||||||
@ -154,8 +228,25 @@ class Worker extends EventEmitter {
|
|||||||
this[kPort].on('message', (data) => this[kOnMessage](data));
|
this[kPort].on('message', (data) => this[kOnMessage](data));
|
||||||
this[kPort].start();
|
this[kPort].start();
|
||||||
this[kPort].unref();
|
this[kPort].unref();
|
||||||
|
this[kPort][kWaitingStreams] = 0;
|
||||||
debug(`[${threadId}] created Worker with ID ${this.threadId}`);
|
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();
|
const { port1, port2 } = new MessageChannel();
|
||||||
this[kPublicPort] = port1;
|
this[kPublicPort] = port1;
|
||||||
this[kPublicPort].on('message', (message) => this.emit('message', message));
|
this[kPublicPort].on('message', (message) => this.emit('message', message));
|
||||||
@ -165,7 +256,8 @@ class Worker extends EventEmitter {
|
|||||||
filename,
|
filename,
|
||||||
doEval: !!options.eval,
|
doEval: !!options.eval,
|
||||||
workerData: options.workerData,
|
workerData: options.workerData,
|
||||||
publicPort: port2
|
publicPort: port2,
|
||||||
|
hasStdin: !!options.stdin
|
||||||
}, [port2]);
|
}, [port2]);
|
||||||
// Actually start the new thread now that everything is in place.
|
// Actually start the new thread now that everything is in place.
|
||||||
this[kHandle].startThread();
|
this[kHandle].startThread();
|
||||||
@ -197,6 +289,16 @@ class Worker extends EventEmitter {
|
|||||||
return this[kOnCouldNotSerializeErr]();
|
return this[kOnCouldNotSerializeErr]();
|
||||||
case 'errorMessage':
|
case 'errorMessage':
|
||||||
return this[kOnErrorMessage](message.error);
|
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}`);
|
assert.fail(`Unknown worker message type ${message.type}`);
|
||||||
@ -207,6 +309,18 @@ class Worker extends EventEmitter {
|
|||||||
this[kHandle] = null;
|
this[kHandle] = null;
|
||||||
this[kPort] = null;
|
this[kPort] = null;
|
||||||
this[kPublicPort] = 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) {
|
postMessage(...args) {
|
||||||
@ -243,6 +357,27 @@ class Worker extends EventEmitter {
|
|||||||
|
|
||||||
return this[kHandle].threadId;
|
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;
|
let originalFatalException;
|
||||||
@ -256,10 +391,14 @@ function setupChild(evalScript) {
|
|||||||
|
|
||||||
port.on('message', (message) => {
|
port.on('message', (message) => {
|
||||||
if (message.type === 'loadScript') {
|
if (message.type === 'loadScript') {
|
||||||
const { filename, doEval, workerData, publicPort } = message;
|
const { filename, doEval, workerData, publicPort, hasStdin } = message;
|
||||||
publicWorker.parentPort = publicPort;
|
publicWorker.parentPort = publicPort;
|
||||||
setupPortReferencing(publicPort, publicPort, 'message');
|
setupPortReferencing(publicPort, publicPort, 'message');
|
||||||
publicWorker.workerData = workerData;
|
publicWorker.workerData = workerData;
|
||||||
|
|
||||||
|
if (!hasStdin)
|
||||||
|
workerStdio.stdin.push(null);
|
||||||
|
|
||||||
debug(`[${threadId}] starts worker script ${filename} ` +
|
debug(`[${threadId}] starts worker script ${filename} ` +
|
||||||
`(eval = ${eval}) at cwd = ${process.cwd()}`);
|
`(eval = ${eval}) at cwd = ${process.cwd()}`);
|
||||||
port.unref();
|
port.unref();
|
||||||
@ -271,6 +410,14 @@ function setupChild(evalScript) {
|
|||||||
require('module').runMain();
|
require('module').runMain();
|
||||||
}
|
}
|
||||||
return;
|
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}`);
|
assert.fail(`Unknown worker message type ${message.type}`);
|
||||||
@ -317,11 +464,24 @@ function deserializeError(error) {
|
|||||||
error.byteLength).toString('utf8');
|
error.byteLength).toString('utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = {
|
module.exports = {
|
||||||
MessagePort,
|
MessagePort,
|
||||||
MessageChannel,
|
MessageChannel,
|
||||||
threadId,
|
threadId,
|
||||||
Worker,
|
Worker,
|
||||||
setupChild,
|
setupChild,
|
||||||
isMainThread
|
isMainThread,
|
||||||
|
workerStdio
|
||||||
};
|
};
|
||||||
|
43
test/parallel/test-worker-stdio.js
Normal file
43
test/parallel/test-worker-stdio.js
Normal file
@ -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');
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user