From 0a2f1cb334e1633f0687bbd3442d433599dc08b0 Mon Sep 17 00:00:00 2001 From: Bert Belder Date: Fri, 3 Dec 2010 01:44:09 +0100 Subject: [PATCH] Child processes --- TODO.win32 | 23 +- src/node_child_process.cc | 4 + src/node_child_process.h | 50 +- src/node_child_process_win32.cc | 877 ++++++++++++++++++++++++++++++++ src/node_extensions.h | 2 - 5 files changed, 947 insertions(+), 9 deletions(-) create mode 100644 src/node_child_process_win32.cc diff --git a/TODO.win32 b/TODO.win32 index b883a8c19c..24e08cfa42 100644 --- a/TODO.win32 +++ b/TODO.win32 @@ -8,12 +8,23 @@ E.g. getaddrinfo() is ansi-only; GetAddrInfoW is utf16-only. Can we get utf16 straight out of v8? Are unix sockets similar to windows named pipes? If so, should they be supported? -> currently: no. Complication: they block. -- Child processes - Should not be too hard using CreatePipe, CreateProcessW and GetExitCodeProcess. - Hooking up a child process to a file handle can be done; hooking up to a normal socket won't work; - we'd need some sort of pump() mechanism. - Waiting for the child to exit is tricky, probably would require a wait thread to wait for the child, then ev_async notify. - How can we distinguish between the exit code and exception number after calling GetExitCodeProcess? +- Child process issues + * Communication between parent and child is slow; it uses a socketpair + where a pipe would be much faster. Replace it by a pipe when there + is a libev backend that supports waiting for a pipe. + * When a child process spawns the pid is not available straightaway. + On linux the pid is available immediately because fork() doesn't + block; on windows a libeio thread is used to call CreateProcess. + So this can't really be fixed, but it could be worked around by adding a + 'spawn' or 'pid' method. + * kill() doesn't work when the pid is not available yet. All the plumbing + is there to make it work, but lib/child_process.js just doesn't call + ChildProcess::Kill() as long as the pid is not known. + * passing socket custom_fds is not supported + * child_process.exec() only works on systems with msys installed. + It's because it relies on the 'sh' shell. The default windows shell + is 'cmd' and it works a little differently. Maybe add an option to + specify the shell to exec()? - Stdio (make TTY's / repl / readline work) This will be hard: there is no ANSI escape code support in windows. diff --git a/src/node_child_process.cc b/src/node_child_process.cc index 000817316a..87c385708a 100644 --- a/src/node_child_process.cc +++ b/src/node_child_process.cc @@ -1,3 +1,7 @@ +#ifdef __MINGW32__ +# include +#endif + #ifdef __POSIX__ // Copyright 2009 Ryan Dahl diff --git a/src/node_child_process.h b/src/node_child_process.h index 0eb38e7830..6d78961232 100644 --- a/src/node_child_process.h +++ b/src/node_child_process.h @@ -7,6 +7,10 @@ #include #include +#ifdef __MINGW32__ +# include // 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 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 index 0000000000..749cd70e69 --- /dev/null +++ b/src/node_child_process_win32.cc @@ -0,0 +1,877 @@ + +// RegisterWaitForSingleObject requires Windows 2000, +// GetProcessId requires windows XP SP1 +#define _WIN32_WINNT 0x0501 + + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + + +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 pid_symbol; +static Persistent 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::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 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 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(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 cmd_args_handle = Local::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 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 + Localcwd_handle = Local::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 env_list_handle = Local::Cast(args[3]); + int envc = env_list_handle->Length(); + Local 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 custom_fds_handle = Local::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 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 ChildProcess::Kill(const Arguments& args) { + HandleScope scope; + ChildProcess *child = ObjectWrap::Unwrap(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 onexit_v = handle_->Get(onexit_symbol); + assert(onexit_v->IsFunction()); + Local onexit = Local::Cast(onexit_v); + + TryCatch try_catch; + Local argv[2]; + + argv[0] = Integer::New(status); + + if (exit_signal_ != 0) { + argv[1] = Integer::New(exit_signal_); + } else { + argv[1] = Local::New(Null()); + } + + onexit->Call(handle_, 2, argv); + + if (try_catch.HasCaught()) { + FatalException(try_catch); + } +} + + +void ChildProcess::Initialize(Handle target) { + HandleScope scope; + + Local 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); diff --git a/src/node_extensions.h b/src/node_extensions.h index a7db557ecb..131d34e6f1 100644 --- a/src/node_extensions.h +++ b/src/node_extensions.h @@ -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