diff --git a/meson.build b/meson.build index c1f3d5d4..f0404c5d 100644 --- a/meson.build +++ b/meson.build @@ -17,6 +17,15 @@ if not lua_dep.found() endif sdl_dep = dependency('sdl2', method: 'config-tool') +reproc_dep = dependency('reproc') + +lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl, libx11] + +if host_machine.system() == 'windows' + # Note that we need to explicitly add the windows socket DLL because + # the pkg-config file from reproc does not include it. + lite_deps += meson.get_compiler('cpp').find_library('ws2_32', required: true) +endif lite_cargs = [] if get_option('portable') diff --git a/src/api/process.c b/src/api/process.c index 92bd016a..1309ede5 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -8,11 +8,10 @@ #include #include #include +#include #include #include -#include "reproc.h" - typedef struct { reproc_t * process; lua_State* L; diff --git a/src/meson.build b/src/meson.build index 469cba1e..9516c94d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,8 +1,3 @@ -platform = 'posix' -if host_machine.system() == 'windows' - platform = 'windows' -endif - lite_sources = [ 'api/api.c', 'api/cp_replace.c', @@ -11,8 +6,6 @@ lite_sources = [ 'api/regex.c', 'api/system.c', 'api/process.c', - 'reproc.c', - 'reproc.' + platform + '.c', 'renderer.c', 'renwindow.c', 'fontdesc.c', @@ -27,7 +20,7 @@ endif executable('lite', lite_sources + lite_rc, include_directories: [lite_include, font_renderer_include], - dependencies: [lua_dep, sdl_dep, pcre2_dep, libm, libdl, libx11], + dependencies: lite_deps, c_args: lite_cargs, link_with: libfontrenderer, link_args: lite_link_args, diff --git a/src/reproc.c b/src/reproc.c deleted file mode 100644 index 9ab5a7f2..00000000 --- a/src/reproc.c +++ /dev/null @@ -1,1246 +0,0 @@ -/** - * @copyright Daan De Meyer - * @license MIT - * @link https://github.com/DaanDeMeyer/reproc Source code. - * @note Amalgamation by Jefferson Gonzalez - */ - -#include "reproc_private.h" - -// -// drain.c -// -int reproc_drain(reproc_t *process, reproc_sink out, reproc_sink err) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(out.function); - ASSERT_EINVAL(err.function); - - const uint8_t initial = 0; - int r = -1; - - // A single call to `read` might contain multiple messages. By always calling - // both sinks once with no data before reading, we give them the chance to - // process all previous output one by one before reading from the child - // process again. - - r = out.function(REPROC_STREAM_IN, &initial, 0, out.context); - if (r != 0) { - return r; - } - - r = err.function(REPROC_STREAM_IN, &initial, 0, err.context); - if (r != 0) { - return r; - } - - uint8_t buffer[4096]; - - for (;;) { - reproc_event_source source = { process, REPROC_EVENT_OUT | REPROC_EVENT_ERR, - 0 }; - - r = reproc_poll(&source, 1, REPROC_INFINITE); - if (r < 0) { - r = r == REPROC_EPIPE ? 0 : r; - break; - } - - if (source.events & REPROC_EVENT_DEADLINE) { - r = REPROC_ETIMEDOUT; - break; - } - - REPROC_STREAM stream = source.events & REPROC_EVENT_OUT ? REPROC_STREAM_OUT - : REPROC_STREAM_ERR; - - r = reproc_read(process, stream, buffer, ARRAY_SIZE(buffer)); - if (r < 0 && r != REPROC_EPIPE) { - break; - } - - size_t bytes_read = r == REPROC_EPIPE ? 0 : (size_t) r; - reproc_sink sink = stream == REPROC_STREAM_OUT ? out : err; - - r = sink.function(stream, buffer, bytes_read, sink.context); - if (r != 0) { - break; - } - } - - return r; -} - -static int sink_string(REPROC_STREAM stream, - const uint8_t *buffer, - size_t size, - void *context) -{ - (void) stream; - - char **string = (char **) context; - size_t string_size = *string == NULL ? 0 : strlen(*string); - - char *r = (char *) realloc(*string, string_size + size + 1); - if (r == NULL) { - return REPROC_ENOMEM; - } - - *string = r; - memcpy(*string + string_size, buffer, size); - (*string)[string_size + size] = '\0'; - - return 0; -} - -reproc_sink reproc_sink_string(char **output) -{ - return (reproc_sink){ sink_string, output }; -} - -static int sink_discard(REPROC_STREAM stream, - const uint8_t *buffer, - size_t size, - void *context) -{ - (void) stream; - (void) buffer; - (void) size; - (void) context; - - return 0; -} - -reproc_sink reproc_sink_discard(void) -{ - return (reproc_sink){ sink_discard, NULL }; -} - -const reproc_sink REPROC_SINK_NULL = { sink_discard, NULL }; - -void *reproc_free(void *ptr) -{ - free(ptr); - return NULL; -} - -// -// options.c -// -static bool redirect_is_set(reproc_redirect redirect) -{ - return redirect.type || redirect.handle || redirect.file || redirect.path; -} - -static int parse_redirect(reproc_redirect *redirect, - REPROC_STREAM stream, - bool parent, - bool discard, - FILE *file, - const char *path) -{ - ASSERT(redirect); - - if (file) { - ASSERT_EINVAL(!redirect_is_set(*redirect)); - ASSERT_EINVAL(!parent && !discard && !path); - redirect->type = REPROC_REDIRECT_FILE; - redirect->file = file; - } - - if (path) { - ASSERT_EINVAL(!redirect_is_set(*redirect)); - ASSERT_EINVAL(!parent && !discard && !file); - redirect->type = REPROC_REDIRECT_PATH; - redirect->path = path; - } - - if (redirect->type == REPROC_REDIRECT_HANDLE || redirect->handle) { - ASSERT_EINVAL(redirect->type == REPROC_REDIRECT_DEFAULT || - redirect->type == REPROC_REDIRECT_HANDLE); - ASSERT_EINVAL(redirect->handle); - ASSERT_EINVAL(!redirect->file && !redirect->path); - redirect->type = REPROC_REDIRECT_HANDLE; - } - - if (redirect->type == REPROC_REDIRECT_FILE || redirect->file) { - ASSERT_EINVAL(redirect->type == REPROC_REDIRECT_DEFAULT || - redirect->type == REPROC_REDIRECT_FILE); - ASSERT_EINVAL(redirect->file); - ASSERT_EINVAL(!redirect->handle && !redirect->path); - redirect->type = REPROC_REDIRECT_FILE; - } - - if (redirect->type == REPROC_REDIRECT_PATH || redirect->path) { - ASSERT_EINVAL(redirect->type == REPROC_REDIRECT_DEFAULT || - redirect->type == REPROC_REDIRECT_PATH); - ASSERT_EINVAL(redirect->path); - ASSERT_EINVAL(!redirect->handle && !redirect->file); - redirect->type = REPROC_REDIRECT_PATH; - } - - if (redirect->type == REPROC_REDIRECT_DEFAULT) { - if (parent) { - ASSERT_EINVAL(!discard); - redirect->type = REPROC_REDIRECT_PARENT; - } else if (discard) { - ASSERT_EINVAL(!parent); - redirect->type = REPROC_REDIRECT_DISCARD; - } else { - redirect->type = stream == REPROC_STREAM_ERR ? REPROC_REDIRECT_PARENT - : REPROC_REDIRECT_PIPE; - } - } - - return 0; -} - -reproc_stop_actions parse_stop_actions(reproc_stop_actions stop) -{ - bool is_noop = stop.first.action == REPROC_STOP_NOOP && - stop.second.action == REPROC_STOP_NOOP && - stop.third.action == REPROC_STOP_NOOP; - - if (is_noop) { - stop.first.action = REPROC_STOP_WAIT; - stop.first.timeout = REPROC_DEADLINE; - stop.second.action = REPROC_STOP_TERMINATE; - stop.second.timeout = REPROC_INFINITE; - } - - return stop; -} - -int parse_options(reproc_options *options, const char *const *argv) -{ - ASSERT(options); - - int r = -1; - - r = parse_redirect(&options->redirect.in, REPROC_STREAM_IN, - options->redirect.parent, options->redirect.discard, NULL, - NULL); - if (r < 0) { - return r; - } - - r = parse_redirect(&options->redirect.out, REPROC_STREAM_OUT, - options->redirect.parent, options->redirect.discard, - options->redirect.file, options->redirect.path); - if (r < 0) { - return r; - } - - r = parse_redirect(&options->redirect.err, REPROC_STREAM_ERR, - options->redirect.parent, options->redirect.discard, - options->redirect.file, options->redirect.path); - if (r < 0) { - return r; - } - - if (options->input.data != NULL || options->input.size > 0) { - ASSERT_EINVAL(options->input.data != NULL); - ASSERT_EINVAL(options->input.size > 0); - ASSERT_EINVAL(options->redirect.in.type == REPROC_REDIRECT_PIPE); - } - - if (options->fork) { - ASSERT_EINVAL(argv == NULL); - } else { - ASSERT_EINVAL(argv != NULL && argv[0] != NULL); - } - - if (options->deadline == 0) { - options->deadline = REPROC_INFINITE; - } - - options->stop = parse_stop_actions(options->stop); - - return 0; -} - -// -// redirect.c -// -static int redirect_pipe(pipe_type *parent, - handle_type *child, - REPROC_STREAM stream, - bool nonblocking) -{ - ASSERT(parent); - ASSERT(child); - - pipe_type pipe[] = { PIPE_INVALID, PIPE_INVALID }; - int r = -1; - - r = pipe_init(&pipe[0], &pipe[1]); - if (r < 0) { - goto finish; - } - - r = pipe_nonblocking(stream == REPROC_STREAM_IN ? pipe[1] : pipe[0], - nonblocking); - if (r < 0) { - goto finish; - } - - *parent = stream == REPROC_STREAM_IN ? pipe[1] : pipe[0]; - *child = stream == REPROC_STREAM_IN ? (handle_type) pipe[0] - : (handle_type) pipe[1]; - -finish: - if (r < 0) { - pipe_destroy(pipe[0]); - pipe_destroy(pipe[1]); - } - - return r; -} - -int redirect_init(pipe_type *parent, - handle_type *child, - REPROC_STREAM stream, - reproc_redirect redirect, - bool nonblocking, - handle_type out) -{ - ASSERT(parent); - ASSERT(child); - - int r = REPROC_EINVAL; - - switch (redirect.type) { - - case REPROC_REDIRECT_DEFAULT: - ASSERT(false); - break; - - case REPROC_REDIRECT_PIPE: - r = redirect_pipe(parent, child, stream, nonblocking); - break; - - case REPROC_REDIRECT_PARENT: - r = redirect_parent(child, stream); - if (r == REPROC_EPIPE) { - // Discard if the corresponding parent stream is closed. - r = redirect_discard(child, stream); - } - - if (r < 0) { - break; - } - - *parent = PIPE_INVALID; - - break; - - case REPROC_REDIRECT_DISCARD: - r = redirect_discard(child, stream); - if (r < 0) { - break; - } - - *parent = PIPE_INVALID; - - break; - - case REPROC_REDIRECT_HANDLE: - ASSERT(redirect.handle); - - r = 0; - - *child = redirect.handle; - *parent = PIPE_INVALID; - - break; - - case REPROC_REDIRECT_FILE: - ASSERT(redirect.file); - - r = redirect_file(child, redirect.file); - if (r < 0) { - break; - } - - *parent = PIPE_INVALID; - - break; - - case REPROC_REDIRECT_STDOUT: - ASSERT(stream == REPROC_STREAM_ERR); - ASSERT(out != HANDLE_INVALID); - - r = 0; - - *child = out; - *parent = PIPE_INVALID; - - break; - - case REPROC_REDIRECT_PATH: - ASSERT(redirect.path); - - r = redirect_path(child, stream, redirect.path); - if (r < 0) { - break; - } - - *parent = PIPE_INVALID; - - break; - } - - return r; -} - -handle_type redirect_destroy(handle_type child, REPROC_REDIRECT type) -{ - if (child == HANDLE_INVALID) { - return HANDLE_INVALID; - } - - switch (type) { - case REPROC_REDIRECT_DEFAULT: - ASSERT(false); - break; - case REPROC_REDIRECT_PIPE: - // We know `handle` is a pipe if `REDIRECT_PIPE` is used so the cast is - // safe. This little hack prevents us from having to introduce a generic - // handle type. - pipe_destroy((pipe_type) child); - break; - case REPROC_REDIRECT_DISCARD: - case REPROC_REDIRECT_PATH: - handle_destroy(child); - break; - case REPROC_REDIRECT_PARENT: - case REPROC_REDIRECT_FILE: - case REPROC_REDIRECT_HANDLE: - case REPROC_REDIRECT_STDOUT: - break; - } - - return HANDLE_INVALID; -} - -// -// reproc.c -// -struct reproc_t { - process_type handle; - - struct { - pipe_type in; - pipe_type out; - pipe_type err; - pipe_type exit; - } pipe; - - int status; - reproc_stop_actions stop; - int64_t deadline; - bool nonblocking; - - struct { - pipe_type out; - pipe_type err; - } child; -}; - -enum { - STATUS_NOT_STARTED = -1, - STATUS_IN_PROGRESS = -2, - STATUS_IN_CHILD = -3, -}; - -#define SIGOFFSET 128 - -const int REPROC_SIGKILL = SIGOFFSET + 9; -const int REPROC_SIGTERM = SIGOFFSET + 15; - -const int REPROC_INFINITE = -1; -const int REPROC_DEADLINE = -2; - -static int setup_input(pipe_type *pipe, const uint8_t *data, size_t size) -{ - if (data == NULL || size == 0) { - ASSERT(data == NULL); - ASSERT(size == 0); - return 0; - } - - ASSERT(pipe && *pipe != PIPE_INVALID); - - // `reproc_write` only needs the child process stdin pipe to be initialized. - size_t written = 0; - int r = -1; - - // Make sure we don't block indefinitely when `input` is bigger than the - // size of the pipe. - r = pipe_nonblocking(*pipe, true); - if (r < 0) { - return r; - } - - while (written < size) { - r = pipe_write(*pipe, data + written, size - written); - if (r < 0) { - return r; - } - - ASSERT(written + (size_t) r <= size); - written += (size_t) r; - } - - *pipe = pipe_destroy(*pipe); - - return 0; -} - -static int expiry(int timeout, int64_t deadline) -{ - if (timeout == REPROC_INFINITE && deadline == REPROC_INFINITE) { - return REPROC_INFINITE; - } - - if (deadline == REPROC_INFINITE) { - return timeout; - } - - int64_t n = now(); - - if (n >= deadline) { - return REPROC_DEADLINE; - } - - // `deadline` exceeds `now` by at most a full `int` so the cast is safe. - int remaining = (int) (deadline - n); - - if (timeout == REPROC_INFINITE) { - return remaining; - } - - return MIN(timeout, remaining); -} - -static size_t find_earliest_deadline(reproc_event_source *sources, - size_t num_sources) -{ - ASSERT(sources); - ASSERT(num_sources > 0); - - size_t earliest = 0; - int min = REPROC_INFINITE; - - for (size_t i = 0; i < num_sources; i++) { - reproc_t *process = sources[i].process; - - if (process == NULL) { - continue; - } - - int current = expiry(REPROC_INFINITE, process->deadline); - - if (current == REPROC_DEADLINE) { - return i; - } - - if (min == REPROC_INFINITE || current < min) { - earliest = i; - min = current; - } - } - - return earliest; -} - -reproc_t *reproc_new(void) -{ - reproc_t *process = malloc(sizeof(reproc_t)); - if (process == NULL) { - return NULL; - } - - *process = (reproc_t){ .handle = PROCESS_INVALID, - .pipe = { .in = PIPE_INVALID, - .out = PIPE_INVALID, - .err = PIPE_INVALID, - .exit = PIPE_INVALID }, - .child = { .out = PIPE_INVALID, .err = PIPE_INVALID }, - .status = STATUS_NOT_STARTED, - .deadline = REPROC_INFINITE }; - - return process; -} - -int reproc_start(reproc_t *process, - const char *const *argv, - reproc_options options) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status == STATUS_NOT_STARTED); - - struct { - handle_type in; - handle_type out; - handle_type err; - pipe_type exit; - } child = { HANDLE_INVALID, HANDLE_INVALID, HANDLE_INVALID, PIPE_INVALID }; - int r = -1; - - r = init(); - if (r < 0) { - return r; // Make sure we can always call `deinit` in `finish`. - } - - r = parse_options(&options, argv); - if (r < 0) { - goto finish; - } - - r = redirect_init(&process->pipe.in, &child.in, REPROC_STREAM_IN, - options.redirect.in, options.nonblocking, HANDLE_INVALID); - if (r < 0) { - goto finish; - } - - r = redirect_init(&process->pipe.out, &child.out, REPROC_STREAM_OUT, - options.redirect.out, options.nonblocking, HANDLE_INVALID); - if (r < 0) { - goto finish; - } - - r = redirect_init(&process->pipe.err, &child.err, REPROC_STREAM_ERR, - options.redirect.err, options.nonblocking, child.out); - if (r < 0) { - goto finish; - } - - r = pipe_init(&process->pipe.exit, &child.exit); - if (r < 0) { - goto finish; - } - - r = setup_input(&process->pipe.in, options.input.data, options.input.size); - if (r < 0) { - goto finish; - } - - struct process_options process_options = { - .env = { .behavior = options.env.behavior, .extra = options.env.extra }, - .working_directory = options.working_directory, - .handle = { .in = child.in, - .out = child.out, - .err = child.err, - .exit = (handle_type) child.exit } - }; - - r = process_start(&process->handle, argv, process_options); - if (r < 0) { - goto finish; - } - - if (r > 0) { - process->stop = options.stop; - - if (options.deadline != REPROC_INFINITE) { - process->deadline = now() + options.deadline; - } - - process->nonblocking = options.nonblocking; - } - -finish: - // Either an error has ocurred or the child pipe endpoints have been copied to - // the stdin/stdout/stderr streams of the child process. Either way, they can - // be safely closed. - redirect_destroy(child.in, options.redirect.in.type); - - // See `reproc_poll` for why we do this. - -#ifdef _WIN32 - if (r < 0 || options.redirect.out.type != REPROC_REDIRECT_PIPE) { - child.out = redirect_destroy(child.out, options.redirect.out.type); - } - - if (r < 0 || options.redirect.err.type != REPROC_REDIRECT_PIPE) { - child.err = redirect_destroy(child.err, options.redirect.err.type); - } -#else - child.out = redirect_destroy(child.out, options.redirect.out.type); - child.err = redirect_destroy(child.err, options.redirect.err.type); -#endif - - pipe_destroy(child.exit); - - if (r < 0) { - process->handle = process_destroy(process->handle); - process->pipe.in = pipe_destroy(process->pipe.in); - process->pipe.out = pipe_destroy(process->pipe.out); - process->pipe.err = pipe_destroy(process->pipe.err); - process->pipe.exit = pipe_destroy(process->pipe.exit); - deinit(); - } else if (r == 0) { - process->handle = PROCESS_INVALID; - // `process_start` has already taken care of closing the handles for us. - process->pipe.in = PIPE_INVALID; - process->pipe.out = PIPE_INVALID; - process->pipe.err = PIPE_INVALID; - process->pipe.exit = PIPE_INVALID; - process->status = STATUS_IN_CHILD; - } else { - process->child.out = (pipe_type) child.out; - process->child.err = (pipe_type) child.err; - process->status = STATUS_IN_PROGRESS; - } - - return r; -} - -enum { PIPES_PER_SOURCE = 4 }; - -static bool contains_valid_pipe(pipe_event_source *sources, size_t num_sources) -{ - for (size_t i = 0; i < num_sources; i++) { - if (sources[i].pipe != PIPE_INVALID) { - return true; - } - } - - return false; -} - -int reproc_poll(reproc_event_source *sources, size_t num_sources, int timeout) -{ - ASSERT_EINVAL(sources); - ASSERT_EINVAL(num_sources > 0); - - size_t earliest = find_earliest_deadline(sources, num_sources); - int64_t deadline = sources[earliest].process == NULL - ? REPROC_INFINITE - : sources[earliest].process->deadline; - - int first = expiry(timeout, deadline); - size_t num_pipes = num_sources * PIPES_PER_SOURCE; - int r = REPROC_ENOMEM; - - if (first == REPROC_DEADLINE) { - for (size_t i = 0; i < num_sources; i++) { - sources[i].events = 0; - } - - sources[earliest].events = REPROC_EVENT_DEADLINE; - return 1; - } - - pipe_event_source *pipes = calloc(num_pipes, sizeof(pipe_event_source)); - if (pipes == NULL) { - return r; - } - - for (size_t i = 0; i < num_pipes; i++) { - pipes[i].pipe = PIPE_INVALID; - } - - for (size_t i = 0; i < num_sources; i++) { - size_t j = i * PIPES_PER_SOURCE; - reproc_t *process = sources[i].process; - int interests = sources[i].interests; - - if (process == NULL) { - continue; - } - - bool in = interests & REPROC_EVENT_IN; - pipes[j + 0].pipe = in ? process->pipe.in : PIPE_INVALID; - pipes[j + 0].interests = PIPE_EVENT_OUT; - - bool out = interests & REPROC_EVENT_OUT; - pipes[j + 1].pipe = out ? process->pipe.out : PIPE_INVALID; - pipes[j + 1].interests = PIPE_EVENT_IN; - - bool err = interests & REPROC_EVENT_ERR; - pipes[j + 2].pipe = err ? process->pipe.err : PIPE_INVALID; - pipes[j + 2].interests = PIPE_EVENT_IN; - - bool exit = (interests & REPROC_EVENT_EXIT) || - (interests & REPROC_EVENT_OUT && - process->child.out != PIPE_INVALID) || - (interests & REPROC_EVENT_ERR && - process->child.err != PIPE_INVALID); - pipes[j + 3].pipe = exit ? process->pipe.exit : PIPE_INVALID; - pipes[j + 3].interests = PIPE_EVENT_IN; - } - - if (!contains_valid_pipe(pipes, num_pipes)) { - r = REPROC_EPIPE; - goto finish; - } - - r = pipe_poll(pipes, num_pipes, first); - if (r < 0) { - goto finish; - } - - for (size_t i = 0; i < num_sources; i++) { - sources[i].events = 0; - } - - if (r == 0 && first != timeout) { - // Differentiate between timeout and deadline expiry. Deadline expiry is an - // event, timeouts are not. - sources[earliest].events = REPROC_EVENT_DEADLINE; - r = 1; - } else if (r > 0) { - // Convert pipe events to process events. - for (size_t i = 0; i < num_pipes; i++) { - if (pipes[i].pipe == PIPE_INVALID) { - continue; - } - - if (pipes[i].events > 0) { - // Index in a set of pipes determines the process pipe and thus the - // process event. - // 0 = stdin pipe => REPROC_EVENT_IN - // 1 = stdout pipe => REPROC_EVENT_OUT - // ... - int event = 1 << (i % PIPES_PER_SOURCE); - sources[i / PIPES_PER_SOURCE].events |= event; - } - } - - r = 0; - - // Count the number of processes with events. - for (size_t i = 0; i < num_sources; i++) { - r += sources[i].events > 0; - } - - // On Windows, when redirecting to sockets, we keep the child handles alive - // in the parent process (see `reproc_start`). We do this because Windows - // doesn't correctly flush redirected socket handles when a child process - // exits. This can lead to data loss where the parent process doesn't - // receive all output of the child process. To get around this, we keep an - // extra handle open in the parent process which we close correctly when we - // detect the child process has exited. Detecting whether a child process - // has exited happens via another inherited socket, but here there's no - // danger of data loss because no data is received over this socket. - - bool again = false; - - for (size_t i = 0; i < num_sources; i++) { - if (!(sources[i].events & REPROC_EVENT_EXIT)) { - continue; - } - - reproc_t *process = sources[i].process; - - if (process->child.out == PIPE_INVALID && - process->child.err == PIPE_INVALID) { - continue; - } - - r = pipe_shutdown(process->child.out); - if (r < 0) { - goto finish; - } - - r = pipe_shutdown(process->child.err); - if (r < 0) { - goto finish; - } - - process->child.out = pipe_destroy(process->child.out); - process->child.err = pipe_destroy(process->child.err); - again = true; - } - - // If we've closed handles, we poll again so we can include any new close - // events that occurred because we closed handles. - - if (again) { - r = reproc_poll(sources, num_sources, timeout); - if (r < 0) { - goto finish; - } - } - } - -finish: - free(pipes); - - return r; -} - -int reproc_read(reproc_t *process, - REPROC_STREAM stream, - uint8_t *buffer, - size_t size) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status != STATUS_IN_CHILD); - ASSERT_EINVAL(stream == REPROC_STREAM_OUT || stream == REPROC_STREAM_ERR); - ASSERT_EINVAL(buffer); - - pipe_type *pipe = stream == REPROC_STREAM_OUT ? &process->pipe.out - : &process->pipe.err; - pipe_type child = stream == REPROC_STREAM_OUT ? process->child.out - : process->child.err; - int r = -1; - - if (*pipe == PIPE_INVALID) { - return REPROC_EPIPE; - } - - // If we've kept extra handles open in the parent, make sure we use - // `reproc_poll` which closes the extra handles we keep open when the child - // process exits. If we don't, `pipe_read` will block forever because the - // extra handles we keep open in the parent would never be closed. - if (child != PIPE_INVALID) { - int event = stream == REPROC_STREAM_OUT ? REPROC_EVENT_OUT - : REPROC_EVENT_ERR; - reproc_event_source source = { process, event, 0 }; - r = reproc_poll(&source, 1, process->nonblocking ? 0 : REPROC_INFINITE); - if (r <= 0) { - return r == 0 ? -REPROC_EWOULDBLOCK : r; - } - } - - r = pipe_read(*pipe, buffer, size); - - if (r == REPROC_EPIPE) { - *pipe = pipe_destroy(*pipe); - } - - return r; -} - -int reproc_write(reproc_t *process, const uint8_t *buffer, size_t size) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status != STATUS_IN_CHILD); - - if (buffer == NULL) { - // Allow `NULL` buffers but only if `size == 0`. - ASSERT_EINVAL(size == 0); - return 0; - } - - if (process->pipe.in == PIPE_INVALID) { - return REPROC_EPIPE; - } - - int r = pipe_write(process->pipe.in, buffer, size); - - if (r == REPROC_EPIPE) { - process->pipe.in = pipe_destroy(process->pipe.in); - } - - return r; -} - -int reproc_close(reproc_t *process, REPROC_STREAM stream) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status != STATUS_IN_CHILD); - - switch (stream) { - case REPROC_STREAM_IN: - process->pipe.in = pipe_destroy(process->pipe.in); - return 0; - case REPROC_STREAM_OUT: - process->pipe.out = pipe_destroy(process->pipe.out); - return 0; - case REPROC_STREAM_ERR: - process->pipe.err = pipe_destroy(process->pipe.err); - return 0; - } - - return REPROC_EINVAL; -} - -int reproc_wait(reproc_t *process, int timeout) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status != STATUS_IN_CHILD); - ASSERT_EINVAL(process->status != STATUS_NOT_STARTED); - - int r = -1; - - if (process->status >= 0) { - return process->status; - } - - if (timeout == REPROC_DEADLINE) { - timeout = expiry(REPROC_INFINITE, process->deadline); - // If the deadline has expired, `expiry` returns `REPROC_DEADLINE` which - // means we'll only check if the process is still running. - if (timeout == REPROC_DEADLINE) { - timeout = 0; - } - } - - ASSERT(process->pipe.exit != PIPE_INVALID); - - pipe_event_source source = { .pipe = process->pipe.exit, - .interests = PIPE_EVENT_IN }; - - r = pipe_poll(&source, 1, timeout); - if (r <= 0) { - return r == 0 ? REPROC_ETIMEDOUT : r; - } - - r = process_wait(process->handle); - if (r < 0) { - return r; - } - - process->pipe.exit = pipe_destroy(process->pipe.exit); - - return process->status = r; -} - -int reproc_terminate(reproc_t *process) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status != STATUS_IN_CHILD); - ASSERT_EINVAL(process->status != STATUS_NOT_STARTED); - - if (process->status >= 0) { - return 0; - } - - return process_terminate(process->handle); -} - -int reproc_kill(reproc_t *process) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status != STATUS_IN_CHILD); - ASSERT_EINVAL(process->status != STATUS_NOT_STARTED); - - if (process->status >= 0) { - return 0; - } - - return process_kill(process->handle); -} - -int reproc_stop(reproc_t *process, reproc_stop_actions stop) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status != STATUS_IN_CHILD); - ASSERT_EINVAL(process->status != STATUS_NOT_STARTED); - - stop = parse_stop_actions(stop); - - reproc_stop_action actions[] = { stop.first, stop.second, stop.third }; - int r = -1; - - for (size_t i = 0; i < ARRAY_SIZE(actions); i++) { - r = REPROC_EINVAL; // NOLINT - - switch (actions[i].action) { - case REPROC_STOP_NOOP: - r = 0; - continue; - case REPROC_STOP_WAIT: - r = 0; - break; - case REPROC_STOP_TERMINATE: - r = reproc_terminate(process); - break; - case REPROC_STOP_KILL: - r = reproc_kill(process); - break; - } - - // Stop if `reproc_terminate` or `reproc_kill` fail. - if (r < 0) { - break; - } - - r = reproc_wait(process, actions[i].timeout); - if (r != REPROC_ETIMEDOUT) { - break; - } - } - - return r; -} - -int reproc_pid(reproc_t *process) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status != STATUS_IN_CHILD); - ASSERT_EINVAL(process->status != STATUS_NOT_STARTED); - - return process_pid(process->handle); -} - -reproc_t *reproc_destroy(reproc_t *process) -{ - ASSERT_RETURN(process, NULL); - - if (process->status == STATUS_IN_PROGRESS) { - reproc_stop(process, process->stop); - } - - process_destroy(process->handle); - pipe_destroy(process->pipe.in); - pipe_destroy(process->pipe.out); - pipe_destroy(process->pipe.err); - pipe_destroy(process->pipe.exit); - - pipe_destroy(process->child.out); - pipe_destroy(process->child.err); - - if (process->status != STATUS_NOT_STARTED) { - deinit(); - } - - free(process); - - return NULL; -} - -const char *reproc_strerror(int error) -{ - return error_string(error); -} - -// -// run.c -// -int reproc_run(const char *const *argv, reproc_options options) -{ - if (!options.redirect.discard && !options.redirect.file && - !options.redirect.path) { - options.redirect.parent = true; - } - - return reproc_run_ex(argv, options, REPROC_SINK_NULL, REPROC_SINK_NULL); -} - -int reproc_run_ex(const char *const *argv, - reproc_options options, // lgtm [cpp/large-parameter] - reproc_sink out, - reproc_sink err) -{ - reproc_t *process = NULL; - int r = REPROC_ENOMEM; - - // There's no way for `reproc_run_ex` to inform the caller whether we're in - // the forked process or the parent process so let's not allow forking when - // using `reproc_run_ex`. - ASSERT_EINVAL(!options.fork); - - process = reproc_new(); - if (process == NULL) { - goto finish; - } - - r = reproc_start(process, argv, options); - if (r < 0) { - goto finish; - } - - r = reproc_drain(process, out, err); - if (r < 0) { - goto finish; - } - - r = reproc_stop(process, options.stop); - if (r < 0) { - goto finish; - } - -finish: - reproc_destroy(process); - - return r; -} - -// -// strv.c -// -static char *str_dup(const char *s) -{ - ASSERT_RETURN(s, NULL); - - char *r = malloc(strlen(s) + 1); - if (!r) { - return NULL; - } - - strcpy(r, s); // NOLINT - - return r; -} - -char **strv_concat(char *const *a, const char *const *b) -{ - char *const *i = NULL; - const char *const *j = NULL; - size_t size = 1; - size_t c = 0; - - STRV_FOREACH(i, a) { - size++; - } - - STRV_FOREACH(j, b) { - size++; - } - - char **r = calloc(size, sizeof(char *)); - if (!r) { - goto finish; - } - - STRV_FOREACH(i, a) { - r[c] = str_dup(*i); - if (!r[c]) { - goto finish; - } - - c++; - } - - STRV_FOREACH(j, b) { - r[c] = str_dup(*j); - if (!r[c]) { - goto finish; - } - - c++; - } - - r[c++] = NULL; - -finish: - if (c < size) { - STRV_FOREACH(i, r) { - free(*i); - } - - free(r); - - return NULL; - } - - return r; -} - -char **strv_free(char **l) -{ - char **s = NULL; - - STRV_FOREACH(s, l) { - free(*s); - } - - free(l); - - return NULL; -} diff --git a/src/reproc.h b/src/reproc.h deleted file mode 100644 index c9bb65b2..00000000 --- a/src/reproc.h +++ /dev/null @@ -1,666 +0,0 @@ -/** - * @copyright Daan De Meyer - * @license MIT - * @link https://github.com/DaanDeMeyer/reproc Source code. - * @note Amalgamation by Jefferson Gonzalez - */ - -#pragma once - -#include -#include -#include -#include - -// -// export.h -// -#ifndef REPROC_EXPORT - #ifdef _WIN32 - #ifdef REPROC_SHARED - #ifdef REPROC_BUILDING - #define REPROC_EXPORT __declspec(dllexport) - #else - #define REPROC_EXPORT __declspec(dllimport) - #endif - #else - #define REPROC_EXPORT - #endif - #else - #ifdef REPROC_BUILDING - #define REPROC_EXPORT __attribute__((visibility("default"))) - #else - #define REPROC_EXPORT - #endif - #endif -#endif - -// -// reproc.h -// -#ifdef __cplusplus -extern "C" { -#endif - -/*! Used to store information about a child process. `reproc_t` is an opaque -type and can be allocated and released via `reproc_new` and `reproc_destroy` -respectively. */ -typedef struct reproc_t reproc_t; - -/*! reproc error naming follows POSIX errno naming prefixed with `REPROC`. */ - -/*! An invalid argument was passed to an API function */ -REPROC_EXPORT extern const int REPROC_EINVAL; -/*! A timeout value passed to an API function expired. */ -REPROC_EXPORT extern const int REPROC_ETIMEDOUT; -/*! The child process closed one of its streams (and in the case of -stdout/stderr all of the data remaining in that stream has been read). */ -REPROC_EXPORT extern const int REPROC_EPIPE; -/*! A memory allocation failed. */ -REPROC_EXPORT extern const int REPROC_ENOMEM; -/*! A call to `reproc_read` or `reproc_write` would have blocked. */ -REPROC_EXPORT extern const int REPROC_EWOULDBLOCK; - -/*! Signal exit status constants. */ - -REPROC_EXPORT extern const int REPROC_SIGKILL; -REPROC_EXPORT extern const int REPROC_SIGTERM; - -/*! Tells a function that takes a timeout value to wait indefinitely. */ -REPROC_EXPORT extern const int REPROC_INFINITE; -/*! Tells `reproc_wait` to wait until the deadline passed to `reproc_start` -expires. */ -REPROC_EXPORT extern const int REPROC_DEADLINE; - -/*! Stream identifiers used to indicate which stream to act on. */ -typedef enum { - /*! stdin */ - REPROC_STREAM_IN, - /*! stdout */ - REPROC_STREAM_OUT, - /*! stderr */ - REPROC_STREAM_ERR, -} REPROC_STREAM; - -/*! Used to tell reproc where to redirect the streams of the child process. */ -typedef enum { - /*! Use the default redirect behavior, see the documentation for `redirect` in - `reproc_options`. */ - REPROC_REDIRECT_DEFAULT, - /*! Redirect to a pipe. */ - REPROC_REDIRECT_PIPE, - /*! Redirect to the corresponding stream from the parent process. */ - REPROC_REDIRECT_PARENT, - /*! Redirect to /dev/null (or NUL on Windows). */ - REPROC_REDIRECT_DISCARD, - /*! Redirect to child process stdout. Only valid for stderr. */ - REPROC_REDIRECT_STDOUT, - /*! Redirect to a handle (fd on Linux, HANDLE/SOCKET on Windows). */ - REPROC_REDIRECT_HANDLE, - /*! Redirect to a `FILE *`. */ - REPROC_REDIRECT_FILE, - /*! Redirect to a specific path. */ - REPROC_REDIRECT_PATH, -} REPROC_REDIRECT; - -/*! Used to tell `reproc_stop` how to stop a child process. */ -typedef enum { - /*! noop (no operation) */ - REPROC_STOP_NOOP, - /*! `reproc_wait` */ - REPROC_STOP_WAIT, - /*! `reproc_terminate` */ - REPROC_STOP_TERMINATE, - /*! `reproc_kill` */ - REPROC_STOP_KILL, -} REPROC_STOP; - -typedef struct reproc_stop_action { - REPROC_STOP action; - int timeout; -} reproc_stop_action; - -typedef struct reproc_stop_actions { - reproc_stop_action first; - reproc_stop_action second; - reproc_stop_action third; -} reproc_stop_actions; - -// clang-format off - -#define REPROC_STOP_ACTIONS_NULL (reproc_stop_actions) { \ - { REPROC_STOP_NOOP, 0 }, \ - { REPROC_STOP_NOOP, 0 }, \ - { REPROC_STOP_NOOP, 0 }, \ -} - -// clang-format on - -#if defined(_WIN32) -typedef void *reproc_handle; // `HANDLE` -#else -typedef int reproc_handle; // fd -#endif - -typedef struct reproc_redirect { - /*! Type of redirection. */ - REPROC_REDIRECT type; - /*! - Redirect a stream to an operating system handle. The given handle must be in - blocking mode ( `O_NONBLOCK` and `OVERLAPPED` handles are not supported). - - Note that reproc does not take ownership of the handle. The user is - responsible for closing the handle after passing it to `reproc_start`. Since - the operating system will copy the handle to the child process, the handle - can be closed immediately after calling `reproc_start` if the handle is not - needed in the parent process anymore. - - If `handle` is set, `type` must be unset or set to `REPROC_REDIRECT_HANDLE` - and `file`, `path` must be unset. - */ - reproc_handle handle; - /*! - Redirect a stream to a file stream. - - Note that reproc does not take ownership of the file. The user is - responsible for closing the file after passing it to `reproc_start`. Just - like with `handles`, the operating system will copy the file handle to the - child process so the file can be closed immediately after calling - `reproc_start` if it isn't needed anymore by the parent process. - - Any file passed to `file.in` must have been opened in read mode. Likewise, - any files passed to `file.out` or `file.err` must have been opened in write - mode. - - If `file` is set, `type` must be unset or set to `REPROC_REDIRECT_FILE` and - `handle`, `path` must be unset. - */ - FILE *file; - /*! - Redirect a stream to a given path. - - reproc will create or open the file at the given path. Depending on the - stream, the file is opened in read or write mode. - - If `path` is set, `type` must be unset or set to `REPROC_REDIRECT_PATH` and - `handle`, `file` must be unset. - */ - const char *path; -} reproc_redirect; - -typedef enum { - REPROC_ENV_EXTEND, - REPROC_ENV_EMPTY, -} REPROC_ENV; - -typedef struct reproc_options { - /*! - `working_directory` specifies the working directory for the child process. If - `working_directory` is `NULL`, the child process runs in the working directory - of the parent process. - */ - const char *working_directory; - - struct { - /*! - `behavior` specifies whether the child process should start with a copy of - the parent process environment variables or an empty environment. By - default, the child process starts with a copy of the parent's environment - variables (`REPROC_ENV_EXTEND`). If `behavior` is set to `REPROC_ENV_EMPTY`, - the child process starts with an empty environment. - */ - REPROC_ENV behavior; - /*! - `extra` is an array of UTF-8 encoded, NUL-terminated strings that specifies - extra environment variables for the child process. It has the following - layout: - - - All elements except the final element must be of the format `NAME=VALUE`. - - The final element must be `NULL`. - - Example: ["IP=127.0.0.1", "PORT=8080", `NULL`] - - If `env` is `NULL`, no extra environment variables are added to the - environment of the child process. - */ - const char *const *extra; - } env; - /*! - `redirect` specifies where to redirect the streams from the child process. - - By default each stream is redirected to a pipe which can be written to (stdin) - or read from (stdout/stderr) using `reproc_write` and `reproc_read` - respectively. - */ - struct { - /*! - `in`, `out` and `err` specify where to redirect the standard I/O streams of - the child process. When not set, `in` and `out` default to - `REPROC_REDIRECT_PIPE` while `err` defaults to `REPROC_REDIRECT_PARENT`. - */ - reproc_redirect in; - reproc_redirect out; - reproc_redirect err; - /*! - Use `REPROC_REDIRECT_PARENT` instead of `REPROC_REDIRECT_PIPE` when `type` - is unset. - - When this option is set, `discard`, `file` and `path` must be unset. - */ - bool parent; - /*! - Use `REPROC_REDIRECT_DISCARD` instead of `REPROC_REDIRECT_PIPE` when `type` - is unset. - - When this option is set, `parent`, `file` and `path` must be unset. - */ - bool discard; - /*! - Shorthand for redirecting stdout and stderr to the same file. - - If this option is set, `out`, `err`, `parent`, `discard` and `path` must be - unset. - */ - FILE *file; - /*! - Shorthand for redirecting stdout and stderr to the same path. - - If this option is set, `out`, `err`, `parent`, `discard` and `file` must be - unset. - */ - const char *path; - } redirect; - /*! - Stop actions that are passed to `reproc_stop` in `reproc_destroy` to stop the - child process. See `reproc_stop` for more information on how `stop` is - interpreted. - */ - reproc_stop_actions stop; - /*! - Maximum allowed duration in milliseconds the process is allowed to run in - milliseconds. If the deadline is exceeded, Any ongoing and future calls to - `reproc_poll` return `REPROC_ETIMEDOUT`. - - Note that only `reproc_poll` takes the deadline into account. More - specifically, if the `nonblocking` option is not enabled, `reproc_read` and - `reproc_write` can deadlock waiting on the child process to perform I/O. If - this is a problem, enable the `nonblocking` option and use `reproc_poll` - together with a deadline/timeout to avoid any deadlocks. - - If `REPROC_DEADLINE` is passed as the timeout to `reproc_wait`, it waits until - the deadline expires. - - When `deadline` is zero, no deadline is set for the process. - */ - int deadline; - /*! - `input` is written to the stdin pipe before the child process is started. - - Because `input` is written to the stdin pipe before the process starts, - `input.size` must be smaller than the system's default pipe size (64KB). - - If `input` is set, the stdin pipe is closed after `input` is written to it. - - If `redirect.in` is set, this option may not be set. - */ - struct { - const uint8_t *data; - size_t size; - } input; - /*! - This option can only be used on POSIX systems. If enabled on Windows, an error - will be returned. - - If `fork` is enabled, `reproc_start` forks a child process and returns 0 in - the child process and > 0 in the parent process. In the child process, only - `reproc_destroy` may be called on the `reproc_t` instance to free its - associated memory. - - When `fork` is enabled. `argv` must be `NULL` when calling `reproc_start`. - */ - bool fork; - /*! - Put pipes created by reproc in nonblocking mode. This makes `reproc_read` and - `reproc_write` nonblocking operations. If needed, use `reproc_poll` to wait - until streams becomes readable/writable. - */ - bool nonblocking; -} reproc_options; - -enum { - /*! Data can be written to stdin. */ - REPROC_EVENT_IN = 1 << 0, - /*! Data can be read from stdout. */ - REPROC_EVENT_OUT = 1 << 1, - /*! Data can be read from stderr. */ - REPROC_EVENT_ERR = 1 << 2, - /*! The process finished running. */ - REPROC_EVENT_EXIT = 1 << 3, - /*! The deadline of the process expired. This event is added by default to the - list of interested events. */ - REPROC_EVENT_DEADLINE = 1 << 4, -}; - -typedef struct reproc_event_source { - /*! Process to poll for events. */ - reproc_t *process; - /*! Events of the process that we're interested in. Takes a combo of - `REPROC_EVENT` flags. */ - int interests; - /*! Combo of `REPROC_EVENT` flags that indicate the events that occurred. This - field is filled in by `reproc_poll`. */ - int events; -} reproc_event_source; - -/*! Allocate a new `reproc_t` instance on the heap. */ -REPROC_EXPORT reproc_t *reproc_new(void); - -/*! -Starts the process specified by `argv` in the given working directory and -redirects its input, output and error streams. - -If this function does not return an error the child process will have started -running and can be inspected using the operating system's tools for process -inspection (e.g. ps on Linux). - -Every successful call to this function should be followed by a successful call -to `reproc_wait` or `reproc_stop` and a call to `reproc_destroy`. If an error -occurs during `reproc_start` all allocated resources are cleaned up before -`reproc_start` returns and no further action is required. - -`argv` is an array of UTF-8 encoded, NUL-terminated strings that specifies the -program to execute along with its arguments. It has the following layout: - -- The first element indicates the executable to run as a child process. This can -be an absolute path, a path relative to the working directory of the parent -process or the name of an executable located in the PATH. It cannot be `NULL`. -- The following elements indicate the whitespace delimited arguments passed to -the executable. None of these elements can be `NULL`. -- The final element must be `NULL`. - -Example: ["cmake", "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release", `NULL`] -*/ -REPROC_EXPORT int reproc_start(reproc_t *process, - const char *const *argv, - reproc_options options); - -/*! -Returns the process ID of the child or `REPROC_EINVAL` on error. - -Note that if `reproc_wait` has been called successfully on this process already, -the returned pid will be that of the just ended child process. The operating -system will have cleaned up the resources allocated to the process -and the operating system is free to reuse the same pid for another process. - -Generally, only pass the result of this function to system calls that need a -valid pid if `reproc_wait` hasn't been called successfully on the process yet. -*/ -REPROC_EXPORT int reproc_pid(reproc_t *process); - -/*! -Polls each process in `sources` for its corresponding events in `interests` and -stores events that occurred for each process in `events`. If an event source -process member is `NULL`, the event source is ignored. - -Pass `REPROC_INFINITE` to `timeout` to have `reproc_poll` wait forever for an -event to occur. - -If one or more events occur, returns the number of processes with events. If the -timeout expires, returns zero. Returns `REPROC_EPIPE` if none of the sources -have valid pipes remaining that can be polled. - -Actionable errors: -- `REPROC_EPIPE` -*/ -REPROC_EXPORT int -reproc_poll(reproc_event_source *sources, size_t num_sources, int timeout); - -/*! -Reads up to `size` bytes into `buffer` from the child process output stream -indicated by `stream`. - -Actionable errors: -- `REPROC_EPIPE` -- `REPROC_EWOULDBLOCK` -*/ -REPROC_EXPORT int reproc_read(reproc_t *process, - REPROC_STREAM stream, - uint8_t *buffer, - size_t size); - -/*! -Writes up to `size` bytes from `buffer` to the standard input (stdin) of the -child process. - -(POSIX) By default, writing to a closed stdin pipe terminates the parent process -with the `SIGPIPE` signal. `reproc_write` will only return `REPROC_EPIPE` if -this signal is ignored by the parent process. - -Returns the amount of bytes written. If `buffer` is `NULL` and `size` is zero, -this function returns 0. - -If the standard input of the child process wasn't opened with -`REPROC_REDIRECT_PIPE`, this function returns `REPROC_EPIPE` unless `buffer` is -`NULL` and `size` is zero. - -Actionable errors: -- `REPROC_EPIPE` -- `REPROC_EWOULDBLOCK` -*/ -REPROC_EXPORT int -reproc_write(reproc_t *process, const uint8_t *buffer, size_t size); - -/*! -Closes the child process standard stream indicated by `stream`. - -This function is necessary when a child process reads from stdin until it is -closed. After writing all the input to the child process using `reproc_write`, -the standard input stream can be closed using this function. -*/ -REPROC_EXPORT int reproc_close(reproc_t *process, REPROC_STREAM stream); - -/*! -Waits `timeout` milliseconds for the child process to exit. If the child process -has already exited or exits within the given timeout, its exit status is -returned. - -If `timeout` is 0, the function will only check if the child process is still -running without waiting. If `timeout` is `REPROC_INFINITE`, this function will -wait indefinitely for the child process to exit. If `timeout` is -`REPROC_DEADLINE`, this function waits until the deadline passed to -`reproc_start` expires. - -Actionable errors: -- `REPROC_ETIMEDOUT` -*/ -REPROC_EXPORT int reproc_wait(reproc_t *process, int timeout); - -/*! -Sends the `SIGTERM` signal (POSIX) or the `CTRL-BREAK` signal (Windows) to the -child process. Remember that successful calls to `reproc_wait` and -`reproc_destroy` are required to make sure the child process is completely -cleaned up. -*/ -REPROC_EXPORT int reproc_terminate(reproc_t *process); - -/*! -Sends the `SIGKILL` signal to the child process (POSIX) or calls -`TerminateProcess` (Windows) on the child process. Remember that successful -calls to `reproc_wait` and `reproc_destroy` are required to make sure the child -process is completely cleaned up. -*/ -REPROC_EXPORT int reproc_kill(reproc_t *process); - -/*! -Simplifies calling combinations of `reproc_wait`, `reproc_terminate` and -`reproc_kill`. The function executes each specified step and waits (using -`reproc_wait`) until the corresponding timeout expires before continuing with -the next step. - -Example: - -Wait 10 seconds for the child process to exit on its own before sending -`SIGTERM` (POSIX) or `CTRL-BREAK` (Windows) and waiting five more seconds for -the child process to exit. - -```c -REPROC_ERROR error = reproc_stop(process, - REPROC_STOP_WAIT, 10000, - REPROC_STOP_TERMINATE, 5000, - REPROC_STOP_NOOP, 0); -``` - -Call `reproc_wait`, `reproc_terminate` and `reproc_kill` directly if you need -extra logic such as logging between calls. - -`stop` can contain up to three stop actions that instruct this function how the -child process should be stopped. The first element of each stop action specifies -which action should be called on the child process. The second element of each -stop actions specifies how long to wait after executing the operation indicated -by the first element. - -When `stop` is 3x `REPROC_STOP_NOOP`, `reproc_destroy` will wait until the -deadline expires (or forever if there is no deadline). If the process is still -running after the deadline expires, `reproc_stop` then calls `reproc_terminate` -and waits forever for the process to exit. - -Note that when a stop action specifies `REPROC_STOP_WAIT`, the function will -just wait for the specified timeout instead of performing an action to stop the -child process. - -If the child process has already exited or exits during the execution of this -function, its exit status is returned. - -Actionable errors: -- `REPROC_ETIMEDOUT` -*/ -REPROC_EXPORT int reproc_stop(reproc_t *process, reproc_stop_actions stop); - -/*! -Release all resources associated with `process` including the memory allocated -by `reproc_new`. Calling this function before a succesfull call to `reproc_wait` -can result in resource leaks. - -Does nothing if `process` is an invalid `reproc_t` instance and always returns -an invalid `reproc_t` instance (`NULL`). By assigning the result of -`reproc_destroy` to the instance being destroyed, it can be safely called -multiple times on the same instance. - -Example: `process = reproc_destroy(process)`. -*/ -REPROC_EXPORT reproc_t *reproc_destroy(reproc_t *process); - -/*! -Returns a string describing `error`. This string must not be modified by the -caller. -*/ -REPROC_EXPORT const char *reproc_strerror(int error); - -#ifdef __cplusplus -} -#endif - -// -// drain.h -// -#ifdef __cplusplus -extern "C" { -#endif - -/*! Used by `reproc_drain` to provide data to the caller. Each time data is -read, `function` is called with `context`. If a sink returns a non-zero value, -`reproc_drain` will return immediately with the same value. */ -typedef struct reproc_sink { - int (*function)(REPROC_STREAM stream, - const uint8_t *buffer, - size_t size, - void *context); - void *context; -} reproc_sink; - -/*! Pass `REPROC_SINK_NULL` as the sink for output streams that have not been -redirected to a pipe. */ -REPROC_EXPORT extern const reproc_sink REPROC_SINK_NULL; - -/*! -Reads from the child process stdout and stderr until an error occurs or both -streams are closed. The `out` and `err` sinks receive the output from stdout and -stderr respectively. The same sink may be passed to both `out` and `err`. - -`reproc_drain` always starts by calling both sinks once with an empty buffer and -`stream` set to `REPROC_STREAM_IN` to give each sink the chance to process all -output from the previous call to `reproc_drain` one by one. - -When a stream is closed, its corresponding `sink` is called once with `size` set -to zero. - -Note that his function returns 0 instead of `REPROC_EPIPE` when both output -streams of the child process are closed. - -Actionable errors: -- `REPROC_ETIMEDOUT` -*/ -REPROC_EXPORT int -reproc_drain(reproc_t *process, reproc_sink out, reproc_sink err); - -/*! -Appends the output of a process (stdout and stderr) to the value of `output`. -`output` must point to either `NULL` or a NUL-terminated string. - -Calls `realloc` as necessary to make space in `output` to store the output of -the child process. Make sure to always call `reproc_free` on the value of -`output` after calling `reproc_drain` (even if it fails). - -Because the resulting sink does not store the output size, `strlen` is called -each time data is read to calculate the current size of the output. This might -cause performance problems when draining processes that produce a lot of output. - -Similarly, this sink will not work on processes that have NUL terminators in -their output because `strlen` is used to calculate the current output size. - -Returns `REPROC_ENOMEM` if a call to `realloc` fails. `output` will contain any -output read from the child process, preceeded by whatever was stored in it at -the moment its corresponding sink was passed to `reproc_drain`. - -The `drain` example shows how to use `reproc_sink_string`. -``` -*/ -REPROC_EXPORT reproc_sink reproc_sink_string(char **output); - -/*! Discards the output of a process. */ -REPROC_EXPORT reproc_sink reproc_sink_discard(void); - -/*! Calls `free` on `ptr` and returns `NULL`. Use this function to free memory -allocated by `reproc_sink_string`. This avoids issues with allocating across -module (DLL) boundaries on Windows. */ -REPROC_EXPORT void *reproc_free(void *ptr); - -#ifdef __cplusplus -} -#endif - -// -// run.h -// -#ifdef __cplusplus -extern "C" { -#endif - -/*! Sets `options.redirect.parent = true` unless `discard` is set and calls -`reproc_run_ex` with `REPROC_SINK_NULL` for the `out` and `err` sinks. */ -REPROC_EXPORT int reproc_run(const char *const *argv, reproc_options options); - -/*! -Wrapper function that starts a process with the given arguments, drain its -output and waits until it exits. Have a look at its (trivial) implementation and -the documentation of the functions it calls to see exactly what it does: -https://github.com/DaanDeMeyer/reproc/blob/master/reproc/src/run.c -*/ -REPROC_EXPORT int reproc_run_ex(const char *const *argv, - reproc_options options, - reproc_sink out, - reproc_sink err); - -#ifdef __cplusplus -} -#endif diff --git a/src/reproc.posix.c b/src/reproc.posix.c deleted file mode 100644 index a5a3def9..00000000 --- a/src/reproc.posix.c +++ /dev/null @@ -1,797 +0,0 @@ -/** - * @copyright Daan De Meyer - * @license MIT - * @link https://github.com/DaanDeMeyer/reproc Source code. - * @note Amalgamation by Jefferson Gonzalez - */ - -#define _POSIX_C_SOURCE 200809L - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "reproc_private.h" - -// -// clocl.posix.c -// -int64_t now(void) -{ - struct timespec timespec = { 0 }; - - int r = clock_gettime(CLOCK_REALTIME, ×pec); - ASSERT_UNUSED(r == 0); - - return timespec.tv_sec * 1000 + timespec.tv_nsec / 1000000; -} - -// -// error.posix.c -// -const int REPROC_EINVAL = -EINVAL; -const int REPROC_EPIPE = -EPIPE; -const int REPROC_ETIMEDOUT = -ETIMEDOUT; -const int REPROC_ENOMEM = -ENOMEM; -const int REPROC_EWOULDBLOCK = -EWOULDBLOCK; - -enum { ERROR_STRING_MAX_SIZE = 512 }; - -const char *error_string(int error) -{ - static THREAD_LOCAL char string[ERROR_STRING_MAX_SIZE]; - - int r = strerror_r(abs(error), string, ARRAY_SIZE(string)); - if (r != 0) { - return "Failed to retrieve error string"; - } - - return string; -} - -// -// handle.posix.c -// -const int HANDLE_INVALID = -1; - -int handle_cloexec(int handle, bool enable) -{ - int r = -1; - - r = fcntl(handle, F_GETFD, 0); - if (r < 0) { - return -errno; - } - - r = enable ? r | FD_CLOEXEC : r & ~FD_CLOEXEC; - - r = fcntl(handle, F_SETFD, r); - if (r < 0) { - return -errno; - } - - return 0; -} - -int handle_destroy(int handle) -{ - if (handle == HANDLE_INVALID) { - return HANDLE_INVALID; - } - - int r = close(handle); - ASSERT_UNUSED(r == 0); - - return HANDLE_INVALID; -} - -// -// init.posix.c -// -int init(void) -{ - return 0; -} - -void deinit(void) {} - -// -// pipe.posix.c -// -const int PIPE_INVALID = -1; - -const short PIPE_EVENT_IN = POLLIN; -const short PIPE_EVENT_OUT = POLLOUT; - -int pipe_init(int *read, int *write) -{ - ASSERT(read); - ASSERT(write); - - int pair[] = { PIPE_INVALID, PIPE_INVALID }; - int r = -1; - - r = pipe(pair); - if (r < 0) { - r = -errno; - goto finish; - } - - r = handle_cloexec(pair[0], true); - if (r < 0) { - goto finish; - } - - r = handle_cloexec(pair[1], true); - if (r < 0) { - goto finish; - } - - *read = pair[0]; - *write = pair[1]; - - pair[0] = PIPE_INVALID; - pair[1] = PIPE_INVALID; - -finish: - pipe_destroy(pair[0]); - pipe_destroy(pair[1]); - - return r; -} - -int pipe_nonblocking(int pipe, bool enable) -{ - int r = -1; - - r = fcntl(pipe, F_GETFL, 0); - if (r < 0) { - return -errno; - } - - r = enable ? r | O_NONBLOCK : r & ~O_NONBLOCK; - - r = fcntl(pipe, F_SETFL, r); - - return r < 0 ? -errno : 0; -} - -int pipe_read(int pipe, uint8_t *buffer, size_t size) -{ - ASSERT(pipe != PIPE_INVALID); - ASSERT(buffer); - - int r = (int) read(pipe, buffer, size); - - if (r == 0) { - // `read` returns 0 to indicate the other end of the pipe was closed. - return -EPIPE; - } - - return r < 0 ? -errno : r; -} - -int pipe_write(int pipe, const uint8_t *buffer, size_t size) -{ - ASSERT(pipe != PIPE_INVALID); - ASSERT(buffer); - - int r = (int) write(pipe, buffer, size); - - return r < 0 ? -errno : r; -} - -int pipe_poll(pipe_event_source *sources, size_t num_sources, int timeout) -{ - ASSERT(num_sources <= INT_MAX); - - struct pollfd *pollfds = NULL; - int r = -1; - - pollfds = calloc(num_sources, sizeof(struct pollfd)); - if (pollfds == NULL) { - r = -errno; - goto finish; - } - - for (size_t i = 0; i < num_sources; i++) { - pollfds[i].fd = sources[i].pipe; - pollfds[i].events = sources[i].interests; - } - - r = poll(pollfds, (nfds_t) num_sources, timeout); - if (r < 0) { - r = -errno; - goto finish; - } - - for (size_t i = 0; i < num_sources; i++) { - sources[i].events = pollfds[i].revents; - } - -finish: - free(pollfds); - - return r; -} - -int pipe_shutdown(int pipe) -{ - (void) pipe; - return 0; -} - -int pipe_destroy(int pipe) -{ - return handle_destroy(pipe); -} - -// -// process.posix.c -// -const pid_t PROCESS_INVALID = -1; - -static int signal_mask(int how, const sigset_t *newmask, sigset_t *oldmask) -{ - int r = -1; - -#if defined(REPROC_MULTITHREADED) - // `pthread_sigmask` returns positive errno values so we negate them. - r = -pthread_sigmask(how, newmask, oldmask); -#else - r = sigprocmask(how, newmask, oldmask); - r = r < 0 ? -errno : 0; -#endif - - return r; -} - -// Returns true if the NUL-terminated string indicated by `path` is a relative -// path. A path is relative if any character except the first is a forward slash -// ('/'). -static bool path_is_relative(const char *path) -{ - return strlen(path) > 0 && path[0] != '/' && strchr(path + 1, '/') != NULL; -} - -// Prepends the NUL-terminated string indicated by `path` with the current -// working directory. The caller is responsible for freeing the result of this -// function. If an error occurs, `NULL` is returned and `errno` is set to -// indicate the error. -static char *path_prepend_cwd(const char *path) -{ - ASSERT(path); - - size_t path_size = strlen(path); - size_t cwd_size = PATH_MAX; - - // We always allocate sufficient space for `path` but do not include this - // space in `cwd_size` so we can be sure that when `getcwd` succeeds there is - // sufficient space left in `cwd` to append `path`. - - // +2 reserves space to add a NUL terminator and potentially a missing '/' - // after the current working directory. - char *cwd = calloc(cwd_size + path_size + 2, sizeof(char)); - if (cwd == NULL) { - return cwd; - } - - while (getcwd(cwd, cwd_size) == NULL) { - if (errno != ERANGE) { - free(cwd); - return NULL; - } - - cwd_size += PATH_MAX; - - char *result = realloc(cwd, cwd_size + path_size + 1); - if (result == NULL) { - free(cwd); - return result; - } - - cwd = result; - } - - cwd_size = strlen(cwd); - - // Add a forward slash after `cwd` if there is none. - if (cwd[cwd_size - 1] != '/') { - cwd[cwd_size] = '/'; - cwd[cwd_size + 1] = '\0'; - cwd_size++; - } - - // We've made sure there's sufficient space left in `cwd` to add `path` and a - // NUL terminator. - memcpy(cwd + cwd_size, path, path_size); - cwd[cwd_size + path_size] = '\0'; - - return cwd; -} - -static const int MAX_FD_LIMIT = 1024 * 1024; - -static int get_max_fd(void) -{ - struct rlimit limit = { 0 }; - - int r = getrlimit(RLIMIT_NOFILE, &limit); - if (r < 0) { - return -errno; - } - - rlim_t soft = limit.rlim_cur; - - if (soft == RLIM_INFINITY || soft > INT_MAX) { - return INT_MAX; - } - - return (int) (soft - 1); -} - -static bool fd_in_set(int fd, const int *fd_set, size_t size) -{ - for (size_t i = 0; i < size; i++) { - if (fd == fd_set[i]) { - return true; - } - } - - return false; -} - -static pid_t process_fork(const int *except, size_t num_except) -{ - struct { - sigset_t old; - sigset_t new; - } mask; - - int r = -1; - - // We don't want signal handlers of the parent to run in the child process so - // we block all signals before forking. - - r = sigfillset(&mask.new); - if (r < 0) { - return -errno; - } - - r = signal_mask(SIG_SETMASK, &mask.new, &mask.old); - if (r < 0) { - return r; - } - - struct { - int read; - int write; - } pipe = { PIPE_INVALID, PIPE_INVALID }; - - r = pipe_init(&pipe.read, &pipe.write); - if (r < 0) { - return r; - } - - r = fork(); - if (r < 0) { - // `fork` error. - - r = -errno; // Save `errno`. - - int q = signal_mask(SIG_SETMASK, &mask.new, &mask.old); - ASSERT_UNUSED(q == 0); - - pipe_destroy(pipe.read); - pipe_destroy(pipe.write); - - return r; - } - - if (r > 0) { - // Parent process - - pid_t child = r; - - // From now on, the child process might have started so we don't report - // errors from `signal_mask` and `read`. This puts the responsibility - // for cleaning up the process in the hands of the caller. - - int q = signal_mask(SIG_SETMASK, &mask.old, &mask.old); - ASSERT_UNUSED(q == 0); - - // Close the error pipe write end on the parent's side so `read` will return - // when it is closed on the child side as well. - pipe_destroy(pipe.write); - - int child_errno = 0; - q = (int) read(pipe.read, &child_errno, sizeof(child_errno)); - ASSERT_UNUSED(q >= 0); - - if (child_errno > 0) { - // If the child writes to the error pipe and exits, we're certain the - // child process exited on its own and we can report errors as usual. - r = waitpid(child, NULL, 0); - ASSERT(r < 0 || r == child); - - r = r < 0 ? -errno : -child_errno; - } - - pipe_destroy(pipe.read); - - return r < 0 ? r : child; - } - - // Child process - - // Reset all signal handlers so they don't run in the child process. By - // default, a child process inherits the parent's signal handlers but we - // override this as most signal handlers won't be written in a way that they - // can deal with being run in a child process. - - struct sigaction action = { .sa_handler = SIG_DFL }; - - r = sigemptyset(&action.sa_mask); - if (r < 0) { - r = -errno; - goto finish; - } - - // NSIG is not standardized so we use a fixed limit instead. - for (int signal = 0; signal < 32; signal++) { - r = sigaction(signal, &action, NULL); - if (r < 0 && errno != EINVAL) { - r = -errno; - goto finish; - } - } - - // Reset the child's signal mask to the default signal mask. By default, a - // child process inherits the parent's signal mask (even over an `exec` call) - // but we override this as most processes won't be written in a way that they - // can deal with starting with a custom signal mask. - - r = sigemptyset(&mask.new); - if (r < 0) { - r = -errno; - goto finish; - } - - r = signal_mask(SIG_SETMASK, &mask.new, NULL); - if (r < 0) { - goto finish; - } - - // Not all file descriptors might have been created with the `FD_CLOEXEC` - // flag so we manually close all file descriptors to prevent file descriptors - // leaking into the child process. - - r = get_max_fd(); - if (r < 0) { - goto finish; - } - - int max_fd = r; - - if (max_fd > MAX_FD_LIMIT) { - // Refuse to try to close too many file descriptors. - r = -EMFILE; - goto finish; - } - - for (int i = 0; i < max_fd; i++) { - // Make sure we don't close the error pipe file descriptors twice. - if (i == pipe.read || i == pipe.write) { - continue; - } - - if (fd_in_set(i, except, num_except)) { - continue; - } - - // Check if `i` is a valid file descriptor before trying to close it. - r = fcntl(i, F_GETFD); - if (r >= 0) { - handle_destroy(i); - } - } - - r = 0; - -finish: - if (r < 0) { - (void) !write(pipe.write, &errno, sizeof(errno)); - _exit(EXIT_FAILURE); - } - - pipe_destroy(pipe.write); - pipe_destroy(pipe.read); - - return 0; -} - -int process_start(pid_t *process, - const char *const *argv, - struct process_options options) -{ - ASSERT(process); - - if (argv != NULL) { - ASSERT(argv[0] != NULL); - } - - struct { - int read; - int write; - } pipe = { PIPE_INVALID, PIPE_INVALID }; - char *program = NULL; - char **env = NULL; - int r = -1; - - // We create an error pipe to receive errors from the child process. - r = pipe_init(&pipe.read, &pipe.write); - if (r < 0) { - goto finish; - } - - if (argv != NULL) { - // We prepend the parent working directory to `program` if it is a - // relative path so that it will always be searched for relative to the - // parent working directory even after executing `chdir`. - program = options.working_directory && path_is_relative(argv[0]) - ? path_prepend_cwd(argv[0]) - : strdup(argv[0]); - if (program == NULL) { - r = -errno; - goto finish; - } - } - - extern char **environ; // NOLINT - char *const *parent = options.env.behavior == REPROC_ENV_EMPTY ? NULL - : environ; - env = strv_concat(parent, options.env.extra); - if (env == NULL) { - goto finish; - } - - int except[] = { options.handle.in, options.handle.out, options.handle.err, - pipe.read, pipe.write, options.handle.exit }; - - r = process_fork(except, ARRAY_SIZE(except)); - if (r < 0) { - goto finish; - } - - if (r == 0) { - // Redirect stdin, stdout and stderr. - - int redirect[] = { options.handle.in, options.handle.out, - options.handle.err }; - - for (int i = 0; i < (int) ARRAY_SIZE(redirect); i++) { - // `i` corresponds to the standard stream we need to redirect. - r = dup2(redirect[i], i); - if (r < 0) { - r = -errno; - goto child; - } - - // Make sure we don't accidentally cloexec the standard streams of the - // child process when we're inheriting the parent standard streams. If we - // don't call `exec`, the caller is responsible for closing the redirect - // and exit handles. - if (redirect[i] != i) { - // Make sure the pipe is closed when we call exec. - r = handle_cloexec(redirect[i], true); - if (r < 0) { - goto child; - } - } - } - - // Make sure the `exit` file descriptor is inherited. - - r = handle_cloexec(options.handle.exit, false); - if (r < 0) { - goto child; - } - - if (options.working_directory != NULL) { - r = chdir(options.working_directory); - if (r < 0) { - r = -errno; - goto child; - } - } - - // `environ` is carried over calls to `exec`. - environ = env; - - if (argv != NULL) { - ASSERT(program); - - r = execvp(program, (char *const *) argv); - if (r < 0) { - r = -errno; - goto child; - } - } - - env = NULL; - - child: - if (r < 0) { - (void) !write(pipe.write, &errno, sizeof(errno)); - _exit(EXIT_FAILURE); - } - - pipe_destroy(pipe.read); - pipe_destroy(pipe.write); - free(program); - strv_free(env); - - return 0; - } - - pid_t child = r; - - // Close the error pipe write end on the parent's side so `read` will return - // when it is closed on the child side as well. - pipe.write = pipe_destroy(pipe.write); - - int child_errno = 0; - r = (int) read(pipe.read, &child_errno, sizeof(child_errno)); - ASSERT_UNUSED(r >= 0); - - if (child_errno > 0) { - r = waitpid(child, NULL, 0); - r = r < 0 ? -errno : -child_errno; - goto finish; - } - - *process = child; - r = 0; - -finish: - pipe_destroy(pipe.read); - pipe_destroy(pipe.write); - free(program); - strv_free(env); - - return r < 0 ? r : 1; -} - -static int parse_status(int status) -{ - return WIFEXITED(status) ? WEXITSTATUS(status) : WTERMSIG(status) + 128; -} - -int process_pid(process_type process) -{ - return process; -} - -int process_wait(pid_t process) -{ - ASSERT(process != PROCESS_INVALID); - - int status = 0; - int r = waitpid(process, &status, 0); - if (r < 0) { - return -errno; - } - - ASSERT(r == process); - - return parse_status(status); -} - -int process_terminate(pid_t process) -{ - ASSERT(process != PROCESS_INVALID); - - int r = kill(process, SIGTERM); - return r < 0 ? -errno : 0; -} - -int process_kill(pid_t process) -{ - ASSERT(process != PROCESS_INVALID); - - int r = kill(process, SIGKILL); - return r < 0 ? -errno : 0; -} - -pid_t process_destroy(pid_t process) -{ - // `waitpid` already cleans up the process for us. - (void) process; - return PROCESS_INVALID; -} - -// -// redirect.posix.c -// -static FILE *stream_to_file(REPROC_STREAM stream) -{ - switch (stream) { - case REPROC_STREAM_IN: - return stdin; - case REPROC_STREAM_OUT: - return stdout; - case REPROC_STREAM_ERR: - return stderr; - } - - return NULL; -} - -int redirect_parent(int *child, REPROC_STREAM stream) -{ - ASSERT(child); - - FILE *file = stream_to_file(stream); - if (file == NULL) { - return -EINVAL; - } - - int r = fileno(file); - if (r < 0) { - return errno == EBADF ? -EPIPE : -errno; - } - - *child = r; // `r` contains the duplicated file descriptor. - - return 0; -} - -int redirect_discard(int *child, REPROC_STREAM stream) -{ - return redirect_path(child, stream, "/dev/null"); -} - -int redirect_file(int *child, FILE *file) -{ - ASSERT(child); - - int r = fileno(file); - if (r < 0) { - return -errno; - } - - *child = r; - - return 0; -} - -int redirect_path(int *child, REPROC_STREAM stream, const char *path) -{ - ASSERT(child); - ASSERT(path); - - int mode = stream == REPROC_STREAM_IN ? O_RDONLY : O_WRONLY; - - int r = open(path, mode | O_CREAT | O_CLOEXEC, 0640); - if (r < 0) { - return -errno; - } - - *child = r; - - return 0; -} - -// -// utf.posix.c -// - -// `utf16_from_utf8` is Windows-only. diff --git a/src/reproc.windows.c b/src/reproc.windows.c deleted file mode 100644 index 00c37e83..00000000 --- a/src/reproc.windows.c +++ /dev/null @@ -1,1001 +0,0 @@ -/** - * @copyright Daan De Meyer - * @license MIT - * @link https://github.com/DaanDeMeyer/reproc Source code. - * @note Amalgamation by Jefferson Gonzalez - */ - -#define _WIN32_WINNT _WIN32_WINNT_VISTA - -#include -#include -#include -#include -#include -#include - -#include "reproc_private.h" - -// -// clock.windows.c -// -int64_t now(void) -{ - return (int64_t) GetTickCount64(); -} - -// -// error.windows.c -// -const int REPROC_EINVAL = -ERROR_INVALID_PARAMETER; -const int REPROC_EPIPE = -ERROR_BROKEN_PIPE; -const int REPROC_ETIMEDOUT = -WAIT_TIMEOUT; -const int REPROC_ENOMEM = -ERROR_NOT_ENOUGH_MEMORY; -const int REPROC_EWOULDBLOCK = -WSAEWOULDBLOCK; - -enum { ERROR_STRING_MAX_SIZE = 512 }; - -const char *error_string(int error) -{ - wchar_t *wstring = NULL; - int r = -1; - - wstring = malloc(sizeof(wchar_t) * ERROR_STRING_MAX_SIZE); - if (wstring == NULL) { - return "Failed to allocate memory for error string"; - } - - // We don't expect message sizes larger than the maximum possible int. - r = (int) FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, (DWORD) abs(error), - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), wstring, - ERROR_STRING_MAX_SIZE, NULL); - if (r == 0) { - free(wstring); - return "Failed to retrieve error string"; - } - - static THREAD_LOCAL char string[ERROR_STRING_MAX_SIZE]; - - r = WideCharToMultiByte(CP_UTF8, 0, wstring, -1, string, ARRAY_SIZE(string), - NULL, NULL); - free(wstring); - if (r == 0) { - return "Failed to convert error string to UTF-8"; - } - - // Remove trailing whitespace and period. - if (r >= 4) { - string[r - 4] = '\0'; - } - - return string; -} - -// -// handle.windows.c -// -const HANDLE HANDLE_INVALID = INVALID_HANDLE_VALUE; // NOLINT - -// `handle_cloexec` is POSIX-only. - -HANDLE handle_destroy(HANDLE handle) -{ - if (handle == NULL || handle == HANDLE_INVALID) { - return HANDLE_INVALID; - } - - int r = CloseHandle(handle); - ASSERT_UNUSED(r != 0); - - return HANDLE_INVALID; -} - -// -// init.windows.c -// -int init(void) -{ - WSADATA data; - int r = WSAStartup(MAKEWORD(2, 2), &data); - return -r; -} - -void deinit(void) -{ - int saved = WSAGetLastError(); - - int r = WSACleanup(); - ASSERT_UNUSED(r == 0); - - WSASetLastError(saved); -} - -// -// pipe.windows.c -// -const SOCKET PIPE_INVALID = INVALID_SOCKET; - -const short PIPE_EVENT_IN = POLLIN; -const short PIPE_EVENT_OUT = POLLOUT; - -// Inspired by https://gist.github.com/geertj/4325783. -static int socketpair(int domain, int type, int protocol, SOCKET *out) -{ - ASSERT(out); - - SOCKET server = PIPE_INVALID; - SOCKET pair[] = { PIPE_INVALID, PIPE_INVALID }; - int r = -1; - - server = WSASocketW(AF_INET, SOCK_STREAM, 0, NULL, 0, 0); - if (server == INVALID_SOCKET) { - r = -WSAGetLastError(); - goto finish; - } - - SOCKADDR_IN localhost = { 0 }; - localhost.sin_family = AF_INET; - localhost.sin_addr.S_un.S_addr = htonl(INADDR_LOOPBACK); - localhost.sin_port = 0; - - r = bind(server, (SOCKADDR *) &localhost, sizeof(localhost)); - if (r < 0) { - r = -WSAGetLastError(); - goto finish; - } - - r = listen(server, 1); - if (r < 0) { - r = -WSAGetLastError(); - goto finish; - } - - SOCKADDR_STORAGE name = { 0 }; - int size = sizeof(name); - r = getsockname(server, (SOCKADDR *) &name, &size); - if (r < 0) { - r = -WSAGetLastError(); - goto finish; - } - - pair[0] = WSASocketW(domain, type, protocol, NULL, 0, 0); - if (pair[0] == INVALID_SOCKET) { - r = -WSAGetLastError(); - goto finish; - } - - struct { - WSAPROTOCOL_INFOW data; - int size; - } info = { { 0 }, sizeof(WSAPROTOCOL_INFOW) }; - - r = getsockopt(pair[0], SOL_SOCKET, SO_PROTOCOL_INFOW, (char *) &info.data, - &info.size); - if (r < 0) { - goto finish; - } - - // We require the returned sockets to be usable as Windows file handles. This - // might not be the case if extra LSP providers are installed. - - if (!(info.data.dwServiceFlags1 & XP1_IFS_HANDLES)) { - r = -ERROR_NOT_SUPPORTED; - goto finish; - } - - r = pipe_nonblocking(pair[0], true); - if (r < 0) { - goto finish; - } - - r = connect(pair[0], (SOCKADDR *) &name, size); - if (r < 0 && WSAGetLastError() != WSAEWOULDBLOCK) { - r = -WSAGetLastError(); - goto finish; - } - - r = pipe_nonblocking(pair[0], false); - if (r < 0) { - goto finish; - } - - pair[1] = accept(server, NULL, NULL); - if (pair[1] == INVALID_SOCKET) { - r = -WSAGetLastError(); - goto finish; - } - - out[0] = pair[0]; - out[1] = pair[1]; - - pair[0] = PIPE_INVALID; - pair[1] = PIPE_INVALID; - -finish: - pipe_destroy(server); - pipe_destroy(pair[0]); - pipe_destroy(pair[1]); - - return r; -} - -int pipe_init(SOCKET *read, SOCKET *write) -{ - ASSERT(read); - ASSERT(write); - - SOCKET pair[] = { PIPE_INVALID, PIPE_INVALID }; - int r = -1; - - // Use sockets instead of pipes so we can use `WSAPoll` which only works with - // sockets. - r = socketpair(AF_INET, SOCK_STREAM, 0, pair); - if (r < 0) { - goto finish; - } - - r = SetHandleInformation((HANDLE) pair[0], HANDLE_FLAG_INHERIT, 0); - if (r == 0) { - r = -(int) GetLastError(); - goto finish; - } - - r = SetHandleInformation((HANDLE) pair[1], HANDLE_FLAG_INHERIT, 0); - if (r == 0) { - r = -(int) GetLastError(); - goto finish; - } - - // Make the connection unidirectional to better emulate a pipe. - - r = shutdown(pair[0], SD_SEND); - if (r < 0) { - r = -WSAGetLastError(); - goto finish; - } - - r = shutdown(pair[1], SD_RECEIVE); - if (r < 0) { - r = -WSAGetLastError(); - goto finish; - } - - *read = pair[0]; - *write = pair[1]; - - pair[0] = PIPE_INVALID; - pair[1] = PIPE_INVALID; - -finish: - pipe_destroy(pair[0]); - pipe_destroy(pair[1]); - - return r; -} - -int pipe_nonblocking(SOCKET pipe, bool enable) -{ - u_long mode = enable; - int r = ioctlsocket(pipe, (long) FIONBIO, &mode); - return r < 0 ? -WSAGetLastError() : 0; -} - -int pipe_read(SOCKET pipe, uint8_t *buffer, size_t size) -{ - ASSERT(pipe != PIPE_INVALID); - ASSERT(buffer); - ASSERT(size <= INT_MAX); - - int r = recv(pipe, (char *) buffer, (int) size, 0); - - if (r == 0) { - return -ERROR_BROKEN_PIPE; - } - - return r < 0 ? -WSAGetLastError() : r; -} - -int pipe_write(SOCKET pipe, const uint8_t *buffer, size_t size) -{ - ASSERT(pipe != PIPE_INVALID); - ASSERT(buffer); - ASSERT(size <= INT_MAX); - - int r = send(pipe, (const char *) buffer, (int) size, 0); - - return r < 0 ? -WSAGetLastError() : r; -} - -int pipe_poll(pipe_event_source *sources, size_t num_sources, int timeout) -{ - ASSERT(num_sources <= INT_MAX); - - WSAPOLLFD *pollfds = NULL; - int r = -1; - - pollfds = calloc(num_sources, sizeof(WSAPOLLFD)); - if (pollfds == NULL) { - r = -ERROR_NOT_ENOUGH_MEMORY; - goto finish; - } - - for (size_t i = 0; i < num_sources; i++) { - pollfds[i].fd = sources[i].pipe; - pollfds[i].events = sources[i].interests; - } - - r = WSAPoll(pollfds, (ULONG) num_sources, timeout); - if (r < 0) { - r = -WSAGetLastError(); - goto finish; - } - - for (size_t i = 0; i < num_sources; i++) { - sources[i].events = pollfds[i].revents; - } - -finish: - free(pollfds); - - return r; -} - -int pipe_shutdown(SOCKET pipe) -{ - if (pipe == PIPE_INVALID) { - return 0; - } - - int r = shutdown(pipe, SD_SEND); - return r < 0 ? -WSAGetLastError() : 0; -} - -SOCKET pipe_destroy(SOCKET pipe) -{ - if (pipe == PIPE_INVALID) { - return PIPE_INVALID; - } - - int r = closesocket(pipe); - ASSERT_UNUSED(r == 0); - - return PIPE_INVALID; -} - -// -// process.windows.c -// -const HANDLE PROCESS_INVALID = INVALID_HANDLE_VALUE; // NOLINT - -static const DWORD CREATION_FLAGS = - // Create each child process in a new process group so we don't send - // `CTRL-BREAK` signals to more than one child process in - // `process_terminate`. - CREATE_NEW_PROCESS_GROUP | - // Create each child process with a Unicode environment as we accept any - // UTF-16 encoded environment (including Unicode characters). Create each - CREATE_UNICODE_ENVIRONMENT | - // Create each child with an extended STARTUPINFOEXW structure so we can - // specify which handles should be inherited. - EXTENDED_STARTUPINFO_PRESENT; - -// Argument escaping implementation is based on the following blog post: -// https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ - -static bool argument_should_escape(const char *argument) -{ - ASSERT(argument); - - bool should_escape = false; - - for (size_t i = 0; i < strlen(argument); i++) { - should_escape = should_escape || argument[i] == ' ' || - argument[i] == '\t' || argument[i] == '\n' || - argument[i] == '\v' || argument[i] == '\"'; - } - - return should_escape; -} - -static size_t argument_escaped_size(const char *argument) -{ - ASSERT(argument); - - size_t argument_size = strlen(argument); - - if (!argument_should_escape(argument)) { - return argument_size; - } - - size_t size = 2; // double quotes - - for (size_t i = 0; i < argument_size; i++) { - size_t num_backslashes = 0; - - while (i < argument_size && argument[i] == '\\') { - i++; - num_backslashes++; - } - - if (i == argument_size) { - size += num_backslashes * 2; - } else if (argument[i] == '"') { - size += num_backslashes * 2 + 2; - } else { - size += num_backslashes + 1; - } - } - - return size; -} - -static size_t argument_escape(char *dest, const char *argument) -{ - ASSERT(dest); - ASSERT(argument); - - size_t argument_size = strlen(argument); - - if (!argument_should_escape(argument)) { - strcpy(dest, argument); // NOLINT - return argument_size; - } - - const char *begin = dest; - - *dest++ = '"'; - - for (size_t i = 0; i < argument_size; i++) { - size_t num_backslashes = 0; - - while (i < argument_size && argument[i] == '\\') { - i++; - num_backslashes++; - } - - if (i == argument_size) { - memset(dest, '\\', num_backslashes * 2); - dest += num_backslashes * 2; - } else if (argument[i] == '"') { - memset(dest, '\\', num_backslashes * 2 + 1); - dest += num_backslashes * 2 + 1; - *dest++ = '"'; - } else { - memset(dest, '\\', num_backslashes); - dest += num_backslashes; - *dest++ = argument[i]; - } - } - - *dest++ = '"'; - - return (size_t)(dest - begin); -} - -static char *argv_join(const char *const *argv) -{ - ASSERT(argv); - - // Determine the size of the concatenated string first. - size_t joined_size = 1; // Count the NUL terminator. - for (int i = 0; argv[i] != NULL; i++) { - joined_size += argument_escaped_size(argv[i]); - - if (argv[i + 1] != NULL) { - joined_size++; // Count whitespace. - } - } - - char *joined = calloc(joined_size, sizeof(char)); - if (joined == NULL) { - SetLastError(ERROR_NOT_ENOUGH_MEMORY); - return NULL; - } - - char *current = joined; - for (int i = 0; argv[i] != NULL; i++) { - current += argument_escape(current, argv[i]); - - // We add a space after each argument in the joined arguments string except - // for the final argument. - if (argv[i + 1] != NULL) { - *current++ = ' '; - } - } - - *current = '\0'; - - return joined; -} - -static size_t env_join_size(const char *const *env) -{ - ASSERT(env); - - size_t joined_size = 1; // Count the NUL terminator. - for (int i = 0; env[i] != NULL; i++) { - joined_size += strlen(env[i]) + 1; // Count the NUL terminator. - } - - return joined_size; -} - -static char *env_join(const char *const *env) -{ - ASSERT(env); - - char *joined = calloc(env_join_size(env), sizeof(char)); - if (joined == NULL) { - SetLastError(ERROR_NOT_ENOUGH_MEMORY); - return NULL; - } - - char *current = joined; - for (int i = 0; env[i] != NULL; i++) { - size_t to_copy = strlen(env[i]) + 1; // Include NUL terminator. - memcpy(current, env[i], to_copy); - current += to_copy; - } - - *current = '\0'; - - return joined; -} - -static const DWORD NUM_ATTRIBUTES = 1; - -static LPPROC_THREAD_ATTRIBUTE_LIST setup_attribute_list(HANDLE *handles, - size_t num_handles) -{ - ASSERT(handles); - - int r = -1; - - // Make sure all the given handles can be inherited. - for (size_t i = 0; i < num_handles; i++) { - r = SetHandleInformation(handles[i], HANDLE_FLAG_INHERIT, - HANDLE_FLAG_INHERIT); - if (r == 0) { - return NULL; - } - } - - // Get the required size for `attribute_list`. - SIZE_T attribute_list_size = 0; - r = InitializeProcThreadAttributeList(NULL, NUM_ATTRIBUTES, 0, - &attribute_list_size); - if (r == 0 && GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - return NULL; - } - - LPPROC_THREAD_ATTRIBUTE_LIST attribute_list = malloc(attribute_list_size); - if (attribute_list == NULL) { - SetLastError(ERROR_NOT_ENOUGH_MEMORY); - return NULL; - } - - r = InitializeProcThreadAttributeList(attribute_list, NUM_ATTRIBUTES, 0, - &attribute_list_size); - if (r == 0) { - free(attribute_list); - return NULL; - } - - // Add the handles to be inherited to `attribute_list`. - r = UpdateProcThreadAttribute(attribute_list, 0, - PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handles, - num_handles * sizeof(HANDLE), NULL, NULL); - if (r == 0) { - DeleteProcThreadAttributeList(attribute_list); - return NULL; - } - - return attribute_list; -} - -#define NULSTR_FOREACH(i, l) \ - for ((i) = (l); (i) && *(i) != L'\0'; (i) = wcschr((i), L'\0') + 1) - -static wchar_t *env_concat(const wchar_t *a, const wchar_t *b) -{ - const wchar_t *i = NULL; - size_t size = 1; - wchar_t *c = NULL; - - NULSTR_FOREACH(i, a) { - size += wcslen(i) + 1; - } - - NULSTR_FOREACH(i, b) { - size += wcslen(i) + 1; - } - - wchar_t *r = calloc(size, sizeof(wchar_t)); - if (!r) { - return NULL; - } - - c = r; - - NULSTR_FOREACH(i, a) { - wcscpy(c, i); - c += wcslen(i) + 1; - } - - NULSTR_FOREACH(i, b) { - wcscpy(c, i); - c += wcslen(i) + 1; - } - - *c = L'\0'; - - return r; -} - -static wchar_t *env_setup(REPROC_ENV behavior, const char *const *extra) -{ - wchar_t *env_parent_wstring = NULL; - char *env_extra = NULL; - wchar_t *env_extra_wstring = NULL; - wchar_t *env_wstring = NULL; - - if (behavior == REPROC_ENV_EXTEND) { - env_parent_wstring = GetEnvironmentStringsW(); - } - - if (extra != NULL) { - env_extra = env_join(extra); - if (env_extra == NULL) { - goto finish; - } - - size_t joined_size = env_join_size(extra); - ASSERT(joined_size <= INT_MAX); - - env_extra_wstring = utf16_from_utf8(env_extra, (int) joined_size); - if (env_extra_wstring == NULL) { - goto finish; - } - } - - env_wstring = env_concat(env_parent_wstring, env_extra_wstring); - if (env_wstring == NULL) { - goto finish; - } - -finish: - FreeEnvironmentStringsW(env_parent_wstring); - free(env_extra); - free(env_extra_wstring); - - return env_wstring; -} - -int process_start(HANDLE *process, - const char *const *argv, - struct process_options options) -{ - ASSERT(process); - - if (argv == NULL) { - return -ERROR_CALL_NOT_IMPLEMENTED; - } - - ASSERT(argv[0] != NULL); - - char *command_line = NULL; - wchar_t *command_line_wstring = NULL; - wchar_t *env_wstring = NULL; - wchar_t *working_directory_wstring = NULL; - LPPROC_THREAD_ATTRIBUTE_LIST attribute_list = NULL; - PROCESS_INFORMATION info = { PROCESS_INVALID, HANDLE_INVALID, 0, 0 }; - int r = -1; - - // Join `argv` to a whitespace delimited string as required by - // `CreateProcessW`. - command_line = argv_join(argv); - if (command_line == NULL) { - r = -(int) GetLastError(); - goto finish; - } - - // Convert UTF-8 to UTF-16 as required by `CreateProcessW`. - command_line_wstring = utf16_from_utf8(command_line, -1); - if (command_line_wstring == NULL) { - r = -(int) GetLastError(); - goto finish; - } - - // Idem for `working_directory` if it isn't `NULL`. - if (options.working_directory != NULL) { - working_directory_wstring = utf16_from_utf8(options.working_directory, -1); - if (working_directory_wstring == NULL) { - r = -(int) GetLastError(); - goto finish; - } - } - - env_wstring = env_setup(options.env.behavior, options.env.extra); - if (env_wstring == NULL) { - r = -(int) GetLastError(); - goto finish; - } - - // Windows Vista added the `STARTUPINFOEXW` structure in which we can put a - // list of handles that should be inherited. Only these handles are inherited - // by the child process. Other code in an application that calls - // `CreateProcess` without passing a `STARTUPINFOEXW` struct containing the - // handles it should inherit can still unintentionally inherit handles meant - // for a reproc child process. See https://stackoverflow.com/a/2345126 for - // more information. - HANDLE handles[] = { options.handle.exit, options.handle.in, - options.handle.out, options.handle.err }; - size_t num_handles = ARRAY_SIZE(handles); - - if (options.handle.out == options.handle.err) { - // CreateProcess doesn't like the same handle being specified twice in the - // `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` attribute. - num_handles--; - } - - attribute_list = setup_attribute_list(handles, num_handles); - if (attribute_list == NULL) { - r = -(int) GetLastError(); - goto finish; - } - - STARTUPINFOEXW extended_startup_info = { - .StartupInfo = { .cb = sizeof(extended_startup_info), - .dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW, - // `STARTF_USESTDHANDLES` - .hStdInput = options.handle.in, - .hStdOutput = options.handle.out, - .hStdError = options.handle.err, - // `STARTF_USESHOWWINDOW`. Make sure the console window of - // the child process isn't visible. See - // https://github.com/DaanDeMeyer/reproc/issues/6 and - // https://github.com/DaanDeMeyer/reproc/pull/7 for more - // information. - .wShowWindow = SW_HIDE }, - .lpAttributeList = attribute_list - }; - - LPSTARTUPINFOW startup_info_address = &extended_startup_info.StartupInfo; - - // Child processes inherit the error mode of their parents. To avoid child - // processes creating error dialogs we set our error mode to not create error - // dialogs temporarily which is inherited by the child process. - DWORD previous_error_mode = SetErrorMode(SEM_NOGPFAULTERRORBOX); - - SECURITY_ATTRIBUTES do_not_inherit = { .nLength = sizeof(SECURITY_ATTRIBUTES), - .bInheritHandle = false, - .lpSecurityDescriptor = NULL }; - - r = CreateProcessW(NULL, command_line_wstring, &do_not_inherit, - &do_not_inherit, true, CREATION_FLAGS, env_wstring, - working_directory_wstring, startup_info_address, &info); - - SetErrorMode(previous_error_mode); - - if (r == 0) { - r = -(int) GetLastError(); - goto finish; - } - - *process = info.hProcess; - r = 0; - -finish: - free(command_line); - free(command_line_wstring); - free(env_wstring); - free(working_directory_wstring); - DeleteProcThreadAttributeList(attribute_list); - handle_destroy(info.hThread); - - return r < 0 ? r : 1; -} - -int process_pid(process_type process) -{ - ASSERT(process); - return (int) GetProcessId(process); -} - -int process_wait(HANDLE process) -{ - ASSERT(process); - - int r = -1; - - r = (int) WaitForSingleObject(process, INFINITE); - if ((DWORD) r == WAIT_FAILED) { - return -(int) GetLastError(); - } - - DWORD status = 0; - r = GetExitCodeProcess(process, &status); - if (r == 0) { - return -(int) GetLastError(); - } - - // `GenerateConsoleCtrlEvent` causes a process to exit with this exit code. - // Because `GenerateConsoleCtrlEvent` has roughly the same semantics as - // `SIGTERM`, we map its exit code to `SIGTERM`. - if (status == 3221225786) { - status = (DWORD) REPROC_SIGTERM; - } - - return (int) status; -} - -int process_terminate(HANDLE process) -{ - ASSERT(process && process != PROCESS_INVALID); - - // `GenerateConsoleCtrlEvent` can only be called on a process group. To call - // `GenerateConsoleCtrlEvent` on a single child process it has to be put in - // its own process group (which we did when starting the child process). - BOOL r = GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, GetProcessId(process)); - - return r == 0 ? -(int) GetLastError() : 0; -} - -int process_kill(HANDLE process) -{ - ASSERT(process && process != PROCESS_INVALID); - - // We use 137 (`SIGKILL`) as the exit status because it is the same exit - // status as a process that is stopped with the `SIGKILL` signal on POSIX - // systems. - BOOL r = TerminateProcess(process, (DWORD) REPROC_SIGKILL); - - return r == 0 ? -(int) GetLastError() : 0; -} - -HANDLE process_destroy(HANDLE process) -{ - return handle_destroy(process); -} - -// -// redirect.windows.c -// -static DWORD stream_to_id(REPROC_STREAM stream) -{ - switch (stream) { - case REPROC_STREAM_IN: - return STD_INPUT_HANDLE; - case REPROC_STREAM_OUT: - return STD_OUTPUT_HANDLE; - case REPROC_STREAM_ERR: - return STD_ERROR_HANDLE; - } - - return 0; -} - -int redirect_parent(HANDLE *child, REPROC_STREAM stream) -{ - ASSERT(child); - - DWORD id = stream_to_id(stream); - if (id == 0) { - return -ERROR_INVALID_PARAMETER; - } - - HANDLE *handle = GetStdHandle(id); - if (handle == INVALID_HANDLE_VALUE) { - return -(int) GetLastError(); - } - - if (handle == NULL) { - return -ERROR_BROKEN_PIPE; - } - - *child = handle; - - return 0; -} - -enum { FILE_NO_TEMPLATE = 0 }; - -int redirect_discard(HANDLE *child, REPROC_STREAM stream) -{ - return redirect_path(child, stream, "NUL"); -} - -int redirect_file(HANDLE *child, FILE *file) -{ - ASSERT(child); - ASSERT(file); - - int r = _fileno(file); - if (r < 0) { - return -ERROR_INVALID_HANDLE; - } - - intptr_t result = _get_osfhandle(r); - if (result == -1) { - return -ERROR_INVALID_HANDLE; - } - - *child = (HANDLE) result; - - return 0; -} - -int redirect_path(handle_type *child, REPROC_STREAM stream, const char *path) -{ - ASSERT(child); - ASSERT(path); - - DWORD mode = stream == REPROC_STREAM_IN ? GENERIC_READ : GENERIC_WRITE; - HANDLE handle = HANDLE_INVALID; - int r = -1; - - wchar_t *wpath = utf16_from_utf8(path, -1); - if (wpath == NULL) { - r = -(int) GetLastError(); - goto finish; - } - - SECURITY_ATTRIBUTES do_not_inherit = { .nLength = sizeof(SECURITY_ATTRIBUTES), - .bInheritHandle = false, - .lpSecurityDescriptor = NULL }; - - handle = CreateFileW(wpath, mode, FILE_SHARE_READ | FILE_SHARE_WRITE, - &do_not_inherit, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, - (HANDLE) FILE_NO_TEMPLATE); - if (handle == INVALID_HANDLE_VALUE) { - r = -(int) GetLastError(); - goto finish; - } - - *child = handle; - handle = HANDLE_INVALID; - r = 0; - -finish: - free(wpath); - handle_destroy(handle); - - return r; -} - -// -// utf.windows.c -// -wchar_t *utf16_from_utf8(const char *string, int size) -{ - ASSERT(string); - - // Determine wstring size (`MultiByteToWideChar` returns the required size if - // its last two arguments are `NULL` and 0). - int r = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string, size, NULL, - 0); - if (r == 0) { - return NULL; - } - - // `MultiByteToWideChar` does not return negative values so the cast to - // `size_t` is safe. - wchar_t *wstring = calloc((size_t) r, sizeof(wchar_t)); - if (wstring == NULL) { - SetLastError(ERROR_NOT_ENOUGH_MEMORY); - return NULL; - } - - // Now we pass our allocated string and its size as the last two arguments - // instead of `NULL` and 0 which makes `MultiByteToWideChar` actually perform - // the conversion. - r = MultiByteToWideChar(CP_UTF8, 0, string, size, wstring, r); - if (r == 0) { - free(wstring); - return NULL; - } - - return wstring; -} diff --git a/src/reproc_private.h b/src/reproc_private.h deleted file mode 100644 index 4d1dac9d..00000000 --- a/src/reproc_private.h +++ /dev/null @@ -1,246 +0,0 @@ -/** - * @copyright Daan De Meyer - * @license MIT - * @link https://github.com/DaanDeMeyer/reproc Source code. - * @note Amalgamation by Jefferson Gonzalez - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "reproc.h" - -// -// macro.h -// -#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) - -#define MIN(a, b) (a) < (b) ? (a) : (b) - -#if defined(_WIN32) && !defined(__MINGW32__) - #define THREAD_LOCAL __declspec(thread) -#else - #define THREAD_LOCAL __thread -#endif - -// -// clock.h -// -int64_t now(void); - -// -// error.h -// -#define ASSERT(expression) assert(expression) - -// Avoid unused assignment warnings in release mode when the result of an -// assignment is only used in an assert statement. -#define ASSERT_UNUSED(expression) \ - do { \ - (void) !(expression); \ - ASSERT((expression)); \ - } while (0) - -// Returns `r` if `expression` is false. -#define ASSERT_RETURN(expression, r) \ - do { \ - if (!(expression)) { \ - return (r); \ - } \ - } while (0) - -#define ASSERT_EINVAL(expression) ASSERT_RETURN(expression, REPROC_EINVAL) - -const char *error_string(int error); - -// -// handle.h -// -#if defined(_WIN32) -typedef void *handle_type; // `HANDLE` -#else -typedef int handle_type; // fd -#endif - -extern const handle_type HANDLE_INVALID; - -// Sets the `FD_CLOEXEC` flag on the file descriptor. POSIX only. -int handle_cloexec(handle_type handle, bool enable); - -// Closes `handle` if it is not an invalid handle and returns an invalid handle. -// Does not overwrite the last system error if an error occurs while closing -// `handle`. -handle_type handle_destroy(handle_type handle); - -// -// init.h -// -int init(void); - -void deinit(void); - -// -// options.h -// -reproc_stop_actions parse_stop_actions(reproc_stop_actions stop); - -int parse_options(reproc_options *options, const char *const *argv); - -// -// pipe.h -// -#ifdef _WIN64 -typedef uint64_t pipe_type; // `SOCKET` -#elif _WIN32 -typedef uint32_t pipe_type; // `SOCKET` -#else -typedef int pipe_type; // fd -#endif - -extern const pipe_type PIPE_INVALID; - -extern const short PIPE_EVENT_IN; -extern const short PIPE_EVENT_OUT; - -typedef struct { - pipe_type pipe; - short interests; - short events; -} pipe_event_source; - -// Creates a new anonymous pipe. `parent` and `child` are set to the parent and -// child endpoint of the pipe respectively. -int pipe_init(pipe_type *read, pipe_type *write); - -// Sets `pipe` to nonblocking mode. -int pipe_nonblocking(pipe_type pipe, bool enable); - -// Reads up to `size` bytes into `buffer` from the pipe indicated by `pipe` and -// returns the amount of bytes read. -int pipe_read(pipe_type pipe, uint8_t *buffer, size_t size); - -// Writes up to `size` bytes from `buffer` to the pipe indicated by `pipe` and -// returns the amount of bytes written. -int pipe_write(pipe_type pipe, const uint8_t *buffer, size_t size); - -// Polls the given event sources for events. -int pipe_poll(pipe_event_source *sources, size_t num_sources, int timeout); - -int pipe_shutdown(pipe_type pipe); - -pipe_type pipe_destroy(pipe_type pipe); - -// -// process.h -// -#if defined(_WIN32) -typedef void *process_type; // `HANDLE` -#else -typedef int process_type; // `pid_t` -#endif - -extern const process_type PROCESS_INVALID; - -struct process_options { - // If `NULL`, the child process inherits the environment of the current - // process. - struct { - REPROC_ENV behavior; - const char *const *extra; - } env; - // If not `NULL`, the working directory of the child process is set to - // `working_directory`. - const char *working_directory; - // The standard streams of the child process are redirected to the `in`, `out` - // and `err` handles. If a handle is `HANDLE_INVALID`, the corresponding child - // process standard stream is closed. The `exit` handle is simply inherited by - // the child process. - struct { - handle_type in; - handle_type out; - handle_type err; - handle_type exit; - } handle; -}; - -// Spawns a child process that executes the command stored in `argv`. -// -// If `argv` is `NULL` on POSIX, `exec` is not called after fork and this -// function returns 0 in the child process and > 0 in the parent process. On -// Windows, if `argv` is `NULL`, an error is returned. -// -// The process handle of the new child process is assigned to `process`. -int process_start(process_type *process, - const char *const *argv, - struct process_options options); - -// Returns the process ID associated with the given handle. On posix systems the -// handle is the process ID and so its returned directly. On WIN32 the process -// ID is returned from GetProcessId on the pointer. -int process_pid(process_type process); - -// Returns the process's exit status if it has finished running. -int process_wait(process_type process); - -// Sends the `SIGTERM` (POSIX) or `CTRL-BREAK` (Windows) signal to the process -// indicated by `process`. -int process_terminate(process_type process); - -// Sends the `SIGKILL` signal to `process` (POSIX) or calls `TerminateProcess` -// on `process` (Windows). -int process_kill(process_type process); - -process_type process_destroy(process_type process); - -// -// redirect.h -// -int redirect_init(pipe_type *parent, - handle_type *child, - REPROC_STREAM stream, - reproc_redirect redirect, - bool nonblocking, - handle_type out); - -handle_type redirect_destroy(handle_type child, REPROC_REDIRECT type); - -// Internal prototypes - -int redirect_parent(handle_type *child, REPROC_STREAM stream); - -int redirect_discard(handle_type *child, REPROC_STREAM stream); - -int redirect_file(handle_type *child, FILE *file); - -int redirect_path(handle_type *child, REPROC_STREAM stream, const char *path); - -// -// strv.h -// -#define STRV_FOREACH(s, l) for ((s) = (l); (s) && *(s); (s)++) - -char **strv_concat(char *const *a, const char *const *b); - -char **strv_free(char **l); - -// -// utf.h -// - -// `size` represents the entire size of `string`, including NUL-terminators. We -// take the entire size because strings like the environment string passed to -// CreateProcessW includes multiple NUL-terminators so we can't always rely on -// `strlen` to calculate the string length for us. See the lpEnvironment -// documentation of CreateProcessW: -// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw -// Pass -1 as the size to have `utf16_from_utf8` calculate the size until (and -// including) the first NUL terminator. -wchar_t *utf16_from_utf8(const char *string, int size);