New api for child_process.spawn; ability to set cwd for spawn()ed process
authorBert Belder <bertbelder@gmail.com>
Thu, 5 Aug 2010 02:15:20 +0000 (04:15 +0200)
committerRyan Dahl <ry@tinyclouds.org>
Fri, 6 Aug 2010 20:37:30 +0000 (13:37 -0700)
Tests for child_process.spawn() use new API

Test for deprecated child_process.spawn() API

doc/api.markdown
lib/child_process.js
src/node_child_process.cc
src/node_child_process.h
test/simple/test-child-process-custom-fds.js
test/simple/test-child-process-cwd.js [new file with mode: 0644]
test/simple/test-child-process-deprecated-api.js [new file with mode: 0644]
test/simple/test-child-process-env.js

index 37a8e2db05148cde2d7e50d32dfa7cc8c4629aa6..403bc66d77f052216e21ca3eff8885b8227b7d17 100644 (file)
@@ -938,11 +938,22 @@ Example:
     grep.stdin.end();
 
 
-### child_process.spawn(command, args=[], env=process.env)
+### child_process.spawn(command, args=[], [options])
 
-Launches a new process with the given `command`, command line arguments, and
-environment variables.  If omitted, `args` defaults to an empty Array, and `env`
-defaults to `process.env`.
+Launches a new process with the given `command`, with  command line arguments in `args`.
+If omitted, `args` defaults to an empty Array.
+
+The third argument is used to specify additional options, which defaults to:
+
+    { cwd: undefined
+    , env: process.env,
+    , customFds: [-1, -1, -1]
+    }
+
+`cwd` allows you to specify the working directory from which the process is spawned.
+Use `env` to specify environment variables that will be visible to the new process.
+With `customFds` it is possible to hook up the new process' [stdin, stout, stderr] to
+existing streams; `-1` means that a new stream should be created.
 
 Example of running `ls -lh /usr`, capturing `stdout`, `stderr`, and the exit code:
 
index 43308a200fb5aaee73c0412be053099e32cf11c1..54bb6671c10ff370b2db8a229a6ecab4cea25dc2 100644 (file)
@@ -4,9 +4,9 @@ var Stream = require('net').Stream;
 var InternalChildProcess = process.binding('child_process').ChildProcess;
 
 
-var spawn = exports.spawn = function (path, args, env, customFds) {
+var spawn = exports.spawn = function (path, args /*, options OR env, customFds */) {
   var child = new ChildProcess();
-  child.spawn(path, args, env, customFds);
+  child.spawn.apply(child, arguments);
   return child;
 };
 
@@ -55,7 +55,7 @@ exports.execFile = function (file /* args, options, callback */) {
     }
   }
 
-  var child = spawn(file, args, options.env);
+  var child = spawn(file, args, {env: options.env});
   var stdout = "";
   var stderr = "";
   var killed = false;
@@ -161,9 +161,24 @@ ChildProcess.prototype.kill = function (sig) {
 };
 
 
-ChildProcess.prototype.spawn = function (path, args, env, customFds) {
+ChildProcess.prototype.spawn = function (path, args, options, customFds) {
   args = args || [];
-  env = env || process.env;
+  options = options || {};
+
+  var cwd, env;
+  if (options.cwd === undefined && options.env === undefined && options.customFds === undefined) {
+    // Deprecated API: (path, args, options, env, customFds)
+    cwd = "";
+    env = options || process.env;
+    customFds = customFds || [-1, -1, -1];
+  }
+  else {
+    // Recommended API: (path, args, options)
+    cwd = options.cwd || "";
+    env = options.env || process.env;
+    customFds = options.customFds || [-1, -1, -1];
+  }
+
   var envPairs = [];
   var keys = Object.keys(env);
   for (var index = 0, keysLength = keys.length; index < keysLength; index++) {
@@ -171,8 +186,7 @@ ChildProcess.prototype.spawn = function (path, args, env, customFds) {
     envPairs.push(key + "=" + env[key]);
   }
 
-  customFds = customFds || [-1, -1, -1];
-  var fds = this.fds = this._internal.spawn(path, args, envPairs, customFds);
+  var fds = this.fds = this._internal.spawn(path, args, cwd, envPairs, customFds);
 
   if (customFds[0] === -1 || customFds[0] === undefined) {
     this.stdin.open(fds[0]);
index 2b8803a55b6f861dcacf81522618ab3824940dc9..493d33be3c9318d0081924ef10e1c8fadbe8599b 100644 (file)
@@ -75,7 +75,8 @@ Handle<Value> ChildProcess::Spawn(const Arguments& args) {
   if (args.Length() < 3 ||
       !args[0]->IsString() ||
       !args[1]->IsArray() ||
-      !args[2]->IsArray()) {
+      !args[2]->IsString() ||
+      !args[3]->IsArray()) {
     return ThrowException(Exception::Error(String::New("Bad argument.")));
   }
 
@@ -99,8 +100,12 @@ Handle<Value> ChildProcess::Spawn(const Arguments& args) {
     argv[i+1] = strdup(*arg);
   }
 
-  // Copy third argument, args[2], into a c-string array called env.
-  Local<Array> env_handle = Local<Array>::Cast(args[2]);
+  // Copy third argument, args[2], into a c-string called cwd.
+  String::Utf8Value arg(args[2]->ToString());
+  char *cwd = strdup(*arg);
+
+  // Copy fourth argument, args[3], into a c-string array called env.
+  Local<Array> env_handle = Local<Array>::Cast(args[3]);
   int envc = env_handle->Length();
   char **env = new char*[envc+1]; // heap allocated to detect errors
   env[envc] = NULL;
@@ -110,9 +115,9 @@ Handle<Value> ChildProcess::Spawn(const Arguments& args) {
   }
 
   int custom_fds[3] = { -1, -1, -1 };
-  if (args[3]->IsArray()) {
+  if (args[4]->IsArray()) {
     // Set the custom file descriptor values (if any) for the child process
-    Local<Array> custom_fds_handle = Local<Array>::Cast(args[3]);
+    Local<Array> custom_fds_handle = Local<Array>::Cast(args[4]);
     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;
@@ -123,7 +128,7 @@ Handle<Value> ChildProcess::Spawn(const Arguments& args) {
 
   int fds[3];
 
-  int r = child->Spawn(argv[0], argv, env, fds, custom_fds);
+  int r = child->Spawn(argv[0], argv, cwd, env, fds, custom_fds);
 
   for (i = 0; i < argv_length; i++) free(argv[i]);
   delete [] argv;
@@ -200,6 +205,7 @@ void ChildProcess::Stop() {
 //
 int ChildProcess::Spawn(const char *file,
                         char *const args[],
+                        const char *cwd,
                         char **env,
                         int stdio_fds[3],
                         int custom_fds[3]) {
@@ -251,6 +257,11 @@ int ChildProcess::Spawn(const char *file,
         dup2(custom_fds[2], STDERR_FILENO);
       }
 
+      if (strlen(cwd) && chdir(cwd)) {
+        perror("chdir()");
+        _exit(127);
+      }
+
       environ = env;
 
       execvp(file, args);
index 7824686b5d62dddf88f17bd54bc0a777aafe5298..0eb38e7830785c09c01fff5e1b54933b25b7a3ad 100644 (file)
@@ -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 custom_fds[3]);
+  int Spawn(const char *file, char *const argv[], const char *cwd, char **env, int stdio_fds[3], int custom_fds[3]);
 
   // Simple syscall wrapper. Does not disable the watcher. onexit will be
   // called still.
index 8d3d2cd1185ff2a3bd798332f7c217162e4beb54..5e3b9b8621b2a9fb504c50eef16bfbb61fa1b5d2 100644 (file)
@@ -21,7 +21,7 @@ function test1(next) {
   console.log("Test 1...");
   fs.open(helloPath, 'w', 400, function (err, fd) {
     if (err) throw err;
-    var child = spawn('/bin/echo', [expected], undefined, [-1, fd] );
+    var child = spawn('/bin/echo', [expected], {customFds: [-1, fd]});
 
     assert.notEqual(child.stdin, null);
     assert.equal(child.stdout, null);
@@ -50,7 +50,7 @@ function test2(next) {
   fs.open(helloPath, 'r', undefined, function (err, fd) {
     var child = spawn(process.argv[0]
                      , [fixtPath('stdio-filter.js'), 'o', 'a']
-                     , undefined, [fd, -1, -1]);
+                     , {customFds: [fd, -1, -1]});
 
     assert.equal(child.stdin, null);
     var actualData = '';
@@ -74,7 +74,7 @@ function test3(next) {
   console.log("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 echo = spawn('/bin/echo', [expected], {customFds: [-1, filter.fds[0]]});
   var actualData = '';
   filter.stdout.addListener('data', function(data) {
     console.log("  Got data --> " + data);
diff --git a/test/simple/test-child-process-cwd.js b/test/simple/test-child-process-cwd.js
new file mode 100644 (file)
index 0000000..740c178
--- /dev/null
@@ -0,0 +1,51 @@
+common = require("../common");
+assert = common.assert
+spawn = require('child_process').spawn,
+path = require('path');
+
+var returns = 0;
+
+/*
+  Spawns 'pwd' with given options, then test 
+  - whether the exit code equals forCode,
+  - optionally whether the stdout result (after removing traling whitespace) matches forData
+*/
+function testCwd(options, forCode, forData) {
+  var data = "";
+  
+  var child = spawn('pwd', [], options);
+  child.stdout.setEncoding('utf8');
+
+  child.stdout.addListener('data', function(chunk) {
+    data += chunk;
+  });
+  
+  child.addListener('exit', function(code, signal) {
+    forData && assert.strictEqual(forData, data.replace(/[\s\r\n]+$/, ''))
+    assert.strictEqual(forCode, code);
+    returns--;
+  });
+  
+  returns++;
+}
+
+// Assume these exist, and 'pwd' gives us the right directory back
+testCwd( { cwd: '/bin'    }, 0, '/bin' );
+testCwd( { cwd: '/dev'    }, 0, '/dev' );
+testCwd( { cwd: '/'       }, 0, '/'    );
+
+// Assume this doesn't exist, we expect exitcode=127
+testCwd( { cwd: 'does-not-exist' }, 127 );
+
+// Spawn() shouldn't try to chdir() so this should just work
+testCwd( undefined,          0 );
+testCwd( {                }, 0 );
+testCwd( { cwd: ''        }, 0 );
+testCwd( { cwd: undefined }, 0 );
+testCwd( { cwd: null      }, 0 );
+
+// Check whether all tests actually returned
+assert.notEqual(0, returns);
+process.addListener('exit', function () {
+  assert.equal(0, returns);
+});
\ No newline at end of file
diff --git a/test/simple/test-child-process-deprecated-api.js b/test/simple/test-child-process-deprecated-api.js
new file mode 100644 (file)
index 0000000..9cbffef
--- /dev/null
@@ -0,0 +1,59 @@
+var common = require("../common");
+var assert = common.assert;
+var spawn  = require('child_process').spawn;
+var path   = require('path');
+var fs     = require('fs');
+var exits  = 0;
+
+// Test `env` parameter for child_process.spawn(path, args, env, customFds) deprecated api
+(function() {
+  var response = "";
+  var child = spawn('/usr/bin/env', [], {'HELLO' : 'WORLD'});
+
+  child.stdout.setEncoding('utf8');
+
+  child.stdout.addListener("data", function (chunk) {
+    response += chunk;
+  });
+
+  process.addListener('exit', function () {
+   assert.ok(response.indexOf('HELLO=WORLD') >= 0);
+   exits++;
+  });
+})();
+
+// Test `customFds` parameter for child_process.spawn(path, args, env, customFds) deprecated api
+(function() {
+  var expected = "hello world";
+  var helloPath = path.join(common.fixturesDir, "hello.txt");
+
+  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");
+          exits++;
+        });
+      });
+    });
+  });
+})();
+
+// Check if all child processes exited
+process.addListener('exit', function () {
+  assert.equal(2, exits);
+});
\ No newline at end of file
index ced574a6aaf4aac453affb42e8d37cb67a7a91ac..c5fb0ef10228c99ae67da7d8c8afbef5e491d2fa 100644 (file)
@@ -2,7 +2,7 @@ common = require("../common");
 assert = common.assert
 
 var spawn = require('child_process').spawn;
-child = spawn('/usr/bin/env', [], {'HELLO' : 'WORLD'});
+child = spawn('/usr/bin/env', [], {env: {'HELLO' : 'WORLD'}});
 
 response = "";