From 9b1b1a3820aa19ea9b9fcc6e11608f0e9296a0da Mon Sep 17 00:00:00 2001 From: jgmdev Date: Wed, 19 May 2021 05:44:52 -0400 Subject: [PATCH] Initial work to allow writing LSP client in LUA. * Added an amalgamated C version of https://github.com/DaanDeMeyer/reproc * Added lua binding of reproc accesible in the process package. * Modified meson build to include reproc sources as src/api/process.c --- meson.build | 1 - src/api/api.c | 2 + src/api/process.c | 431 +++++++++++++++ src/meson.build | 9 +- src/reproc.c | 1246 ++++++++++++++++++++++++++++++++++++++++++ src/reproc.h | 666 ++++++++++++++++++++++ src/reproc.posix.c | 797 +++++++++++++++++++++++++++ src/reproc.windows.c | 1001 +++++++++++++++++++++++++++++++++ src/reproc_private.h | 246 +++++++++ 9 files changed, 4397 insertions(+), 2 deletions(-) create mode 100644 src/api/process.c create mode 100644 src/reproc.c create mode 100644 src/reproc.h create mode 100644 src/reproc.posix.c create mode 100644 src/reproc.windows.c create mode 100644 src/reproc_private.h diff --git a/meson.build b/meson.build index c0857769..c1f3d5d4 100644 --- a/meson.build +++ b/meson.build @@ -48,4 +48,3 @@ endif subdir('lib/font_renderer') subdir('src') - diff --git a/src/api/api.c b/src/api/api.c index 5ea2e782..c479ca4a 100644 --- a/src/api/api.c +++ b/src/api/api.c @@ -4,12 +4,14 @@ int luaopen_system(lua_State *L); int luaopen_renderer(lua_State *L); int luaopen_regex(lua_State *L); +int luaopen_process(lua_State *L); static const luaL_Reg libs[] = { { "system", luaopen_system }, { "renderer", luaopen_renderer }, { "regex", luaopen_regex }, + { "process", luaopen_process }, { NULL, NULL } }; diff --git a/src/api/process.c b/src/api/process.c new file mode 100644 index 00000000..92bd016a --- /dev/null +++ b/src/api/process.c @@ -0,0 +1,431 @@ +/** + * Basic binding of reproc into Lua. + * @copyright Jefferson Gonzalez + * @license MIT + */ + +#include +#include +#include +#include +#include +#include + +#include "reproc.h" + +typedef struct { + reproc_t * process; + lua_State* L; + +} process_t; + +static int process_new(lua_State* L) +{ + process_t* self = (process_t*) lua_newuserdata( + L, sizeof(process_t) + ); + + memset(self, 0, sizeof(process_t)); + + self->process = NULL; + self->L = L; + + luaL_getmetatable(L, "PROCESS"); + lua_setmetatable(L, -2); + + return 1; +} + +static int process_strerror(lua_State* L) +{ + int error_code = luaL_checknumber(L, 1); + + if(error_code){ + lua_pushstring( + L, + reproc_strerror(error_code) + ); + } else { + lua_pushnil(L); + } + + return 1; +} + +static int process_gc(lua_State* L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, "PROCESS"); + + if(self->process){ + reproc_kill(self->process); + reproc_destroy(self->process); + self->process = NULL; + } + + return 0; +} + +static int process_start(lua_State* L) +{ + process_t* self = (process_t*) lua_touserdata(L, 1); + + luaL_checktype(L, 2, LUA_TTABLE); + + char* path = NULL; + size_t path_len = 0; + + if(lua_type(L, 3) == LUA_TSTRING){ + path = (char*) lua_tolstring(L, 3, &path_len); + } + + size_t deadline = 0; + + if(lua_type(L, 4) == LUA_TNUMBER){ + deadline = lua_tonumber(L, 4); + } + + size_t table_len = luaL_len(L, 2); + char* command[table_len+1]; + command[table_len] = NULL; + + int i; + for(i=1; i<=table_len; i++){ + lua_pushnumber(L, i); + lua_gettable(L, 2); + + command[i-1] = (char*) lua_tostring(L, -1); + + lua_remove(L, -1); + } + + if(self->process){ + reproc_kill(self->process); + reproc_destroy(self->process); + } + + self->process = reproc_new(); + + int out = reproc_start( + self->process, + (const char* const*) command, + (reproc_options){ + .working_directory = path, + .deadline = deadline, + .nonblocking=true, + .redirect.err.type=REPROC_REDIRECT_PIPE + } + ); + + if(out > 0) { + lua_pushboolean(L, 1); + } + else { + reproc_destroy(self->process); + self->process = NULL; + lua_pushnumber(L, out); + } + + return 1; +} + +static int process_pid(lua_State* L) +{ + process_t* self = (process_t*) lua_touserdata(L, 1); + + if(self->process){ + lua_pushnumber(L, reproc_pid(self->process)); + } else { + lua_pushnumber(L, 0); + } + + return 1; +} + +static int process_read(lua_State* L) +{ + process_t* self = (process_t*) lua_touserdata(L, 1); + + if(self->process){ + int read_size = 4096; + if (lua_type(L, 2) == LUA_TNUMBER){ + read_size = (int) lua_tonumber(L, 2); + } + + int tries = 1; + if (lua_type(L, 3) == LUA_TNUMBER){ + tries = (int) lua_tonumber(L, 3); + } + + int out = 0; + uint8_t buffer[read_size]; + + int runs; + for (runs=0; runsprocess, + REPROC_STREAM_OUT, + buffer, + read_size + ); + + if (out >= 0) + break; + } + + // if request for tries was set and nothing + // read kill the process + if(tries > 1 && out < 0) + out = REPROC_EPIPE; + + if(out == REPROC_EPIPE){ + reproc_kill(self->process); + reproc_destroy(self->process); + self->process = NULL; + + lua_pushnil(L); + } else if(out > 0) { + lua_pushlstring(L, (const char*) buffer, out); + } else { + lua_pushnil(L); + } + } else { + lua_pushnil(L); + } + + return 1; +} + +static int process_read_errors(lua_State* L) +{ + process_t* self = (process_t*) lua_touserdata(L, 1); + + if(self->process){ + int read_size = 4096; + if (lua_type(L, 2) == LUA_TNUMBER){ + read_size = (int) lua_tonumber(L, 2); + } + + int tries = 1; + if (lua_type(L, 3) == LUA_TNUMBER){ + tries = (int) lua_tonumber(L, 3); + } + + int out = 0; + uint8_t buffer[read_size]; + + int runs; + for (runs=0; runsprocess, + REPROC_STREAM_ERR, + buffer, + read_size + ); + + if (out >= 0) + break; + } + + // if request for tries was set and nothing + // read kill the process + if(tries > 1 && out < 0) + out = REPROC_EPIPE; + + if(out == REPROC_EPIPE){ + reproc_kill(self->process); + reproc_destroy(self->process); + self->process = NULL; + + lua_pushnil(L); + } else if(out > 0) { + lua_pushlstring(L, (const char*) buffer, out); + } else { + lua_pushnil(L); + } + } else { + lua_pushnil(L); + } + + return 1; +} + +static int process_write(lua_State* L) +{ + process_t* self = (process_t*) lua_touserdata(L, 1); + + if(self->process){ + size_t data_size = 0; + const char* data = luaL_checklstring(L, 2, &data_size); + + int out = 0; + + out = reproc_write( + self->process, + (uint8_t*) data, + data_size + ); + + if(out == REPROC_EPIPE){ + reproc_kill(self->process); + reproc_destroy(self->process); + self->process = NULL; + } + + lua_pushnumber(L, out); + } else { + lua_pushnumber(L, REPROC_EPIPE); + } + + return 1; +} + +static int process_close_stream(lua_State* L) +{ + process_t* self = (process_t*) lua_touserdata(L, 1); + + if(self->process){ + size_t stream = luaL_checknumber(L, 2); + + int out = reproc_close(self->process, stream); + + lua_pushnumber(L, out); + } else { + lua_pushnumber(L, REPROC_EINVAL); + } + + return 1; +} + +static int process_wait(lua_State* L) +{ + process_t* self = (process_t*) lua_touserdata(L, 1); + + if(self->process){ + size_t timeout = luaL_checknumber(L, 2); + + int out = reproc_wait(self->process, timeout); + + if(out >= 0){ + reproc_destroy(self->process); + self->process = NULL; + } + + lua_pushnumber(L, out); + } else { + lua_pushnumber(L, REPROC_EINVAL); + } + + return 1; +} + +static int process_terminate(lua_State* L) +{ + process_t* self = (process_t*) lua_touserdata(L, 1); + + if(self->process){ + int out = reproc_terminate(self->process); + + if(out < 0){ + lua_pushnumber(L, out); + } else { + reproc_destroy(self->process); + self->process = NULL; + lua_pushboolean(L, 1); + } + } else { + lua_pushnumber(L, REPROC_EINVAL); + } + + return 1; +} + +static int process_kill(lua_State* L) +{ + process_t* self = (process_t*) lua_touserdata(L, 1); + + if(self->process){ + int out = reproc_kill(self->process); + + if(out < 0){ + lua_pushnumber(L, out); + } else { + reproc_destroy(self->process); + self->process = NULL; + lua_pushboolean(L, 1); + } + } else { + lua_pushnumber(L, REPROC_EINVAL); + } + + return 1; +} + +static int process_running(lua_State* L) +{ + process_t* self = (process_t*) lua_touserdata(L, 1); + + if(self->process){ + lua_pushboolean(L, 1); + } else { + lua_pushboolean(L, 0); + } + + return 1; +} + +static const struct luaL_Reg process_methods[] = { + { "__gc", process_gc}, + {"start", process_start}, + {"pid", process_pid}, + {"read", process_read}, + {"read_errors", process_read_errors}, + {"write", process_write}, + {"close_stream", process_close_stream}, + {"wait", process_wait}, + {"terminate", process_terminate}, + {"kill", process_kill}, + {"running", process_running}, + {NULL, NULL} +}; + +static const struct luaL_Reg process[] = { + {"new", process_new}, + {"strerror", process_strerror}, + {"ERROR_PIPE", NULL}, + {"ERROR_WOULDBLOCK", NULL}, + {"ERROR_TIMEDOUT", NULL}, + {"STREAM_STDIN", NULL}, + {"STREAM_STDOUT", NULL}, + {"STREAM_STDERR", NULL}, + {NULL, NULL} +}; + +int luaopen_process(lua_State *L) +{ + luaL_newmetatable(L, "PROCESS"); + luaL_setfuncs(L, process_methods, 0); + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); + + luaL_newlib(L, process); + + lua_pushnumber(L, REPROC_EPIPE); + lua_setfield(L, -2, "ERROR_PIPE"); + + lua_pushnumber(L, REPROC_EWOULDBLOCK); + lua_setfield(L, -2, "ERROR_WOULDBLOCK"); + + lua_pushnumber(L, REPROC_ETIMEDOUT); + lua_setfield(L, -2, "ERROR_TIMEDOUT"); + + lua_pushnumber(L, REPROC_STREAM_IN); + lua_setfield(L, -2, "STREAM_STDIN"); + + lua_pushnumber(L, REPROC_STREAM_OUT); + lua_setfield(L, -2, "STREAM_STDOUT"); + + lua_pushnumber(L, REPROC_STREAM_ERR); + lua_setfield(L, -2, "STREAM_STDERR"); + + return 1; +} diff --git a/src/meson.build b/src/meson.build index faa1a8ea..469cba1e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,3 +1,8 @@ +platform = 'posix' +if host_machine.system() == 'windows' + platform = 'windows' +endif + lite_sources = [ 'api/api.c', 'api/cp_replace.c', @@ -5,6 +10,9 @@ lite_sources = [ 'api/renderer_font.c', 'api/regex.c', 'api/system.c', + 'api/process.c', + 'reproc.c', + 'reproc.' + platform + '.c', 'renderer.c', 'renwindow.c', 'fontdesc.c', @@ -26,4 +34,3 @@ executable('lite', install: true, gui_app: true, ) - diff --git a/src/reproc.c b/src/reproc.c new file mode 100644 index 00000000..9ab5a7f2 --- /dev/null +++ b/src/reproc.c @@ -0,0 +1,1246 @@ +/** + * @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 new file mode 100644 index 00000000..c9bb65b2 --- /dev/null +++ b/src/reproc.h @@ -0,0 +1,666 @@ +/** + * @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 new file mode 100644 index 00000000..a5a3def9 --- /dev/null +++ b/src/reproc.posix.c @@ -0,0 +1,797 @@ +/** + * @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 new file mode 100644 index 00000000..00c37e83 --- /dev/null +++ b/src/reproc.windows.c @@ -0,0 +1,1001 @@ +/** + * @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 new file mode 100644 index 00000000..4d1dac9d --- /dev/null +++ b/src/reproc_private.h @@ -0,0 +1,246 @@ +/** + * @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);