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
This commit is contained in:
parent
838bbf8285
commit
9b1b1a3820
|
@ -48,4 +48,3 @@ endif
|
||||||
|
|
||||||
subdir('lib/font_renderer')
|
subdir('lib/font_renderer')
|
||||||
subdir('src')
|
subdir('src')
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,14 @@
|
||||||
int luaopen_system(lua_State *L);
|
int luaopen_system(lua_State *L);
|
||||||
int luaopen_renderer(lua_State *L);
|
int luaopen_renderer(lua_State *L);
|
||||||
int luaopen_regex(lua_State *L);
|
int luaopen_regex(lua_State *L);
|
||||||
|
int luaopen_process(lua_State *L);
|
||||||
|
|
||||||
|
|
||||||
static const luaL_Reg libs[] = {
|
static const luaL_Reg libs[] = {
|
||||||
{ "system", luaopen_system },
|
{ "system", luaopen_system },
|
||||||
{ "renderer", luaopen_renderer },
|
{ "renderer", luaopen_renderer },
|
||||||
{ "regex", luaopen_regex },
|
{ "regex", luaopen_regex },
|
||||||
|
{ "process", luaopen_process },
|
||||||
{ NULL, NULL }
|
{ NULL, NULL }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,431 @@
|
||||||
|
/**
|
||||||
|
* Basic binding of reproc into Lua.
|
||||||
|
* @copyright Jefferson Gonzalez
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <lua.h>
|
||||||
|
#include <lualib.h>
|
||||||
|
#include <lauxlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#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; runs<tries; runs++){
|
||||||
|
out = reproc_read(
|
||||||
|
self->process,
|
||||||
|
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; runs<tries; runs++){
|
||||||
|
out = reproc_read(
|
||||||
|
self->process,
|
||||||
|
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;
|
||||||
|
}
|
|
@ -1,3 +1,8 @@
|
||||||
|
platform = 'posix'
|
||||||
|
if host_machine.system() == 'windows'
|
||||||
|
platform = 'windows'
|
||||||
|
endif
|
||||||
|
|
||||||
lite_sources = [
|
lite_sources = [
|
||||||
'api/api.c',
|
'api/api.c',
|
||||||
'api/cp_replace.c',
|
'api/cp_replace.c',
|
||||||
|
@ -5,6 +10,9 @@ lite_sources = [
|
||||||
'api/renderer_font.c',
|
'api/renderer_font.c',
|
||||||
'api/regex.c',
|
'api/regex.c',
|
||||||
'api/system.c',
|
'api/system.c',
|
||||||
|
'api/process.c',
|
||||||
|
'reproc.c',
|
||||||
|
'reproc.' + platform + '.c',
|
||||||
'renderer.c',
|
'renderer.c',
|
||||||
'renwindow.c',
|
'renwindow.c',
|
||||||
'fontdesc.c',
|
'fontdesc.c',
|
||||||
|
@ -26,4 +34,3 @@ executable('lite',
|
||||||
install: true,
|
install: true,
|
||||||
gui_app: true,
|
gui_app: true,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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 <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
//
|
||||||
|
// 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
|
|
@ -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 <time.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/resource.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
#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.
|
File diff suppressed because it is too large
Load Diff
|
@ -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 <assert.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <wchar.h>
|
||||||
|
|
||||||
|
#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);
|
Loading…
Reference in New Issue