child_process: create proper public API for channel

Instead of exposing the C++ bindings object as `subprocess.channel`
or `process.channel`, provide the “control” object that was
previously used internally as the public-facing variant of it.

This should be better than returning the raw pipe object, and
matches the original intention (when the `channel` property was
first added) of providing a proper way to `.ref()` or `.unref()`
the channel.

PR-URL: https://github.com/nodejs/node/pull/30165
Refs: https://github.com/nodejs/node/pull/9322
Refs: https://github.com/nodejs/node/issues/9313
Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
Reviewed-By: Denys Otrishko <shishugi@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Anna Henningsen 2019-10-29 20:15:31 +01:00 committed by Ruben Bridgewater
parent d6a2badef7
commit e65bed1b7e
No known key found for this signature in database
GPG Key ID: F07496B3EB3C1762
7 changed files with 100 additions and 23 deletions

View File

@ -1018,6 +1018,10 @@ See [Advanced Serialization][] for more details.
### `subprocess.channel`
<!-- YAML
added: v7.1.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/30165
description: The object no longer accidentally exposes native C++ bindings.
-->
* {Object} A pipe representing the IPC channel to the child process.
@ -1025,6 +1029,22 @@ added: v7.1.0
The `subprocess.channel` property is a reference to the child's IPC channel. If
no IPC channel currently exists, this property is `undefined`.
#### `subprocess.channel.ref()`
<!-- YAML
added: v7.1.0
-->
This method makes the IPC channel keep the event loop of the parent process
running if `.unref()` has been called before.
#### `subprocess.channel.unref()`
<!-- YAML
added: v7.1.0
-->
This method makes the IPC channel not keep the event loop of the parent process
running, and lets it finish even while the channel is open.
### `subprocess.connected`
<!-- YAML
added: v0.7.2

View File

@ -626,6 +626,10 @@ $ bash -c 'exec -a customArgv0 ./node'
## `process.channel`
<!-- YAML
added: v7.1.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/30165
description: The object no longer accidentally exposes native C++ bindings.
-->
* {Object}
@ -635,6 +639,30 @@ If the Node.js process was spawned with an IPC channel (see the
property is a reference to the IPC channel. If no IPC channel exists, this
property is `undefined`.
### `process.channel.ref()`
<!-- YAML
added: v7.1.0
-->
This method makes the IPC channel keep the event loop of the process
running if `.unref()` has been called before.
Typically, this is managed through the number of `'disconnect'` and `'message'`
listeners on the `process` object. However, this method can be used to
explicitly request a specific behavior.
### `process.channel.unref()`
<!-- YAML
added: v7.1.0
-->
This method makes the IPC channel not keep the event loop of the process
running, and lets it finish even while the channel is open.
Typically, this is managed through the number of `'disconnect'` and `'message'`
listeners on the `process` object. However, this method can be used to
explicitly request a specific behavior.
## `process.chdir(directory)`
<!-- YAML
added: v0.1.17

View File

@ -122,10 +122,10 @@ function _forkChild(fd, serializationMode) {
p.unref();
const control = setupChannel(process, p, serializationMode);
process.on('newListener', function onNewListener(name) {
if (name === 'message' || name === 'disconnect') control.ref();
if (name === 'message' || name === 'disconnect') control.refCounted();
});
process.on('removeListener', function onRemoveListener(name) {
if (name === 'message' || name === 'disconnect') control.unref();
if (name === 'message' || name === 'disconnect') control.unrefCounted();
});
}

View File

@ -91,8 +91,9 @@ function createWritableStdioStream(fd) {
// an error when trying to use it again. In that case, create the socket
// using the existing handle instead of the fd.
if (process.channel && process.channel.fd === fd) {
const { kChannelHandle } = require('internal/child_process');
stream = new net.Socket({
handle: process.channel,
handle: process[kChannelHandle],
readable: false,
writable: true
});

View File

@ -66,6 +66,7 @@ let freeParser;
let HTTPParser;
const MAX_HANDLE_RETRANSMISSIONS = 3;
const kChannelHandle = Symbol('kChannelHandle');
const kIsUsedAsStdio = Symbol('kIsUsedAsStdio');
// This object contain function to convert TCP objects to native handle objects
@ -108,7 +109,7 @@ const handleConversion = {
// The worker should keep track of the socket
message.key = socket.server._connectionKey;
const firstTime = !this.channel.sockets.send[message.key];
const firstTime = !this[kChannelHandle].sockets.send[message.key];
const socketList = getSocketList('send', this, message.key);
// The server should no longer expose a .connection property
@ -508,22 +509,45 @@ ChildProcess.prototype.unref = function() {
};
class Control extends EventEmitter {
#channel = null;
#refs = 0;
#refExplicitlySet = false;
constructor(channel) {
super();
this.channel = channel;
this.refs = 0;
this.#channel = channel;
}
ref() {
if (++this.refs === 1) {
this.channel.ref();
// The methods keeping track of the counter are being used to track the
// listener count on the child process object as well as when writes are
// in progress. Once the user has explicitly requested a certain state, these
// methods become no-ops in order to not interfere with the user's intentions.
refCounted() {
if (++this.#refs === 1 && !this.#refExplicitlySet) {
this.#channel.ref();
}
}
unref() {
if (--this.refs === 0) {
this.channel.unref();
unrefCounted() {
if (--this.#refs === 0 && !this.#refExplicitlySet) {
this.#channel.unref();
this.emit('unref');
}
}
ref() {
this.#refExplicitlySet = true;
this.#channel.ref();
}
unref() {
this.#refExplicitlySet = true;
this.#channel.unref();
}
get fd() {
return this.#channel ? this.#channel.fd : undefined;
}
}
const channelDeprecationMsg = '_channel is deprecated. ' +
@ -531,7 +555,9 @@ const channelDeprecationMsg = '_channel is deprecated. ' +
let serialization;
function setupChannel(target, channel, serializationMode) {
target.channel = channel;
const control = new Control(channel);
target.channel = control;
target[kChannelHandle] = channel;
ObjectDefineProperty(target, '_channel', {
get: deprecate(() => {
@ -547,8 +573,6 @@ function setupChannel(target, channel, serializationMode) {
target._handleQueue = null;
target._pendingMessage = null;
const control = new Control(channel);
if (serialization === undefined)
serialization = require('internal/child_process/serialization');
const {
@ -796,11 +820,11 @@ function setupChannel(target, channel, serializationMode) {
if (wasAsyncWrite) {
req.oncomplete = () => {
control.unref();
control.unrefCounted();
if (typeof callback === 'function')
callback(null);
};
control.ref();
control.refCounted();
} else if (typeof callback === 'function') {
process.nextTick(callback, null);
}
@ -855,6 +879,7 @@ function setupChannel(target, channel, serializationMode) {
// This marks the fact that the channel is actually disconnected.
this.channel = null;
this[kChannelHandle] = null;
if (this._pendingMessage)
closePendingHandle(this);
@ -1011,7 +1036,7 @@ function getValidStdio(stdio, sync) {
function getSocketList(type, worker, key) {
const sockets = worker.channel.sockets[type];
const sockets = worker[kChannelHandle].sockets[type];
let socketList = sockets[key];
if (!socketList) {
const Construct = type === 'send' ? SocketListSend : SocketListReceive;
@ -1054,6 +1079,7 @@ function spawnSync(options) {
module.exports = {
ChildProcess,
kChannelHandle,
setupChannel,
getValidStdio,
stdioStringToArray,

View File

@ -36,7 +36,9 @@ else
function master() {
// spawn() can only create one IPC channel so we use stdin/stdout as an
// ad-hoc command channel.
const proc = spawn(process.execPath, [__filename, 'worker'], {
const proc = spawn(process.execPath, [
'--expose-internals', __filename, 'worker'
], {
stdio: ['pipe', 'pipe', 'pipe', 'ipc']
});
let handle = null;
@ -57,12 +59,13 @@ function master() {
}
function worker() {
process.channel.readStop(); // Make messages batch up.
const { kChannelHandle } = require('internal/child_process');
process[kChannelHandle].readStop(); // Make messages batch up.
process.stdout.ref();
process.stdout.write('ok\r\n');
process.stdin.once('data', common.mustCall((data) => {
assert.strictEqual(data.toString(), 'ok\r\n');
process.channel.readStart();
process[kChannelHandle].readStart();
}));
let n = 0;
process.on('message', common.mustCall((msg, handle) => {

View File

@ -42,8 +42,7 @@ if (process.argv[2] === 'pipe') {
const child = childProcess.fork(process.argv[1], ['pipe'], { silent: true });
// Allow child process to self terminate
child.channel.close();
child.channel = null;
child.disconnect();
child.on('exit', function() {
process.exit(0);