Skip to content

Instantly share code, notes, and snippets.

@isaacs
Created March 16, 2012 17:15
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save isaacs/2051217 to your computer and use it in GitHub Desktop.
Save isaacs/2051217 to your computer and use it in GitHub Desktop.
/*
# Use cases:
1. Efficiently ignore all data coming from a stdio stream (ie, >/dev/null)
2. Interact with stdio data in a JS Stream object
3. Inherit stdio streams from the parent process
4. Connect arbitrary handle to a stdio stream
5. Connect an arbitrary handle to a child stdio stream other than 0,1,2
# Constraints:
1. No numeric FD arguments
2. Use cases 1, 2, and 3 must work on all platforms.
3. Use case 4 and 5 should work on all platforms, but MAY generate an ENOSYS
error immediately if this is not possible.
# Proposal:
The spawn argument 'streams' can be used to set the stdio file descriptor
streams that will be opened in the child process. This is an array, where
each index corresponds to a FD. The values can be:
* any falsey value: Use the default behavior for 0,1,2, otherwise the same
as 'ignore'
* 'inherit': The same as passing process.streams[n]. This is the default
setting for stdout and stderr in the fork/cluster case. Akin to doing
`customFds:[0,1,2]`.
* 'stream': Open a pipe_pair, and create a stream object in JS in the parent.
This is the default setting for 0,1,2 in the spawn case, and for 0 in the
fork/cluster case. Akin to doing `customFds:[-1,-1,-1]`.
* 'ignore': Do nothing. No JS Stream object, no pipe_wrap thing. Direct all
bytes to /dev/null.
* Any handle object: Use the socket/file/etc. handle as the FD in the child.
This is a bit like the deprecated `customFds` option, but not limited to
only 3 members.
The resulting ChildProcess object has a 'streams' member which is
an array with keys corresponding to the file descriptors opened for
the child.
* child.stdin === child.streams[0]
* child.stdout === child.streams[1]
* child.stderr === child.streams[2]
The value of child.streams[n] depends on what it was set up with:
* falsey value: null
* 'inherit': process.streams[n]
* 'stream': a JavaScript object, instanceof Stream.
* 'ignore': null
* Specific handle object: a reference to the handle provided.
As a shorthand, the 'streams' member can be set to one of:
* 'ignore'
* 'stream'
* 'inherit'
* Any handle that is both readable and writable
which sets up the standard 3 file descriptors in the specified mode,
but does not open any additional file descriptors in the child.
Node sets an environment variable for the child process, such that it
knows to add the correct kind of reference to the process.streams Array.
* process.streams[0] === process.stdin
* process.streams[1] === process.stdout
* process.streams[2] === process.stderr
* process.streams[3...] === depends on context
*/
var spawn = require('child_process').spawn
var c = 'command'
var a = ['arg1', 'arg2']
var child
// Simple cases.
// 1. Ignore all data from a stdio stream
// No pipe of any sort.
child = spawn(c, a, { streams: 'ignore' })
child = spawn(c, a, { streams: ['ignore','ignore','ignore'] })
assert(child.stdin === null)
assert(child.stdout === null)
assert(child.stderr === null)
// 2. Get a JS Stream object. This is the default case for spawn and exec.
child = spawn(c, a, { streams: 'stream' })
child = spawn(c, a, { streams: ['stream','stream','stream'] })
assert(child.stdin instanceof Stream)
assert(child.stdin.writable)
assert(child.stdout instanceof Stream)
assert(child.stdout.readable)
assert(child.stderr instanceof Stream)
assert(child.stderr.readable)
// 3. Inherit stdio from parent process
child = spawn(c, a, { streams: 'inherit' })
child = spawn(c, a, { streams: ['inherit','inherit','inherit'] })
assert(child.stdin === null)
assert(child.stdout === null)
assert(child.stderr === null)
// 4. assign to arbitrary sockets, files, etc.
// In this case, the chunks are not passed up through the JS layer
// unless necessary due to implementation details. The JS stream object
// is thus not exposed, and possibly not created.
// 4a. connect a socket
var sock = require('net').connect('some-socket')
child = spawn(c, a, { streams: sock })
child = spawn(c, a, { streams: [sock, sock, sock] })
assert(child.stdin === null)
assert(child.stdout === null)
assert(child.stderr === null)
// 4b. connect a file
var fin = require('fs').createReadStream('input.txt')
var fout = require('fs').createWriteStream('output.log')
var ferr = require('fs').createWriteStream('error.log')
child = spawn(c, a, { streams: [fin, fout, ferr] })
assert(child.stdin === null)
assert(child.stdout === null)
assert(child.stderr === null)
// 4c. connect a sibling process.
// Note that assigning a writable stream to stdout or stderr
// will cause it to inherit/share, rather than pipe.
// Maybe too magical?
var sib = spawn(c, a, { streams: 'stream' })
// child.stdout -> sib.stdin
// child.stdin <- sib.stdout
// child.stderr === sib.stderr
child = spawn(c, a, { streams: [sib.stdout, sib.stdin, sib.stderr] })
assert(child.stdin === null)
assert(child.stdout === null)
assert(child.stderr === null)
// 5. open additional stdio file descriptors in child
// Necessary for qmail and other random Unix programs.
// Nice-to-have on Windows.
//
// Removed: named streams and in/out pairs. Too much API overhead.
child = spawn(c, a, { streams: [ 'stream', 'stream', 'stream', // stdio
'stream', // fd=3
'ignore', // fd=4
null, // fd=5
process.stdin, // fd=6
process.stdout, // fd=7
sock, // fd=8
ferr, // fd=9
sib.stdout, // fd=10
fin // fd=11
] })
assert(child.streams[0] === child.stdin)
assert(child.streams[1] === child.stdout)
assert(child.streams[2] === child.stderr)
// custom streams
// streams[3] = 'stream'
assert(child.streams[3] instanceof Stream)
assert(child.streams[3].readable)
assert(child.streams[3].writable)
// kind of redundant, but important for completeness, and
// to space out fd assignments in the child proc.
// eg, if you wanted to do something with fd=4 and 6, but not 5.
// Opened in the child and connected to /dev/null
// streams[4] = 'ignore'
assert(child.streams[4] === null)
// streams[5] = null
assert(child.streams[5] === null)
// passing process.stdin as an arg.
//
// Significant question:
// Is this inheriting stdin, or piping into stdin?
// I think it ought ot be inheriting stdin (so that it shows up
// as read-only in the child process), but the semantics here are
// not clear from the API.
assert(child.streams[6] === null)
// streams[7] = process.stdout
// Same questionable semantics as #6 above.
assert(child.streams[7] === null)
// attached to socket.
// opened as read/write in child. Semantically equivalent to
// sock.pipe(child.streams[8]).pipe(sock)
assert(child.streams[8] === null)
// attached to file handle.
// opened as write-only in child. Semantically equivalent to
// child.streams[9].pile(ferr)
assert(child.streams[9] === null)
// streams[10] = sib.stdout
// Same semantic problems as passing process.std*
assert(child.streams[10] === null)
// streams[11] = fin
// opened as read-only in child
// like: fin.pipe(child.streams.hoo)
assert(child.streams[11] === null)
/*
# Access to/awareness of custom streams in child process.
When a custom stream is assigned to a child proc, node starts the
procss with:
NODE_CUSTOM_STREAMS=<info>
On unix, this info can be the file descriptor. On windows, it may
contain additional hinting about the sort of thing that it is (socket,
file, etc.)
On startup, node sets up the process.streams array, so that the child
process can interact with them.
*/
// Example: An implementation of a child_process.fork()-like thing.
function fork (module, args, opts) {
if (typeof module !== 'string') {
// fork(args, opts)
opts = args;
args = module;
module = __filename;
}
if (!Array.isArray(args)) {
opts = args;
args = [];
}
if (!opts) {
opts = {};
}
var argv = process.execArgv.concat(module).concat(args)
// don't monkey with the original object passed in,
// or its streams settings. We do need to change them, though.
var opts_ = {};
for (var i in opts) opts_[i] = opts[i];
// create with 'stream' for 0 and 3, even though we'll remove them below.
var streams = opts_.streams = opts.streams ? opts.streams.slice(0) : [];
streams[0] = 'stream';
streams[1] = opts.silent ? 'ignore' : streams[1] || process.stdout;
streams[2] = opts.silent ? 'ignore' : streams[2] || process.stderr;
streams[3] = 'stream';
var env = opts_.env = util._extend({}, opts.env || process.env);
env.NODE_CHANNEL_FD = 42; // don't ask.
var child = spawn(process.execPath, argv, opts_);
// the child's stdin is used for passing messages to it.
var sendStream = child.streams[0];
child.streams[0] = null;
child.send = function(m) {
sendStream.write(JSON.stringify(m) + '\n');
};
// the childs fd=3 is used for getting messages from it.
var recvStream = child.streams[3];
child.streams[3] = null;
var buf = '';
var StringDecoder = require('string_decoder').StringDecoder;
var decoder = new StringDecoder('utf8');
recvStream.on('data', function(chunk) {
buf += decoder.write(chunk);
var c = buf.split('\n');
// keep whatever unfinished message was at the end.
buf = c.pop();
// emit the messages.
c.forEach(function (msg) {
child.emit('message', JSON.parse(msg));
});
});
return child;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment