Skip to content

Commit 9e26dab

Browse files
committed
child_process.spawnNode
For making easy worker processes.
1 parent 205b9be commit 9e26dab

9 files changed

Lines changed: 238 additions & 8 deletions

doc/api/child_processes.markdown

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,39 @@ amount of data allowed on stdout or stderr - if this value is exceeded then
179179
the child process is killed.
180180

181181

182+
### child_process.spawnNode(modulePath, arguments, options)
183+
184+
This is a special case of the `spawn()` functionality for spawning Node
185+
processes. In addition to having all the methods in a normal ChildProcess
186+
instance, the returned object, has a communication channel built-in. The
187+
channel is written to with `child.send(message)` and messages are recieved
188+
by a `'message'` event on the child.
189+
190+
For example:
191+
192+
var n = spawnNode(__dirname + '/sub.js');
193+
194+
n.on('message', function(m) {
195+
console.log('PARENT got message:', m);
196+
});
197+
198+
n.send({ hello: 'world' });
199+
200+
And then the child script, `'sub.js'` would might look like this:
201+
202+
process.on('message', function(m) {
203+
console.log('CHILD got message:', m);
204+
});
205+
206+
process.send({ foo: 'bar' });
207+
208+
In the child the `process` object will have a `send()` method, and `process`
209+
will emit objects each time it receives a message on its channel.
210+
211+
By default the spawned Node process will have the stdin, stdout, stderr associated
212+
with the parent's. This can be overridden by using the `customFds` option.
213+
214+
182215
### child.kill(signal='SIGTERM')
183216

184217
Send a signal to the child process. If no argument is given, the process will

lib/child_process.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,60 @@ var spawn = exports.spawn = function(path, args /*, options, customFds */) {
3232
return child;
3333
};
3434

35+
36+
function setupChannel(target, fd) {
37+
target._channel = new Stream(fd);
38+
target._channel.writable = true;
39+
target._channel.readable = true;
40+
41+
target._channel.resume();
42+
target._channel.setEncoding('ascii');
43+
44+
var buffer = '';
45+
target._channel.on('data', function(d) {
46+
buffer += d;
47+
var i;
48+
while ((i = buffer.indexOf('\n')) >= 0) {
49+
var json = buffer.slice(0, i);
50+
buffer = buffer.slice(i + 1);
51+
var m = JSON.parse(json);
52+
target.emit('message', m);
53+
}
54+
});
55+
56+
target.send = function(m) {
57+
target._channel.write(JSON.stringify(m) + '\n');
58+
};
59+
}
60+
61+
62+
exports.spawnNode = function(modulePath, args, options) {
63+
if (!options) options = {};
64+
options.wantChannel = true;
65+
66+
if (!args) args = [];
67+
args.unshift(modulePath);
68+
69+
// Unless they gave up customFds, just use the parent process
70+
if (!options.customFds) options.customFds = [0, 1, 2];
71+
72+
var child = spawn(process.execPath, args, options);
73+
74+
setupChannel(child, child.fds[3]);
75+
76+
child.on('exit', function() {
77+
child._channel.destroy();
78+
});
79+
80+
return child;
81+
};
82+
83+
84+
exports._spawnNodeChild = function(fd) {
85+
setupChannel(process, fd);
86+
};
87+
88+
3589
exports.exec = function(command /*, options, callback */) {
3690
var _slice = Array.prototype.slice;
3791
var args = ['/bin/sh', ['-c', command]].concat(_slice.call(arguments, 1));
@@ -240,6 +294,12 @@ ChildProcess.prototype.spawn = function(path, args, options, customFds) {
240294
envPairs.push(key + '=' + env[key]);
241295
}
242296

297+
if (options && options.wantChannel) {
298+
// The FILLMEIN will be replaced in C land with an integer!
299+
// AWFUL! :D
300+
envPairs.push('NODE_CHANNEL_FD=FILLMEIN');
301+
}
302+
243303
var fds = this._internal.spawn(path,
244304
args,
245305
cwd,

src/node.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
startup.processKillAndExit();
3939
startup.processSignalHandlers();
4040

41+
startup.processChannel();
42+
4143
startup.removedMethods();
4244

4345
startup.resolveArgv0();
@@ -307,6 +309,19 @@
307309
};
308310
};
309311

312+
313+
startup.processChannel = function() {
314+
// If we were spawned with env NODE_CHANNEL_FD then load that up and
315+
// start parsing data from that stream.
316+
if (process.env.NODE_CHANNEL_FD) {
317+
var fd = parseInt(process.env.NODE_CHANNEL_FD);
318+
assert(fd >= 0);
319+
var cp = NativeModule.require('child_process');
320+
cp._spawnNodeChild(fd);
321+
assert(process.send);
322+
}
323+
}
324+
310325
startup._removedProcessMethods = {
311326
'assert': 'process.assert() use require("assert").ok() instead',
312327
'debug': 'process.debug() use console.error() instead',

src/node_child_process.cc

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
#include <sys/wait.h>
3636
#endif
3737

38+
#include <sys/socket.h> /* socketpair */
39+
#include <sys/un.h>
40+
3841
# ifdef __APPLE__
3942
# include <crt_externs.h>
4043
# define environ (*_NSGetEnviron())
@@ -153,7 +156,7 @@ Handle<Value> ChildProcess::Spawn(const Arguments& args) {
153156
// Copy fourth argument, args[3], into a c-string array called env.
154157
Local<Array> env_handle = Local<Array>::Cast(args[3]);
155158
int envc = env_handle->Length();
156-
char **env = new char*[envc+1]; // heap allocated to detect errors
159+
char **env = new char*[envc + 1]; // heap allocated to detect errors
157160
env[envc] = NULL;
158161
for (int i = 0; i < envc; i++) {
159162
String::Utf8Value pair(env_handle->Get(Integer::New(i))->ToString());
@@ -206,7 +209,7 @@ Handle<Value> ChildProcess::Spawn(const Arguments& args) {
206209
String::New("setgid argument must be a number or a string")));
207210
}
208211

209-
212+
int channel_fd = -1;
210213

211214
int r = child->Spawn(argv[0],
212215
argv,
@@ -218,7 +221,8 @@ Handle<Value> ChildProcess::Spawn(const Arguments& args) {
218221
custom_uid,
219222
custom_uname,
220223
custom_gid,
221-
custom_gname);
224+
custom_gname,
225+
&channel_fd);
222226

223227
if (custom_uname != NULL) free(custom_uname);
224228
if (custom_gname != NULL) free(custom_gname);
@@ -235,7 +239,8 @@ Handle<Value> ChildProcess::Spawn(const Arguments& args) {
235239
return ThrowException(Exception::Error(String::New("Error spawning")));
236240
}
237241

238-
Local<Array> a = Array::New(3);
242+
243+
Local<Array> a = Array::New(channel_fd >= 0 ? 4 : 3);
239244

240245
assert(fds[0] >= 0);
241246
a->Set(0, Integer::New(fds[0])); // stdin
@@ -244,6 +249,10 @@ Handle<Value> ChildProcess::Spawn(const Arguments& args) {
244249
assert(fds[2] >= 0);
245250
a->Set(2, Integer::New(fds[2])); // stderr
246251

252+
if (channel_fd >= 0) {
253+
a->Set(3, Integer::New(channel_fd));
254+
}
255+
247256
return scope.Close(a);
248257
}
249258

@@ -291,6 +300,8 @@ void ChildProcess::Stop() {
291300
// Note that args[0] must be the same as the "file" param. This is an
292301
// execvp() requirement.
293302
//
303+
// TODO: The arguments are rediculously long. Needs to be put into a struct.
304+
//
294305
int ChildProcess::Spawn(const char *file,
295306
char *const args[],
296307
const char *cwd,
@@ -301,7 +312,8 @@ int ChildProcess::Spawn(const char *file,
301312
int custom_uid,
302313
char *custom_uname,
303314
int custom_gid,
304-
char *custom_gname) {
315+
char *custom_gname,
316+
int* channel) {
305317
HandleScope scope;
306318
assert(pid_ == -1);
307319
assert(!ev_is_active(&child_watcher_));
@@ -332,11 +344,37 @@ int ChildProcess::Spawn(const char *file,
332344
SetCloseOnExec(stderr_pipe[1]);
333345
}
334346

347+
348+
// The channel will be used by spawnNode() for a little JSON channel.
349+
// The pointer is used to pass one end of the socket pair back to the
350+
// parent.
351+
// channel_fds[0] is for the parent
352+
// channel_fds[1] is for the child
353+
int channel_fds[2] = { -1, -1 };
354+
355+
#define NODE_CHANNEL_FD "NODE_CHANNEL_FD"
356+
357+
for (int i = 0; env[i]; i++) {
358+
if (!strncmp(env[i], NODE_CHANNEL_FD, sizeof NODE_CHANNEL_FD - 1)) {
359+
if (socketpair(AF_UNIX, SOCK_STREAM, 0, channel_fds)) {
360+
perror("socketpair()");
361+
return -1;
362+
}
363+
364+
assert(channel_fds[0] >= 0 && channel_fds[1] >= 0);
365+
366+
SetNonBlocking(channel_fds[0]);
367+
SetNonBlocking(channel_fds[1]);
368+
// Write over the FILLMEIN :D
369+
sprintf(env[i], NODE_CHANNEL_FD "=%d", channel_fds[1]);
370+
}
371+
}
372+
335373
// Save environ in the case that we get it clobbered
336374
// by the child process.
337375
char **save_our_env = environ;
338376

339-
switch (pid_ = vfork()) {
377+
switch (pid_ = fork()) {
340378
case -1: // Error.
341379
Stop();
342380
return -4;
@@ -429,7 +467,11 @@ int ChildProcess::Spawn(const char *file,
429467
_exit(127);
430468
}
431469

432-
470+
// Close the parent's end of the channel.
471+
if (channel_fds[0] >= 0) {
472+
close(channel_fds[0]);
473+
channel_fds[0] = -1;
474+
}
433475

434476
environ = env;
435477

@@ -472,6 +514,17 @@ int ChildProcess::Spawn(const char *file,
472514
stdio_fds[2] = custom_fds[2];
473515
}
474516

517+
// Close the child's end of the channel.
518+
if (channel_fds[1] >= 0) {
519+
close(channel_fds[1]);
520+
channel_fds[1] = -1;
521+
assert(channel_fds[0] >= 0);
522+
assert(channel);
523+
*channel = channel_fds[0];
524+
} else {
525+
*channel = -1;
526+
}
527+
475528
return 0;
476529
}
477530

src/node_child_process.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ class ChildProcess : ObjectWrap {
8989
int custom_uid,
9090
char *custom_uname,
9191
int custom_gid,
92-
char *custom_gname);
92+
char *custom_gname,
93+
int* channel);
9394

9495
// Simple syscall wrapper. Does not disable the watcher. onexit will be
9596
// called still.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
var assert = require('assert');
2+
3+
console.log("NODE_CHANNEL_FD", process.env.NODE_CHANNEL_FD);
4+
assert.ok(process.env.NODE_CHANNEL_FD);
5+
6+
var fd = parseInt(process.env.NODE_CHANNEL_FD);
7+
assert.ok(fd >= 0);
8+
9+
process.exit(0);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
var assert = require('assert');
2+
3+
process.on('message', function(m) {
4+
console.log('CHILD got message:', m);
5+
assert.ok(m.hello);
6+
// Note that we have to force exit.
7+
process.exit();
8+
});
9+
10+
process.send({ foo: 'bar' });
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
var assert = require('assert');
2+
var spawn = require('child_process').spawn;
3+
var common = require('../common');
4+
5+
var sub = common.fixturesDir + '/child-process-channel.js';
6+
7+
var child = spawn(process.execPath, [ sub ], {
8+
customFds: [0, 1, 2],
9+
wantChannel: true
10+
});
11+
12+
console.log("fds", child.fds);
13+
14+
assert.ok(child.fds.length == 4);
15+
assert.ok(child.fds[3] >= 0);
16+
17+
var childExitCode = -1;
18+
19+
child.on('exit', function(code) {
20+
childExitCode = code;
21+
});
22+
23+
process.on('exit', function() {
24+
assert.ok(childExitCode == 0);
25+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
var assert = require('assert');
2+
var common = require('../common');
3+
var spawnNode = require('child_process').spawnNode;
4+
5+
var n = spawnNode(common.fixturesDir + '/child-process-spawn-node.js');
6+
7+
var messageCount = 0;
8+
9+
n.on('message', function(m) {
10+
console.log('PARENT got message:', m);
11+
assert.ok(m.foo);
12+
messageCount++;
13+
});
14+
15+
n.send({ hello: 'world' });
16+
17+
var childExitCode = -1;
18+
n.on('exit', function(c) {
19+
childExitCode = c;
20+
});
21+
22+
process.on('exit', function() {
23+
assert.ok(childExitCode == 0);
24+
});

0 commit comments

Comments
 (0)