diff --git a/include/uv-unix.h b/include/uv-unix.h index cad2ed4a..72c66a9f 100644 --- a/include/uv-unix.h +++ b/include/uv-unix.h @@ -132,4 +132,7 @@ typedef struct { struct addrinfo* res; \ int retcode; +#define UV_PROCESS_PRIVATE_FIELDS \ + ev_child child_watcher; + #endif /* UV_UNIX_H */ diff --git a/include/uv.h b/include/uv.h index e2ef403b..d72caa6d 100644 --- a/include/uv.h +++ b/include/uv.h @@ -50,6 +50,7 @@ typedef struct uv_check_s uv_check_t; typedef struct uv_idle_s uv_idle_t; typedef struct uv_async_s uv_async_t; typedef struct uv_getaddrinfo_s uv_getaddrinfo_t; +typedef struct uv_process_s uv_process_t; /* Request types */ typedef struct uv_req_s uv_req_t; typedef struct uv_shutdown_s uv_shutdown_t; @@ -85,6 +86,7 @@ typedef void (*uv_prepare_cb)(uv_prepare_t* handle, int status); typedef void (*uv_check_cb)(uv_check_t* handle, int status); typedef void (*uv_idle_cb)(uv_idle_t* handle, int status); typedef void (*uv_getaddrinfo_cb)(uv_getaddrinfo_t* handle, int status, struct addrinfo* res); +typedef void (*uv_exit_cb)(uv_process_t*, int exit_status, int term_signal); /* Expand this list if necessary. */ @@ -145,7 +147,8 @@ typedef enum { UV_ASYNC, UV_ARES_TASK, UV_ARES_EVENT, - UV_GETADDRINFO + UV_GETADDRINFO, + UV_PROCESS } uv_handle_type; typedef enum { @@ -495,6 +498,41 @@ struct uv_getaddrinfo_s { const char* service, const struct addrinfo* hints); +/* + * Child process. Subclass of uv_handle_t. + */ +typedef struct uv_process_options_s { + uv_exit_cb exit_cb; + const char* file; + char** args; + char** env; + char* cwd; + /* + * The user should supply pointers to uninitialized uv_pipe_t structs for + * stdio. They will be initialized by uv_spawn. The user is reponsible for + * calling uv_close on them. + */ + uv_pipe_t* stdin_stream; + uv_pipe_t* stdout_stream; + uv_pipe_t* stderr_stream; +} uv_process_options_t; + +struct uv_process_s { + UV_HANDLE_FIELDS + uv_exit_cb exit_cb; + int pid; + UV_PROCESS_PRIVATE_FIELDS +}; + +/* Initializes uv_process_t and starts the process. */ +int uv_spawn(uv_process_t*, uv_process_options_t options); + +/* + * Kills the process with the specified signal. The user must still + * call uv_close on the process. + */ +int uv_process_kill(uv_process_t*, int signum); + /* * Most functions return boolean: 0 for success and -1 for failure. @@ -576,6 +614,7 @@ typedef struct { uint64_t idle_init; uint64_t async_init; uint64_t timer_init; + uint64_t process_init; } uv_counters_t; uv_counters_t* uv_counters(); diff --git a/src/uv-unix.c b/src/uv-unix.c index 69cdd604..43c0bb6a 100644 --- a/src/uv-unix.c +++ b/src/uv-unix.c @@ -51,6 +51,14 @@ #endif +# ifdef __APPLE__ +# include +# define environ (*_NSGetEnviron()) +# else +extern char **environ; +# endif + + static uv_err_t last_err; struct uv_ares_data_s { @@ -194,6 +202,7 @@ void uv_close(uv_handle_t* handle, uv_close_cb close_cb) { uv_pipe_t* pipe; uv_async_t* async; uv_timer_t* timer; + uv_process_t* process; handle->close_cb = close_cb; @@ -246,6 +255,11 @@ void uv_close(uv_handle_t* handle, uv_close_cb close_cb) { ev_io_stop(EV_DEFAULT_ &pipe->write_watcher); break; + case UV_PROCESS: + process = (uv_process_t*)handle; + ev_child_stop(EV_DEFAULT_UC_ &process->child_watcher); + break; + default: assert(0); } @@ -583,6 +597,10 @@ void uv__finish_close(uv_handle_t* handle) { break; } + case UV_PROCESS: + assert(!ev_is_active(&((uv_process_t*)handle)->child_watcher)); + break; + default: assert(0); break; @@ -1562,7 +1580,7 @@ int64_t uv_timer_get_repeat(uv_timer_t* timer) { /* - * This is called once per second by ares_data.timer. It is used to + * This is called once per second by ares_data.timer. It is used to * constantly callback into c-ares for possibly processing timeouts. */ static void uv__ares_timeout(EV_P_ struct ev_timer* watcher, int revents) { @@ -2137,3 +2155,156 @@ size_t uv__strlcpy(char* dst, const char* src, size_t size) { uv_stream_t* uv_std_handle(uv_std_type type) { assert(0 && "implement me"); } + + +static void uv__chld(EV_P_ ev_child* watcher, int revents) { + int status = watcher->rstatus; + int exit_status = 0; + int term_signal = 0; + uv_process_t *process = watcher->data; + + assert(&process->child_watcher == watcher); + assert(revents & EV_CHILD); + + ev_child_stop(EV_A_ &process->child_watcher); + + if (WIFEXITED(status)) { + exit_status = WEXITSTATUS(status); + } + + if (WIFSIGNALED(status)) { + term_signal = WTERMSIG(status); + } + + if (process->exit_cb) { + process->exit_cb(process, exit_status, term_signal); + } +} + + +int uv_spawn(uv_process_t* process, uv_process_options_t options) { + /* + * Save environ in the case that we get it clobbered + * by the child process. + */ + char** save_our_env = environ; + int stdin_pipe[2] = { -1, -1 }; + int stdout_pipe[2] = { -1, -1 }; + int stderr_pipe[2] = { -1, -1 }; + pid_t pid; + + uv__handle_init((uv_handle_t*)process, UV_PROCESS); + uv_counters()->process_init++; + + process->exit_cb = options.exit_cb; + + + if (options.stdin_stream && pipe(stdin_pipe) < 0) { + goto error; + } + + if (options.stdout_stream && pipe(stdout_pipe) < 0) { + goto error; + } + + if (options.stderr_stream && pipe(stderr_pipe) < 0) { + goto error; + } + + pid = fork(); + + if (pid == 0) { + if (stdin_pipe[0] >= 0) { + close(stdin_pipe[1]); + dup2(stdin_pipe[0], STDIN_FILENO); + } + + if (stdout_pipe[1] >= 0) { + close(stdout_pipe[0]); + dup2(stdout_pipe[1], STDOUT_FILENO); + } + + if (stderr_pipe[1] >= 0) { + close(stderr_pipe[0]); + dup2(stderr_pipe[1], STDERR_FILENO); + } + + if (options.cwd && chdir(options.cwd)) { + perror("chdir()"); + _exit(127); + } + + environ = options.env; + + execvp(options.file, options.args); + perror("execvp()"); + _exit(127); + /* Execution never reaches here. */ + } else if (pid == -1) { + /* Restore environment. */ + environ = save_our_env; + goto error; + } + + /* Parent. */ + + /* Restore environment. */ + environ = save_our_env; + + process->pid = pid; + + ev_child_init(&process->child_watcher, uv__chld, pid, 0); + ev_child_start(EV_DEFAULT_UC_ &process->child_watcher); + process->child_watcher.data = process; + + if (stdin_pipe[1] >= 0) { + assert(options.stdin_stream); + assert(stdin_pipe[0] >= 0); + close(stdin_pipe[0]); + uv__nonblock(stdin_pipe[1], 1); + uv_pipe_init(options.stdin_stream); + uv__stream_open(options.stdin_stream, stdin_pipe[1]); + } + + if (stdout_pipe[0] >= 0) { + assert(options.stdout_stream); + assert(stdout_pipe[1] >= 0); + close(stdout_pipe[1]); + uv__nonblock(stdout_pipe[0], 1); + uv_pipe_init(options.stdout_stream); + uv__stream_open(options.stdout_stream, stdout_pipe[0]); + } + + if (stderr_pipe[0] >= 0) { + assert(options.stderr_stream); + assert(stderr_pipe[1] >= 0); + close(stderr_pipe[1]); + uv__nonblock(stderr_pipe[0], 1); + uv_pipe_init(options.stderr_stream); + uv__stream_open(options.stderr_stream, stderr_pipe[0]); + } + + return 0; + +error: + uv_err_new((uv_handle_t*)process, errno); + close(stdin_pipe[0]); + close(stdin_pipe[1]); + close(stdout_pipe[0]); + close(stdout_pipe[1]); + close(stderr_pipe[0]); + close(stderr_pipe[1]); + return -1; +} + + +int uv_process_kill(uv_process_t* process, int signum) { + int r = kill(process->pid, signum); + + if (r) { + uv_err_new((uv_handle_t*)process, errno); + return -1; + } else { + return 0; + } +} diff --git a/test/run-tests.c b/test/run-tests.c index 7ae6c6c7..a050172e 100644 --- a/test/run-tests.c +++ b/test/run-tests.c @@ -34,8 +34,14 @@ int main(int argc, char **argv) { + int i; + platform_init(argc, argv); + if (argc == 2 && strcmp(argv[1], "spawn_helper1") == 0) { + return 1; + } + switch (argc) { case 1: return run_tests(TEST_TIMEOUT, 0); case 2: return run_test(argv[1], TEST_TIMEOUT, 0); diff --git a/test/test-list.h b/test/test-list.h index de9f1d3a..741cac2e 100644 --- a/test/test-list.h +++ b/test/test-list.h @@ -63,6 +63,7 @@ TEST_DECLARE (gethostbyname) TEST_DECLARE (getsockname) TEST_DECLARE (fail_always) TEST_DECLARE (pass_always) +TEST_DECLARE (spawn_exit_code) HELPER_DECLARE (tcp4_echo_server) HELPER_DECLARE (tcp6_echo_server) HELPER_DECLARE (pipe_echo_server) @@ -140,6 +141,8 @@ TASK_LIST_START TEST_ENTRY (getsockname) + TEST_ENTRY (spawn_exit_code) + #if 0 /* These are for testing the test runner. */ TEST_ENTRY (fail_always) diff --git a/test/test-spawn.c b/test/test-spawn.c new file mode 100644 index 00000000..9690b610 --- /dev/null +++ b/test/test-spawn.c @@ -0,0 +1,74 @@ +/* Copyright Joyent, Inc. and other Node contributors. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include "uv.h" +#include "task.h" +#include +#include + +static int close_cb_called; +static int exit_cb_called; + +static void close_cb(uv_handle_t* handle) { + printf("close_cb\n"); + close_cb_called++; +} + +static void exit_cb(uv_process_t* process, int exit_status, int term_signal) { + printf("exit_cb\n"); + exit_cb_called++; + ASSERT(exit_status == 1); + ASSERT(term_signal == 0); + uv_close((uv_handle_t*)process, close_cb); +} + + +TEST_IMPL(spawn_exit_code) { + int r; + char exepath[1024]; + size_t exepath_size = 1024; + uv_process_t process; + uv_process_options_t options = { 0 }; + /* Note spawn_helper1 defined in test/run-tests.c */ + char* args[3] = { NULL, "spawn_helper1", NULL }; + + r = uv_exepath(exepath, &exepath_size); + ASSERT(r == 0); + exepath[exepath_size] = '\0'; + options.file = exepath; + args[0] = exepath; + + options.args = args; + options.exit_cb = exit_cb; + + uv_init(); + + r = uv_spawn(&process, options); + ASSERT(r == 0); + + r = uv_run(); + ASSERT(r == 0); + + ASSERT(exit_cb_called == 1); + ASSERT(close_cb_called == 1); + + return 0; +}