Child processes
authorBert Belder <bertbelder@gmail.com>
Fri, 3 Dec 2010 00:44:09 +0000 (01:44 +0100)
committerBert Belder <bertbelder@gmail.com>
Mon, 20 Dec 2010 22:51:31 +0000 (23:51 +0100)
TODO.win32
src/node_child_process.cc
src/node_child_process.h
src/node_child_process_win32.cc [new file with mode: 0644]
src/node_extensions.h

index b883a8c..24e08cf 100644 (file)
@@ -8,12 +8,23 @@
   E.g. getaddrinfo() is ansi-only; GetAddrInfoW is utf16-only. Can we get utf16 straight out of v8?\r
   Are unix sockets similar to windows named pipes? If so, should they be supported? -> currently: no. Complication: they block.\r
 \r
-- Child processes\r
-  Should not be too hard using CreatePipe, CreateProcessW and GetExitCodeProcess.\r
-  Hooking up a child process to a file handle can be done; hooking up to a normal socket won't work;\r
-  we'd need some sort of pump() mechanism.\r
-  Waiting for the child to exit is tricky, probably would require a wait thread to wait for the child, then ev_async notify.\r
-  How can we distinguish between the exit code and exception number after calling GetExitCodeProcess?\r
+- Child process issues\r
+  * Communication between parent and child is slow; it uses a socketpair\r
+    where a pipe would be much faster. Replace it by a pipe when there\r
+    is a libev backend that supports waiting for a pipe.\r
+  * When a child process spawns the pid is not available straightaway.\r
+    On linux the pid is available immediately because fork() doesn't\r
+    block; on windows a libeio thread is used to call CreateProcess.\r
+    So this can't really be fixed, but it could be worked around by adding a\r
+    'spawn' or 'pid' method.\r
+  * kill() doesn't work when the pid is not available yet. All the plumbing\r
+    is there to make it work, but lib/child_process.js just doesn't call\r
+    ChildProcess::Kill() as long as the pid is not known.\r
+  * passing socket custom_fds is not supported\r
+  * child_process.exec() only works on systems with msys installed.\r
+    It's because it relies on the 'sh' shell. The default windows shell\r
+    is 'cmd' and it works a little differently. Maybe add an option to\r
+    specify the shell to exec()?\r
 \r
 - Stdio (make TTY's / repl / readline work)\r
   This will be hard: there is no ANSI escape code support in windows.\r
index 0008173..87c3857 100644 (file)
@@ -1,3 +1,7 @@
+#ifdef __MINGW32__
+# include <node_child_process_win32.cc>
+#endif
+
 #ifdef __POSIX__
 
 // Copyright 2009 Ryan Dahl <ry@tinyclouds.org>
index 0eb38e7..6d78961 100644 (file)
@@ -7,6 +7,10 @@
 #include <v8.h>
 #include <ev.h>
 
+#ifdef __MINGW32__
+# include <windows.h> // HANDLE type
+#endif
+
 // ChildProcess is a thin wrapper around ev_child. It has the extra
 // functionality that it can spawn a child process with pipes connected to
 // its stdin, stdout, stderr. This class is not meant to be exposed to but
@@ -28,13 +32,25 @@ class ChildProcess : ObjectWrap {
   static v8::Handle<v8::Value> Kill(const v8::Arguments& args);
 
   ChildProcess() : ObjectWrap() {
+#ifdef __POSIX__
     ev_init(&child_watcher_, ChildProcess::on_chld);
     child_watcher_.data = this;
+#endif // __POSIX__
+
     pid_ = -1;
+
+#ifdef __MINGW32__
+    InitializeCriticalSection(&info_lock_);
+    kill_me_ = false;
+    did_start_ = false;
+    exit_signal_ = 0;
+#endif // __MINGW32__
   }
 
   ~ChildProcess() {
+#ifdef __POSIX__
     Stop();
+#endif // __POSIX__
   }
 
   // Returns 0 on success. stdio_fds will contain file desciptors for stdin,
@@ -48,8 +64,10 @@ class ChildProcess : ObjectWrap {
   // called still.
   int Kill(int sig);
 
- private:
+private:
   void OnExit(int code);
+
+#ifdef __POSIX__ // Shouldn't this just move to node_child_process.cc?
   void Stop(void);
 
   static void on_chld(EV_P_ ev_child *watcher, int revents) {
@@ -62,6 +80,36 @@ class ChildProcess : ObjectWrap {
 
   ev_child child_watcher_;
   pid_t pid_;
+#endif // __POSIX__
+
+#ifdef __MINGW32__
+  static int do_spawn(eio_req *req);
+  static int after_spawn(eio_req *req);
+  static void watch(ChildProcess *child);
+  static void CALLBACK watch_wait_callback(void *data, BOOLEAN didTimeout);
+  static void notify_spawn_failure(ChildProcess *child);
+  static void notify_exit(ev_async *ev, int revent);
+  static int do_kill(ChildProcess *child, int sig);static void close_stdio_handles(ChildProcess *child);
+
+  int pid_;
+  int exit_signal_;
+
+  WCHAR *application_;
+  WCHAR *arguments_;
+  WCHAR *env_win_;
+  WCHAR *cwd_;
+  const WCHAR *path_;
+  const WCHAR *path_ext_;
+
+  HANDLE stdio_handles_[3];
+  bool got_custom_fds_[3];
+
+  CRITICAL_SECTION info_lock_;
+  bool did_start_;
+  bool kill_me_;
+  HANDLE wait_handle_;
+  HANDLE process_handle_;
+#endif // __MINGW32__
 };
 
 }  // namespace node
diff --git a/src/node_child_process_win32.cc b/src/node_child_process_win32.cc
new file mode 100644 (file)
index 0000000..749cd70
--- /dev/null
@@ -0,0 +1,877 @@
+
+// RegisterWaitForSingleObject requires Windows 2000,
+// GetProcessId requires windows XP SP1
+#define _WIN32_WINNT 0x0501
+
+
+#include <node.h>
+#include <node_child_process.h>
+#include <platform_win32.h>
+#include <platform_win32_winsock.h>
+
+#include <windows.h>
+#include <winsock.h>
+
+#include <v8.h>
+#include <ev.h>
+#include <eio.h>
+
+#include <assert.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <unistd.h>
+#include <sys/types.h>
+
+
+namespace node {
+
+using namespace v8;
+
+
+static const WCHAR DEFAULT_PATH[1] = L"";
+static const WCHAR DEFAULT_PATH_EXT[20] = L".COM;.EXE;.BAT;.CMD";
+
+
+static Persistent<String> pid_symbol;
+static Persistent<String> onexit_symbol;
+
+
+static struct watcher_status_struct {
+  ev_async async_watcher;
+  ChildProcess *child;
+  HANDLE lock;
+  int num_active;
+} watcher_status;
+
+
+/*
+ * Path search functions
+ */
+
+/*
+ * Helper function for search_path
+ */
+static inline WCHAR* search_path_join_test(
+    const WCHAR* dir, int dir_len, const WCHAR* name, int name_len,
+    const WCHAR* ext, int ext_len, const WCHAR* cwd, int cwd_len) {
+  WCHAR *result, *result_pos;
+
+  if (dir_len >= 1 && (dir[0] == L'/' || dir[0] == L'\\')) {
+    // It's a full path with drive letter, don't use cwd
+    cwd_len = 0;
+  } else if (dir_len == 2 && dir[1] == L':') {
+    // It's a relative path with drive letter (ext.g. D:../some/file)
+    // Replace dir by full cwd if it points to the same drive,
+    // otherwise use the dir only.
+    if (cwd_len < 2 || _wcsnicmp(cwd, dir, 2) != 0) {
+      cwd_len = 0;
+    } else {
+      dir_len = 0;
+    }
+  } else if (dir_len > 2 && dir[1] == L':') {
+    // It's an absolute path with drive letter
+    // Don't use the cwd at all
+    cwd_len = 0;
+  }
+
+  // Allocate buffer for output
+  result = result_pos =
+      new WCHAR[cwd_len + 1 + dir_len + 1 + name_len + 1 + ext_len + 1];
+
+  // Copy cwd
+  wcsncpy(result_pos, cwd, cwd_len);
+  result_pos += cwd_len;
+
+  // Add a path separator if cwd didn't end with one
+  if (cwd_len && wcsrchr(L"\\/:", result_pos[-1]) == NULL) {
+    result_pos[0] = L'\\';
+    result_pos++;
+  }
+
+  // Copy dir
+  wcsncpy(result_pos, dir, dir_len);
+  result_pos += dir_len;
+
+  // Add a separator if the dir didn't end with one
+  if (dir_len && wcsrchr(L"\\/:", result_pos[-1]) == NULL) {
+    result_pos[0] = L'\\';
+    result_pos++;
+  }
+
+  // Copy filename
+  wcsncpy(result_pos, name, name_len);
+  result_pos += name_len;
+
+  // Copy extension
+  if (ext_len) {
+    result_pos[0] = L'.';
+    result_pos++;
+    wcsncpy(result_pos, ext, ext_len);
+    result_pos += ext_len;
+  }
+
+  // Null terminator
+  result_pos[0] = L'\0';
+
+  DWORD attrs = GetFileAttributesW(result);
+
+  if (attrs != INVALID_FILE_ATTRIBUTES &&
+        !(attrs & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT))) {
+    return result;
+  }
+
+  delete[] result;
+  return NULL;
+}
+
+
+/*
+ * Helper function for search_path
+ */
+static inline WCHAR* path_search_walk_ext(
+    const WCHAR *dir, int dir_len, const WCHAR *name, int name_len,
+    WCHAR *cwd, int cwd_len, const WCHAR *path_ext, bool name_has_ext) {
+  WCHAR* result = NULL;
+
+  const WCHAR *ext_start,
+              *ext_end = path_ext;
+
+  // If the name itself has a nonemtpy extension, try this extension first
+  if (name_has_ext) {
+    result = search_path_join_test(dir, dir_len,
+                                   name, name_len,
+                                   L"", 0,
+                                   cwd, cwd_len);
+  }
+
+  // Add path_ext extensions and try to find a name that matches
+  while (result == NULL) {
+    if (*ext_end == L'\0') {
+      break;
+    }
+
+    // Skip the separator that ext_end now points to
+    if (ext_end != path_ext) {
+      ext_end++;
+    }
+
+    // Find the next dot in path_ext
+    ext_start = wcschr(ext_end, L'.');
+    if (ext_start == NULL) {
+      break;
+    }
+
+    // Skip the dot
+    ext_start++;
+
+    // Slice until we found a ; or alternatively a \0
+    ext_end = wcschr(ext_start, L';');
+    if (ext_end == NULL) {
+       ext_end = wcschr(ext_start, '\0');
+    }
+
+    result = search_path_join_test(dir, dir_len,
+                                   name, name_len,
+                                   ext_start, (ext_end - ext_start),
+                                   cwd, cwd_len);
+  }
+
+  return result;
+}
+
+
+/*
+ * search_path searches the system path for an executable filename -
+ * the windows API doesn't provide this as a standalone function nor as an
+ * option to CreateProcess.
+ *
+ * It tries to return an absolute filename.
+ *
+ * Furthermore, it tries to follow the semantics that cmd.exe uses as closely
+ * as possible:
+ *
+ * - Do not search the path if the filename already contains a path (either
+ *   relative or absolute).
+ *     (but do use path_ext)
+ *
+ * - If there's really only a filename, check the current directory for file,
+ *   then search all path directories.
+ *
+ * - If filename specifies has *any* extension, search for the file with the
+ *   specified extension first.
+ *     (not necessary an executable one or one that appears in path_ext;
+ *      *but* no extension or just a dot is *not* allowed)
+ *
+ * - If the literal filename is not found in a directory, try *appending*
+ *   (not replacing) extensions from path_ext in the specified order.
+ *     (an extension consisting of just a dot *may* appear in path_ext;
+ *      unlike what happens if the specified filename ends with a dot,
+ *      if path_ext specifies a single dot cmd.exe *does* look for an
+ *      extension-less file)
+ *
+ * - The path variable may contain relative paths; relative paths are relative
+ *   to the cwd.
+ *
+ * - Directories in path may or may not end with a trailing backslash.
+ *
+ * - Extensions path_ext portions must always start with a dot.
+ *
+ * - CMD does not trim leading/trailing whitespace from path/pathex entries
+ *   nor from the environment variables as a whole.
+ *
+ * - When cmd.exe cannot read a directory, it wil just skip it and go on
+ *   searching. However, unlike posix-y systems, it will happily try to run a
+ *   file that is not readable/executable; if the spawn fails it will not
+ *   continue searching.
+ *
+ * TODO: correctly interpret UNC paths
+ * TODO: check with cmd what should happen when a pathext entry does not start
+ *       with a dot
+ */
+static inline WCHAR* search_path(const WCHAR *file, WCHAR *cwd,
+    const WCHAR *path, const WCHAR *path_ext) {
+  WCHAR* result = NULL;
+
+  int file_len = wcslen(file);
+  int cwd_len = wcslen(cwd);
+
+  // If the caller supplies an empty filename,
+  // we're not gonna return c:\windows\.exe -- GFY!
+  if (file_len == 0
+      || (file_len == 1 && file[0] == L'.')) {
+    return NULL;
+  }
+
+  // Find the start of the filename so we can split the directory from the name
+  WCHAR *file_name_start;
+  for (file_name_start = (WCHAR*)file + file_len;
+       file_name_start > file
+           && file_name_start[-1] != L'\\'
+           && file_name_start[-1] != L'/'
+           && file_name_start[-1] != L':';
+       file_name_start--);
+
+  bool file_has_dir = file_name_start != file;
+
+  // Check if the filename includes an extension
+  WCHAR *dot = wcschr(file_name_start, L'.');
+  bool name_has_ext = (dot != NULL && dot[1] != L'\0');
+
+  if (file_has_dir) {
+    // The file has a path inside, don't use path (but do use path_ex)
+    result = path_search_walk_ext(
+        file, file_name_start - file,
+        file_name_start, file_len - (file_name_start - file),
+        cwd, cwd_len,
+        path_ext, name_has_ext);
+
+  } else {
+    const WCHAR *dir_start,
+                *dir_end = path;
+
+    // The file is really only a name; look in cwd first, then scan path
+    result = path_search_walk_ext(L"", 0,
+                                  file, file_len,
+                                  cwd, cwd_len,
+                                  path_ext, name_has_ext);
+
+    while (result == NULL) {
+      if (*dir_end == L'\0') {
+        break;
+      }
+
+      // Skip the separator that dir_end now points to
+      if (dir_end != path) {
+        dir_end++;
+      }
+
+      // Next slice starts just after where the previous one ended
+      dir_start = dir_end;
+
+      // Slice until the next ; or \0 is found
+      dir_end = wcschr(dir_start, L';');
+      if (dir_end == NULL) {
+        dir_end = wcschr(dir_start, L'\0');
+      }
+
+      // If the slice is zero-length, don't bother
+      if (dir_end - dir_start == 0) {
+        continue;
+      }
+
+      result = path_search_walk_ext(dir_start, dir_end - dir_start,
+                                    file, file_len,
+                                    cwd, cwd_len,
+                                    path_ext, name_has_ext);
+    }
+  }
+
+  return result;
+}
+
+
+/*
+ * Process exit "watcher" functions. It's not like a real libev watcher,
+ * it's more like a wrapper around ev_async, and RegisterWaitForSingleObject.
+ * And its not generalized, it only works with child processes.
+ * BTW there is only one exit watcher that watches all childs!
+ */
+
+
+// Called from either a eio, a wait thread or a callback thread created by a
+// wait thread
+void ChildProcess::close_stdio_handles(ChildProcess *child) {
+  // Before we proceed to synchronize with the main thread, first close
+  // the stdio sockets that the child process has used, because it may
+  // take some time and would deadlock if done in the main thread.
+  for (int i = 0; i < 3; i++) {
+    if (!child->got_custom_fds_[i]) {
+      wsa_disconnect_ex((SOCKET)child->stdio_handles_[i], NULL, 0, 0);
+      closesocket((SOCKET)child->stdio_handles_[i]);
+    }
+  }
+}
+
+
+// Called from the main thread
+void ChildProcess::notify_exit(ev_async *ev, int revent) {
+  // Get the child process, then release the lock
+  ChildProcess *child = watcher_status.child;
+
+  // Stop the watcher if appropriate
+  if (!--watcher_status.num_active) {
+    ev_async_stop(EV_DEFAULT_UC_ &watcher_status.async_watcher);
+  }
+
+  ReleaseSemaphore(watcher_status.lock, 1, NULL);
+
+  DWORD exit_code = -127;
+
+  EnterCriticalSection(&child->info_lock_);
+
+  // Did the process even start anyway?
+  if (child->did_start_) {
+    // Process launched, then exited
+
+    // Drop the wait handle
+    UnregisterWait(child->wait_handle_);
+
+    // Fetch the process exit code
+    if (GetExitCodeProcess(child->process_handle_, &exit_code) == 0) {
+      winapi_perror("GetExitCodeProcess");
+    }
+
+    // Close and unset the process handle
+    EnterCriticalSection(&child->info_lock_);
+    CloseHandle(child->process_handle_);
+    child->process_handle_ = NULL;
+    child->pid_ = 0;
+  }
+
+  LeaveCriticalSection(&child->info_lock_);
+
+  // The process never even started
+  child->OnExit(exit_code);
+}
+
+
+// Called from the eio thread
+void ChildProcess::notify_spawn_failure(ChildProcess *child) {
+  close_stdio_handles(child);
+
+  DWORD result = WaitForSingleObject(watcher_status.lock, INFINITE);
+  assert(result == WAIT_OBJECT_0);
+
+  if (!watcher_status.num_active++) {
+    ev_async_start(EV_DEFAULT_UC_ &watcher_status.async_watcher);
+  }
+
+  watcher_status.child = child;
+
+  ev_async_send(&watcher_status.async_watcher);
+}
+
+
+// Called from the windows-managed wait thread
+void CALLBACK ChildProcess::watch_wait_callback(void *data,
+    BOOLEAN didTimeout) {
+  assert(didTimeout == FALSE);
+
+  ChildProcess *child = (ChildProcess*)data;
+
+  close_stdio_handles(child);
+
+  // If the main thread is blocked, and more than one child process returns,
+  // the wait thread will block as well here. It doesn't matter because the
+  // main thread can only do one thing at a time anyway.
+  DWORD result = WaitForSingleObject(watcher_status.lock, INFINITE);
+  assert(result == WAIT_OBJECT_0);
+
+  watcher_status.child = child;
+  ev_async_send(&watcher_status.async_watcher);
+}
+
+
+// Called from the eio thread
+inline void ChildProcess::watch(ChildProcess *child) {
+  DWORD result = WaitForSingleObject(watcher_status.lock, INFINITE);
+  assert(result == WAIT_OBJECT_0);
+
+  if (!watcher_status.num_active++) {
+    ev_async_start(EV_DEFAULT_UC_ &watcher_status.async_watcher);
+  }
+
+  // We must retain the lock here because we don't want the RegisterWait
+  // to complete before the waithandle is set to the child process.
+  RegisterWaitForSingleObject(&child->wait_handle_, child->process_handle_,
+      watch_wait_callback, (void*)child, INFINITE,
+      WT_EXECUTEINWAITTHREAD | WT_EXECUTEONLYONCE);
+
+  ReleaseSemaphore(watcher_status.lock, 1, NULL);
+}
+
+
+/*
+ * Spawn helper functions
+ */
+
+
+/*
+ * Quotes command line arguments
+ * Returns a pointer to the end (next char to be written) of the buffer
+ */
+static inline WCHAR* quote_cmd_arg(WCHAR *source, WCHAR *target,
+    WCHAR terminator) {
+  int len = wcslen(source),
+      i;
+
+  // Check if the string must be quoted;
+  // if unnecessary, don't do it, it may only confuse older programs.
+  if (len == 0) {
+    goto quote;
+  }
+  for (i = 0; i < len; i++) {
+    if (source[i] == L' ' || source[i] == L'"') {
+      goto quote;
+    }
+  }
+
+  // No quotation needed
+  wcsncpy(target, source, len);
+  target += len;
+  *(target++) = terminator;
+  return target;
+
+quote:
+  // Quote
+  *(target++) = L'"';
+  for (i = 0; i < len; i++) {
+    if (source[i] == L'"' || source[i] == L'\\') {
+      *(target++) = '\\';
+    }
+    *(target++) = source[i];
+  }
+  *(target++) = L'"';
+  *(target++) = terminator;
+
+  return target;
+}
+
+
+/*
+ * Spawns a child process from a libeio thread
+ */
+int ChildProcess::do_spawn(eio_req *req) {
+  ChildProcess* child = (ChildProcess*)req->data;
+
+  WCHAR* application_path = search_path(child->application_, child->cwd_,
+      child->path_, child->path_ext_);
+
+  STARTUPINFOW startup;
+  PROCESS_INFORMATION info;
+
+  startup.cb = sizeof(startup);
+  startup.lpReserved = NULL;
+  startup.lpDesktop = NULL;
+  startup.lpTitle = NULL;
+  startup.dwFlags = STARTF_USESTDHANDLES;
+  startup.cbReserved2 = 0;
+  startup.lpReserved2 = NULL;
+  startup.hStdInput = child->stdio_handles_[0];
+  startup.hStdOutput = child->stdio_handles_[1];
+  startup.hStdError = child->stdio_handles_[2];
+
+  EnterCriticalSection(&child->info_lock_);
+
+  if (!child->kill_me_) {
+    // Try start the process
+    BOOL success = CreateProcessW(
+      application_path,
+      child->arguments_,
+      NULL,
+      NULL,
+      1,
+      CREATE_UNICODE_ENVIRONMENT,
+      child->env_win_,
+      child->cwd_,
+      &startup,
+      &info
+    );
+
+    if (success) {
+      child->process_handle_ = info.hProcess;
+      child->pid_ = GetProcessId(info.hProcess);
+      child->did_start_ = true;
+      watch(child);
+      LeaveCriticalSection(&child->info_lock_);
+
+      // Not interesting
+      CloseHandle(info.hThread);
+
+      return 0;
+    }
+  }
+
+  // kill_me set or process failed to start
+  notify_spawn_failure(child);
+  LeaveCriticalSection(&child->info_lock_);
+
+  return 0;
+}
+
+
+// Called from the main thread after spawn has finished,
+// there's no need to lock the child because did_start is reliable
+int ChildProcess::after_spawn(eio_req *req) {
+  ChildProcess* child = (ChildProcess*)req->data;
+
+  if (child->did_start_) {
+    child->handle_->Set(pid_symbol, Integer::New(child->pid_));
+  } else {
+    child->handle_->Set(pid_symbol, Local<Value>::New(Null()));
+  }
+
+  // Cleanup data structures needed only for spawn() here
+  delete [] child->application_;
+  delete [] child->arguments_;
+  delete [] child->env_win_;
+  delete [] child->cwd_;
+
+  return 0;
+}
+
+
+/*
+ * Kill helper functions
+ */
+
+// Called from the main thread while eio/wait threads may still be busy with
+// the process
+int ChildProcess::do_kill(ChildProcess *child, int sig) {
+  EnterCriticalSection(&child->info_lock_);
+
+  child->exit_signal_ = sig;
+
+  if (child->did_start_) {
+    // On windows killed processes normally return 1
+    if (TerminateProcess(child->process_handle_, 1) != 0) {
+      return 0;
+    } else {
+      return GetLastError();
+    }
+  } else {
+    child->kill_me_ = true;
+    return 0;
+  }
+
+  LeaveCriticalSection(&child->info_lock_);
+}
+
+
+/*
+ * ChildProcess non-static Methods
+ */
+
+Handle<Value> ChildProcess::New(const Arguments& args) {
+  HandleScope scope;
+  ChildProcess *p = new ChildProcess();
+  p->Wrap(args.Holder());
+  return args.This();
+}
+
+
+// This is an internal function. The third argument should be an array
+// of key value pairs seperated with '='.
+Handle<Value> ChildProcess::Spawn(const Arguments& args) {
+  HandleScope scope;
+
+  if (args.Length() < 3 ||
+      !args[0]->IsString() ||
+      !args[1]->IsArray() ||
+      !args[2]->IsString() ||
+      !args[3]->IsArray()) {
+    return ThrowException(Exception::Error(String::New("Bad argument.")));
+  }
+
+  // Get ChildProcess object
+  ChildProcess *child = ObjectWrap::Unwrap<ChildProcess>(args.Holder());
+
+  // Copy appplication name
+  String::Value application(args[0]->ToString());
+  child->application_ = _wcsdup((WCHAR*)*application);
+
+  /*
+   * Copy second argument args[1] into a c-string called argv.
+   * On windows command line arguments are all quoted and concatenated to
+   * one string.
+   * Assuming that all arguments must be wrapped in quotes,
+   * every character needs to be quoted with a backslash,
+   * and every argument is followed by either a space or a nul char,
+   * the maximum required buffer size is Σ[arg1..argc](2 * length + 3).
+   */
+  Local<Array> cmd_args_handle = Local<Array>::Cast(args[1]);
+  int cmd_argc = cmd_args_handle->Length();
+
+  if (cmd_argc > 0) {
+       // Compute required buffer
+    int max_buf = cmd_argc * 3,
+        i;
+       for (i = 0; i < cmd_argc; i++) {
+         Local<String> arg_handle =
+             cmd_args_handle->Get(Integer::New(i))->ToString();
+         max_buf += 2 * arg_handle->Length();
+       }
+
+       child->arguments_ = new WCHAR[max_buf];
+       WCHAR *pos = child->arguments_;
+       for (i = 0; i < cmd_argc - 1; i++) {
+         String::Value arg(cmd_args_handle->Get(Integer::New(i))->ToString());
+         pos = quote_cmd_arg((WCHAR*)*arg, pos, L' ');
+       }
+       String::Value arg(cmd_args_handle->Get(Integer::New(i))->ToString());
+       quote_cmd_arg((WCHAR*)*arg, pos, L'\0');
+
+  } else {
+    // No arguments
+    child->arguments_ = _wcsdup(L"\0");
+  }
+
+  // Copy command-line arguments
+  Local<String>cwd_handle = Local<String>::Cast(args[2]);
+  if (cwd_handle->Length() > 0) {
+    // Cwd was specified
+    String::Value cwd(args[2]);
+    child->cwd_ = _wcsdup((WCHAR*)*cwd);
+  } else {
+    // Cwd not specified
+    int chars = GetCurrentDirectoryW(0, NULL);
+    if (!chars) {
+      winapi_perror("GetCurrentDirectoryW");
+      child->cwd_ = _wcsdup(L"");
+    } else {
+      child->cwd_ = new WCHAR[chars];
+      GetCurrentDirectoryW(chars, child->cwd_);
+    }
+  }
+
+  /*
+   * args[3] holds the environment as a js array containing key=value pairs.
+   * The way windows takes environment variables is different than what C does;
+   * Windows wants a contiguous block of null-terminated strings, terminated
+   * with an additional null.
+   * Get a pointer to the pathext and path environment variables as well,
+   * because do_spawn needs it. These are just pointers into env_win.
+   */
+  Local<Array> env_list_handle = Local<Array>::Cast(args[3]);
+  int envc = env_list_handle->Length();
+  Local<String> env_val_handle[envc];
+
+  int env_win_len = envc + 1; // room for \0 terminators plus closing null
+  for (int i = 0; i < envc; i++) {
+    env_val_handle[i] = env_list_handle->Get(Integer::New(i))->ToString();
+    env_win_len += env_val_handle[i]->Length();
+  }
+
+  WCHAR *env_win = new WCHAR[env_win_len],
+        *env_win_pos = env_win;
+  WCHAR *path = NULL, *path_ext = NULL;
+
+  for (int i = 0; i < envc; i++) {
+    int len = env_val_handle[i]->Length() + 1; // including \0
+    String::Value pair(env_val_handle[i]);
+    wcsncpy(env_win_pos, (WCHAR*)*pair, (size_t)len);
+
+    // Try to get a pointer to PATH and PATHEXT
+    if (_wcsnicmp(L"PATH=", env_win_pos, 5) == 0) {
+      path = env_win_pos + 5;
+    }
+    if (_wcsnicmp(L"PATHEXT=", env_win_pos, 8) == 0) {
+      path_ext = env_win_pos + 8;
+    }
+
+    env_win_pos += len;
+  }
+
+  *env_win_pos = L'\0';
+
+  child->env_win_ = env_win;
+
+  if (path != NULL) {
+    child->path_ = path;
+  } else {
+    child->path_ = DEFAULT_PATH;
+  }
+
+  if (path_ext != NULL) {
+    child->path_ext_ = path_ext;
+  } else {
+    child->path_ext_ = DEFAULT_PATH_EXT;
+  }
+
+  // Open pipes or re-use custom_fds to talk to child
+  Local<Array> custom_fds_handle = Local<Array>::Cast(args[4]);
+  int custom_fds_len = custom_fds_handle->Length();
+
+  HANDLE *child_handles = (HANDLE*)&child->stdio_handles_;
+  bool *has_custom_fds = (bool*)&child->got_custom_fds_;
+  int parent_fds[3];
+
+  for (int i = 0; i < 3; i++) {
+    int custom_fd = -1;
+    if (i < custom_fds_len && !custom_fds_handle->Get(i)->IsUndefined())
+      custom_fd = custom_fds_handle->Get(i)->ToInteger()->Value();
+
+    if (custom_fd == -1) {
+      // Create a new pipe
+      HANDLE parent_handle, child_handle;
+      if (wsa_sync_async_socketpair(AF_INET, SOCK_STREAM, IPPROTO_IP,
+          (SOCKET*)&child_handle, (SOCKET*)&parent_handle) == SOCKET_ERROR)
+        wsa_perror("wsa_sync_async_socketpair");
+
+      // Make parent handle nonblocking
+      unsigned long ioctl_value = 1;
+      if (ioctlsocket((SOCKET)parent_handle, FIONBIO, &ioctl_value) ==
+          SOCKET_ERROR)
+        wsa_perror("ioctlsocket");
+
+      // Make child handle inheritable
+      if (!SetHandleInformation(child_handle, HANDLE_FLAG_INHERIT,
+          HANDLE_FLAG_INHERIT))
+        winapi_perror("SetHandleInformation");
+
+      // Enable linger on socket so all written data gets through
+      BOOL opt_value = 0;
+      if (setsockopt((SOCKET)child_handle, SOL_SOCKET, SO_DONTLINGER,
+          (char*)&opt_value, sizeof(opt_value)) == SOCKET_ERROR)
+        wsa_perror("setsockopt");
+
+      has_custom_fds[i] = false;
+      child_handles[i] = child_handle;
+      parent_fds[i] = (int)_open_osfhandle((intptr_t)parent_handle, 0);
+
+    } else {
+      // Use this custom fd
+      HANDLE custom_handle = (HANDLE)_get_osfhandle(custom_fd);
+
+      // Make handle inheritable
+      if (!SetHandleInformation(child_handles[i], HANDLE_FLAG_INHERIT,
+          HANDLE_FLAG_INHERIT))
+        winapi_perror("SetHandleInformation");
+
+      has_custom_fds[i] = true;
+      child_handles[i] = custom_handle;
+      parent_fds[i] = custom_fd;
+    }
+  }
+
+  // Return the opened fds
+  Local<Array> result = Array::New(3);
+  assert(parent_fds[0] >= 0);
+  result->Set(0, Integer::New(parent_fds[0]));
+  assert(parent_fds[1] >= 0);
+  result->Set(1, Integer::New(parent_fds[1]));
+  assert(parent_fds[2] >= 0);
+  result->Set(2, Integer::New(parent_fds[2]));
+
+  eio_custom(do_spawn, EIO_PRI_DEFAULT, after_spawn, (void*)child);
+
+  return scope.Close(result);
+}
+
+
+Handle<Value> ChildProcess::Kill(const Arguments& args) {
+  HandleScope scope;
+  ChildProcess *child = ObjectWrap::Unwrap<ChildProcess>(args.Holder());
+  assert(child);
+
+  int sig = SIGTERM;
+
+  if (args.Length() > 0) {
+    if (args[0]->IsNumber()) {
+      sig = args[0]->Int32Value();
+    } else {
+      return ThrowException(Exception::Error(String::New("Bad argument.")));
+    }
+  }
+
+  if (do_kill(child, sig) != 0) {
+    return ThrowException(Exception::Error(String::New(strerror(errno))));
+  }
+
+  return Undefined();
+}
+
+
+// Called from the main thread _after_ all eio/wait threads are done with the
+// process, so there's no need to lock here.
+void ChildProcess::OnExit(int status) {
+  HandleScope scope;
+
+  handle_->Set(pid_symbol, Null());
+
+  Local<Value> onexit_v = handle_->Get(onexit_symbol);
+  assert(onexit_v->IsFunction());
+  Local<Function> onexit = Local<Function>::Cast(onexit_v);
+
+  TryCatch try_catch;
+  Local<Value> argv[2];
+
+  argv[0] = Integer::New(status);
+
+  if (exit_signal_ != 0) {
+    argv[1] = Integer::New(exit_signal_);
+  } else {
+    argv[1] = Local<Value>::New(Null());
+  }
+
+  onexit->Call(handle_, 2, argv);
+
+  if (try_catch.HasCaught()) {
+    FatalException(try_catch);
+  }
+}
+
+
+void ChildProcess::Initialize(Handle<Object> target) {
+  HandleScope scope;
+
+  Local<FunctionTemplate> t = FunctionTemplate::New(ChildProcess::New);
+  t->InstanceTemplate()->SetInternalFieldCount(1);
+  t->SetClassName(String::NewSymbol("ChildProcess"));
+
+  pid_symbol = NODE_PSYMBOL("pid");
+  onexit_symbol = NODE_PSYMBOL("onexit");
+
+  NODE_SET_PROTOTYPE_METHOD(t, "spawn", ChildProcess::Spawn);
+  NODE_SET_PROTOTYPE_METHOD(t, "kill", ChildProcess::Kill);
+
+  target->Set(String::NewSymbol("ChildProcess"), t->GetFunction());
+
+  ev_async_init(&watcher_status.async_watcher, notify_exit);
+  watcher_status.lock = CreateSemaphore(NULL, 1, 1, NULL);
+}
+
+}  // namespace node
+
+NODE_MODULE(node_child_process, node::ChildProcess::Initialize);
index a7db557..131d34e 100644 (file)
@@ -2,9 +2,7 @@
 NODE_EXT_LIST_START
 NODE_EXT_LIST_ITEM(node_buffer)
 NODE_EXT_LIST_ITEM(node_cares)
-#ifdef __POSIX__
 NODE_EXT_LIST_ITEM(node_child_process)
-#endif
 #ifdef HAVE_OPENSSL
 NODE_EXT_LIST_ITEM(node_crypto)
 #endif