Skip to content

Commit

Permalink
child_process: create proper public API for channel
Browse files Browse the repository at this point in the history
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.

Refs: nodejs#9322
Refs: nodejs#9313
  • Loading branch information
addaleax committed Nov 5, 2019
1 parent 973f324 commit cc7ff9e
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 27 deletions.
20 changes: 20 additions & 0 deletions doc/api/child_process.md
Original file line number Diff line number Diff line change
Expand Up @@ -1018,13 +1018,33 @@ See [Advanced Serialization][] for more details.
### subprocess.channel
<!-- YAML
added: v7.1.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/????
description: The object no longer accidentally exposes native C++ bindings.
-->

* {Object} A pipe representing the IPC channel to the child process.

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
Expand Down
28 changes: 28 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,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/????
description: The object no longer accidentally exposes native C++ bindings.
-->

* {Object}
Expand All @@ -633,6 +637,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
Expand Down
4 changes: 2 additions & 2 deletions lib/child_process.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,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();
});
}

Expand Down
58 changes: 42 additions & 16 deletions lib/internal/child_process.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,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
Expand Down Expand Up @@ -103,8 +104,8 @@ const handleConversion = {
// The worker should keep track of the socket
message.key = socket.server._connectionKey;

var firstTime = !this.channel.sockets.send[message.key];
var socketList = getSocketList('send', this, 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
// and when asked to close it should query the socket status from
Expand Down Expand Up @@ -503,30 +504,55 @@ 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. ' +
'Use ChildProcess.channel instead.';

let serialization;
function setupChannel(target, channel, serializationMode) {
target.channel = channel;
const control = new Control(channel);
target.channel = control;
target[kChannelHandle] = channel;

Object.defineProperty(target, '_channel', {
get: deprecate(() => {
Expand All @@ -542,8 +568,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 {
Expand Down Expand Up @@ -791,11 +815,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);
}
Expand Down Expand Up @@ -850,6 +874,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);
Expand Down Expand Up @@ -1006,7 +1031,7 @@ function getValidStdio(stdio, sync) {


function getSocketList(type, worker, key) {
const sockets = worker.channel.sockets[type];
const sockets = worker[kChannelHandle].sockets[type];
var socketList = sockets[key];
if (!socketList) {
var Construct = type === 'send' ? SocketListSend : SocketListReceive;
Expand Down Expand Up @@ -1049,6 +1074,7 @@ function spawnSync(options) {

module.exports = {
ChildProcess,
kChannelHandle,
setupChannel,
getValidStdio,
stdioStringToArray,
Expand Down
9 changes: 5 additions & 4 deletions lib/internal/process/stdio.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,17 @@ function getMainThreadStdio() {
break;

case 'PIPE':
case 'TCP':
var net = require('net');
case 'TCP': {
const net = require('net');

// It could be that process has been started with an IPC channel
// sitting on fd=0, in such case the pipe for this fd is already
// present and creating a new one will lead to the assertion failure
// in libuv.
if (process.channel && process.channel.fd === fd) {
const kChannelHandle = require('internal/child_process');
stdin = new net.Socket({
handle: process.channel,
handle: process[kChannelHandle],
readable: true,
writable: false,
manualStart: true
Expand All @@ -92,7 +93,7 @@ function getMainThreadStdio() {
// Make sure the stdin can't be `.end()`-ed
stdin._writableState.ended = true;
break;

}
default:
// Provide a dummy contentless input for e.g. non-console
// Windows applications.
Expand Down
9 changes: 6 additions & 3 deletions test/parallel/test-child-process-recv-handle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) => {
Expand Down
3 changes: 1 addition & 2 deletions test/parallel/test-child-process-silent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit cc7ff9e

Please sign in to comment.