From 92da636b977282d58f1d43560387f3e9bff6be4d Mon Sep 17 00:00:00 2001 From: Orlando Vazquez Date: Tue, 1 Jun 2010 00:56:08 -0700 Subject: [PATCH] Add a parameter to spawn() that sets the child's stdio file descriptors. After the child is forked, these file descriptors will get dup2()'d to STDIN, STDIO, and STDERR. (API may be changed.) --- lib/child_process.js | 57 ++++++++---- src/node_child_process.cc | 85 +++++++++++++----- src/node_child_process.h | 2 +- test/fixtures/stdio-filter.js | 11 +++ test/simple/test-child-process-custom-fds.js | 93 ++++++++++++++++++++ 5 files changed, 207 insertions(+), 41 deletions(-) create mode 100644 test/fixtures/stdio-filter.js create mode 100644 test/simple/test-child-process-custom-fds.js diff --git a/lib/child_process.js b/lib/child_process.js index ebca206df4..849f878d0a 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -4,9 +4,9 @@ var Stream = require('net').Stream; var InternalChildProcess = process.binding('child_process').ChildProcess; -var spawn = exports.spawn = function (path, args, env) { +var spawn = exports.spawn = function (path, args, env, customFds) { var child = new ChildProcess(); - child.spawn(path, args, env); + child.spawn(path, args, env, customFds); return child; }; @@ -104,14 +104,14 @@ function ChildProcess () { stderr.addListener('close', function () { stderrClosed = true; - if (gotCHLD && stdoutClosed) { + if (gotCHLD && (!self.stdout || stdoutClosed)) { self.emit('exit', exitCode, termSignal); } }); stdout.addListener('close', function () { stdoutClosed = true; - if (gotCHLD && stderrClosed) { + if (gotCHLD && (!self.stderr || stderrClosed)) { self.emit('exit', exitCode, termSignal); } }); @@ -120,8 +120,11 @@ function ChildProcess () { gotCHLD = true; exitCode = code; termSignal = signal; - stdin.end(); - if (!stdout.readable && !stderr.readable) { + if (self.stdin) { + self.stdin.end(); + } + if ( (!self.stdout || !self.stdout.readable) + && (!self.stderr || !self.stderr.readable)) { self.emit('exit', exitCode, termSignal); } }; @@ -136,7 +139,7 @@ ChildProcess.prototype.kill = function (sig) { }; -ChildProcess.prototype.spawn = function (path, args, env) { +ChildProcess.prototype.spawn = function (path, args, env, customFds) { args = args || []; env = env || process.env; var envPairs = []; @@ -146,20 +149,36 @@ ChildProcess.prototype.spawn = function (path, args, env) { envPairs.push(key + "=" + env[key]); } - var fds = this._internal.spawn(path, args, envPairs); + customFds = customFds || [-1, -1, -1]; + var fds = this.fds = this._internal.spawn(path, args, envPairs, customFds); - this.stdin.open(fds[0]); - this.stdin.writable = true; - this.stdin.readable = false; + if (customFds[0] === -1 || customFds[0] === undefined) { + this.stdin.open(fds[0]); + this.stdin.writable = true; + this.stdin.readable = false; + } + else { + this.stdin = null; + } - this.stdout.open(fds[1]); - this.stdout.writable = false; - this.stdout.readable = true; - this.stdout.resume(); + if (customFds[1] === -1 || customFds[1] === undefined) { + this.stdout.open(fds[1]); + this.stdout.writable = false; + this.stdout.readable = true; + this.stdout.resume(); + } + else { + this.stdout = null; + } - this.stderr.open(fds[2]); - this.stderr.writable = false; - this.stderr.readable = true; - this.stderr.resume(); + if (customFds[2] === -1 || customFds[2] === undefined) { + this.stderr.open(fds[2]); + this.stderr.writable = false; + this.stderr.readable = true; + this.stderr.resume(); + } + else { + this.stderr = null; + } }; diff --git a/src/node_child_process.cc b/src/node_child_process.cc index 65be16d1e3..72787f0ddf 100644 --- a/src/node_child_process.cc +++ b/src/node_child_process.cc @@ -64,7 +64,7 @@ Handle ChildProcess::New(const Arguments& args) { Handle ChildProcess::Spawn(const Arguments& args) { HandleScope scope; - if (args.Length() != 3 || + if (args.Length() < 3 || !args[0]->IsString() || !args[1]->IsArray() || !args[2]->IsArray()) { @@ -101,9 +101,21 @@ Handle ChildProcess::Spawn(const Arguments& args) { env[i] = strdup(*pair); } + int custom_fds[3] = { -1, -1, -1 }; + if (args[3]->IsArray()) { + // Set the custom file descriptor values (if any) for the child process + Local custom_fds_handle = Local::Cast(args[3]); + int custom_fds_len = custom_fds_handle->Length(); + for (int i = 0; i < custom_fds_len; i++) { + if (custom_fds_handle->Get(i)->IsUndefined()) continue; + Local fd = custom_fds_handle->Get(i)->ToInteger(); + custom_fds[i] = fd->Value(); + } + } + int fds[3]; - int r = child->Spawn(argv[0], argv, env, fds); + int r = child->Spawn(argv[0], argv, env, fds, custom_fds); for (i = 0; i < argv_length; i++) free(argv[i]); delete [] argv; @@ -181,7 +193,8 @@ void ChildProcess::Stop() { int ChildProcess::Spawn(const char *file, char *const args[], char **env, - int stdio_fds[3]) { + int stdio_fds[3], + int custom_fds[3]) { HandleScope scope; assert(pid_ == -1); assert(!ev_is_active(&child_watcher_)); @@ -189,9 +202,9 @@ int ChildProcess::Spawn(const char *file, int stdin_pipe[2], stdout_pipe[2], stderr_pipe[2]; /* An implementation of popen(), basically */ - if (pipe(stdin_pipe) < 0 || - pipe(stdout_pipe) < 0 || - pipe(stderr_pipe) < 0) { + if (custom_fds[0] == -1 && pipe(stdin_pipe) < 0 || + custom_fds[1] == -1 && pipe(stdout_pipe) < 0 || + custom_fds[2] == -1 && pipe(stderr_pipe) < 0) { perror("pipe()"); return -1; } @@ -206,14 +219,29 @@ int ChildProcess::Spawn(const char *file, return -4; case 0: // Child. - close(stdin_pipe[1]); // close write end - dup2(stdin_pipe[0], STDIN_FILENO); + if (custom_fds[0] == -1) { + close(stdin_pipe[1]); // close write end + dup2(stdin_pipe[0], STDIN_FILENO); + } + else { + dup2(custom_fds[0], STDIN_FILENO); + } - close(stdout_pipe[0]); // close read end - dup2(stdout_pipe[1], STDOUT_FILENO); + if (custom_fds[1] == -1) { + close(stdout_pipe[0]); // close read end + dup2(stdout_pipe[1], STDOUT_FILENO); + } + else { + dup2(custom_fds[1], STDOUT_FILENO); + } - close(stderr_pipe[0]); // close read end - dup2(stderr_pipe[1], STDERR_FILENO); + if (custom_fds[2] == -1) { + close(stderr_pipe[0]); // close read end + dup2(stderr_pipe[1], STDERR_FILENO); + } + else { + dup2(custom_fds[2], STDERR_FILENO); + } environ = env; @@ -232,17 +260,32 @@ int ChildProcess::Spawn(const char *file, Ref(); handle_->Set(pid_symbol, Integer::New(pid_)); - close(stdin_pipe[0]); - stdio_fds[0] = stdin_pipe[1]; - SetNonBlocking(stdin_pipe[1]); + if (custom_fds[0] == -1) { + close(stdin_pipe[0]); + stdio_fds[0] = stdin_pipe[1]; + SetNonBlocking(stdin_pipe[1]); + } + else { + stdio_fds[0] = custom_fds[0]; + } - close(stdout_pipe[1]); - stdio_fds[1] = stdout_pipe[0]; - SetNonBlocking(stdout_pipe[0]); + if (custom_fds[1] == -1) { + close(stdout_pipe[1]); + stdio_fds[1] = stdout_pipe[0]; + SetNonBlocking(stdout_pipe[0]); + } + else { + stdio_fds[1] = custom_fds[1]; + } - close(stderr_pipe[1]); - stdio_fds[2] = stderr_pipe[0]; - SetNonBlocking(stderr_pipe[0]); + if (custom_fds[2] == -1) { + close(stderr_pipe[1]); + stdio_fds[2] = stderr_pipe[0]; + SetNonBlocking(stderr_pipe[0]); + } + else { + stdio_fds[2] = custom_fds[2]; + } return 0; } diff --git a/src/node_child_process.h b/src/node_child_process.h index 14a13ecf42..7824686b5d 100644 --- a/src/node_child_process.h +++ b/src/node_child_process.h @@ -42,7 +42,7 @@ class ChildProcess : ObjectWrap { // are readable. // The user of this class has responsibility to close these pipes after // the child process exits. - int Spawn(const char *file, char *const argv[], char **env, int stdio_fds[3]); + int Spawn(const char *file, char *const argv[], char **env, int stdio_fds[3], int custom_fds[3]); // Simple syscall wrapper. Does not disable the watcher. onexit will be // called still. diff --git a/test/fixtures/stdio-filter.js b/test/fixtures/stdio-filter.js new file mode 100644 index 0000000000..ed5dfbfe42 --- /dev/null +++ b/test/fixtures/stdio-filter.js @@ -0,0 +1,11 @@ +sys = require('sys'); + +var regexIn = process.argv[2]; +var replacement = process.argv[3]; +var re = new RegExp(regexIn, 'g'); +var stdin = process.openStdin(); + +stdin.addListener("data", function (data) { + data = data.toString(); + process.stdout.write(data.replace(re, replacement)); +}); diff --git a/test/simple/test-child-process-custom-fds.js b/test/simple/test-child-process-custom-fds.js new file mode 100644 index 0000000000..29eaaa525f --- /dev/null +++ b/test/simple/test-child-process-custom-fds.js @@ -0,0 +1,93 @@ +require("../common"); + +var assert = require('assert'); +var spawn = require('child_process').spawn; +var path = require('path'); +var fs = require('fs'); +var sys = require('sys'); + +function fixtPath(p) { + return path.join(fixturesDir, p); +} + +var expected = "hello world"; + +// Test the equivalent of: +// $ /bin/echo "hello world" > hello.txt +var helloPath = fixtPath("hello.txt"); + +function test1(next) { + puts("Test 1..."); + fs.open(helloPath, 'w', 400, function (err, fd) { + if (err) throw err; + var child = spawn('/bin/echo', [expected], undefined, [-1, fd] ); + + assert.notEqual(child.stdin, null); + assert.equal(child.stdout, null); + assert.notEqual(child.stderr, null); + + child.addListener('exit', function (err) { + if (err) throw err; + fs.close(fd, function (error) { + if (error) throw error; + + fs.readFile(helloPath, function (err, data) { + if (err) throw err; + assert.equal(data.toString(), expected + "\n"); + puts(' File was written.'); + next(test3); + }); + }); + }); + }); +} + +// Test the equivalent of: +// $ node ../fixture/stdio-filter.js < hello.txt +function test2(next) { + puts("Test 2..."); + fs.open(helloPath, 'r', undefined, function (err, fd) { + var child = spawn(process.argv[0] + , [fixtPath('stdio-filter.js'), 'o', 'a'] + , undefined, [fd, -1, -1]); + + assert.equal(child.stdin, null); + var actualData = ''; + child.stdout.addListener('data', function (data) { + actualData += data.toString(); + }); + child.addListener('exit', function (code) { + if (err) throw err; + assert.equal(actualData, "hella warld\n"); + puts(" File was filtered successfully"); + fs.close(fd, function () { + next(test3); + }); + }); + }); +} + +// Test the equivalent of: +// $ /bin/echo "hello world" | ../stdio-filter.js a o +function test3(next) { + puts("Test 3..."); + var filter = spawn(process.argv[0] + , [fixtPath('stdio-filter.js'), 'o', 'a']); + var echo = spawn('/bin/echo', [expected], undefined, [-1, filter.fds[0]]); + var actualData = ''; + filter.stdout.addListener('data', function(data) { + puts(" Got data --> " + data); + actualData += data; + }); + filter.addListener('exit', function(code) { + if (code) throw "Return code was " + code; + assert.equal(actualData, "hella warld\n"); + puts(" Talked to another process successfully"); + }); + echo.addListener('exit', function(code) { + if (code) throw "Return code was " + code; + filter.stdin.end(); + }); +} + +test1(test2);