383 lines
13 KiB
C
383 lines
13 KiB
C
// calogProc.c -- calog subprocess library (see calogProc.h). Spawns a child with posix_spawn (NOT
|
|
// fork -- calog runs many pthreads), feeds its stdin while draining stdout/stderr through one poll
|
|
// loop so a large transfer cannot deadlock, and returns its exit code + captured output. POSIX-only;
|
|
// on Windows procRun reports "unsupported" rather than shipping a half-working implementation.
|
|
|
|
#define _GNU_SOURCE
|
|
|
|
#include "calogProc.h"
|
|
|
|
#include "calogInternal.h"
|
|
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#ifndef _WIN32
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <poll.h>
|
|
#include <spawn.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
extern char **environ;
|
|
#endif
|
|
|
|
// Upper bound on captured stdout/stderr, so a runaway child cannot exhaust memory.
|
|
#define PROC_MAX (64 * 1024 * 1024)
|
|
|
|
typedef struct ProcBufT {
|
|
char *data;
|
|
size_t len;
|
|
size_t cap;
|
|
} ProcBufT;
|
|
|
|
static int32_t procBufAppend(ProcBufT *buffer, const void *bytes, size_t length);
|
|
static CalogValueT *procOpt(CalogAggT *opts, const char *name);
|
|
static int32_t procRun(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
|
|
|
|
int32_t calogProcRegister(CalogT *calog) {
|
|
return calogRegisterInline(calog, "procRun", procRun, NULL);
|
|
}
|
|
|
|
|
|
static int32_t procBufAppend(ProcBufT *buffer, const void *bytes, size_t length) {
|
|
if (buffer->len + length > buffer->cap) {
|
|
size_t wanted;
|
|
char *grown;
|
|
wanted = buffer->cap ? buffer->cap * 2 : 4096;
|
|
while (wanted < buffer->len + length) {
|
|
wanted *= 2;
|
|
}
|
|
grown = (char *)realloc(buffer->data, wanted);
|
|
if (grown == NULL) {
|
|
return calogErrOomE;
|
|
}
|
|
buffer->data = grown;
|
|
buffer->cap = wanted;
|
|
}
|
|
memcpy(buffer->data + buffer->len, bytes, length);
|
|
buffer->len += length;
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static CalogValueT *procOpt(CalogAggT *opts, const char *name) {
|
|
CalogValueT key;
|
|
CalogValueT *field;
|
|
|
|
if (opts == NULL) {
|
|
return NULL;
|
|
}
|
|
if (calogValueString(&key, name, (int64_t)strlen(name)) != calogOkE) {
|
|
return NULL;
|
|
}
|
|
field = calogAggGet(opts, &key);
|
|
calogValueFree(&key);
|
|
return field;
|
|
}
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
|
static int32_t procRun(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)args;
|
|
(void)argCount;
|
|
(void)userData;
|
|
calogValueNil(result);
|
|
return calogFail(result, calogErrUnsupportedE, "procRun: subprocess spawning is not supported on Windows in this build");
|
|
}
|
|
|
|
|
|
#else
|
|
|
|
|
|
// Build a result map { exit, stdout, stderr } and hand ownership to result.
|
|
static int32_t procResult(CalogValueT *result, int32_t exitCode, ProcBufT *out, ProcBufT *err) {
|
|
CalogAggT *map;
|
|
int32_t status;
|
|
|
|
status = calogAggCreate(&map, calogMapE);
|
|
if (status != calogOkE) {
|
|
return calogFail(result, status, "procRun: out of memory");
|
|
}
|
|
status = calogMapSetInt(map, "exit", exitCode);
|
|
if (status == calogOkE) {
|
|
status = calogMapSetStr(map, "stdout", out->data ? out->data : "", (int64_t)out->len);
|
|
}
|
|
if (status == calogOkE) {
|
|
status = calogMapSetStr(map, "stderr", err->data ? err->data : "", (int64_t)err->len);
|
|
}
|
|
if (status != calogOkE) {
|
|
calogAggFree(map);
|
|
return calogFail(result, status, "procRun: failed to build the result");
|
|
}
|
|
calogValueAgg(result, map);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static int32_t procRun(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
posix_spawn_file_actions_t actions;
|
|
ProcBufT outBuf;
|
|
ProcBufT errBuf;
|
|
char **argv;
|
|
char **builtEnv;
|
|
char **envp;
|
|
const char *stdinBytes;
|
|
const char *cwd;
|
|
CalogValueT *field;
|
|
int64_t stdinLen;
|
|
int64_t stdinPos;
|
|
int64_t argCountList;
|
|
int64_t i;
|
|
int inPipe[2];
|
|
int outPipe[2];
|
|
int errPipe[2];
|
|
int spawnRc;
|
|
int waitStatus;
|
|
int32_t exitCode;
|
|
int32_t status;
|
|
pid_t pid;
|
|
bool actionsReady;
|
|
|
|
(void)userData;
|
|
calogValueNil(result);
|
|
if (argCount < 1 || argCount > 2 || args[0].type != calogAggE || calogAggIsKeyed(args[0].as.agg)) {
|
|
return calogFail(result, calogErrArgE, "procRun expects (argvList [, opts])");
|
|
}
|
|
if (argCount == 2 && (args[1].type != calogAggE || !calogAggIsKeyed(args[1].as.agg))) {
|
|
return calogFail(result, calogErrArgE, "procRun: opts must be a map");
|
|
}
|
|
argCountList = args[0].as.agg->arrayCount;
|
|
if (argCountList < 1) {
|
|
return calogFail(result, calogErrArgE, "procRun: argvList must have at least the program");
|
|
}
|
|
argv = (char **)calloc((size_t)argCountList + 1, sizeof(char *));
|
|
if (argv == NULL) {
|
|
return calogFail(result, calogErrOomE, "procRun: out of memory");
|
|
}
|
|
for (i = 0; i < argCountList; i++) {
|
|
if (args[0].as.agg->array[i].type != calogStringE) {
|
|
free(argv);
|
|
return calogFail(result, calogErrArgE, "procRun: every argv element must be a string");
|
|
}
|
|
argv[i] = args[0].as.agg->array[i].as.s.bytes;
|
|
}
|
|
stdinBytes = NULL;
|
|
stdinLen = 0;
|
|
cwd = NULL;
|
|
builtEnv = NULL;
|
|
envp = environ;
|
|
if (argCount == 2) {
|
|
field = procOpt(args[1].as.agg, "stdin");
|
|
if (field != NULL && field->type == calogStringE) {
|
|
stdinBytes = field->as.s.bytes;
|
|
stdinLen = field->as.s.length;
|
|
}
|
|
field = procOpt(args[1].as.agg, "cwd");
|
|
if (field != NULL && field->type == calogStringE) {
|
|
cwd = field->as.s.bytes;
|
|
}
|
|
field = procOpt(args[1].as.agg, "env");
|
|
if (field != NULL && field->type == calogAggE && calogAggIsKeyed(field->as.agg)) {
|
|
CalogAggT *envMap;
|
|
int64_t envCount;
|
|
int64_t e;
|
|
envMap = field->as.agg;
|
|
envCount = envMap->pairCount;
|
|
builtEnv = (char **)calloc((size_t)envCount + 1, sizeof(char *));
|
|
if (builtEnv == NULL) {
|
|
free(argv);
|
|
return calogFail(result, calogErrOomE, "procRun: out of memory");
|
|
}
|
|
for (e = 0; e < envCount; e++) {
|
|
const char *k;
|
|
const char *v;
|
|
size_t klen;
|
|
size_t vlen;
|
|
if (envMap->pairs[e].key.type != calogStringE || envMap->pairs[e].value.type != calogStringE) {
|
|
continue;
|
|
}
|
|
k = envMap->pairs[e].key.as.s.bytes;
|
|
v = envMap->pairs[e].value.as.s.bytes;
|
|
klen = (size_t)envMap->pairs[e].key.as.s.length;
|
|
vlen = (size_t)envMap->pairs[e].value.as.s.length;
|
|
builtEnv[e] = (char *)malloc(klen + vlen + 2);
|
|
if (builtEnv[e] == NULL) {
|
|
continue;
|
|
}
|
|
memcpy(builtEnv[e], k, klen);
|
|
builtEnv[e][klen] = '=';
|
|
memcpy(builtEnv[e] + klen + 1, v, vlen);
|
|
builtEnv[e][klen + 1 + vlen] = '\0';
|
|
}
|
|
envp = builtEnv;
|
|
}
|
|
}
|
|
if (pipe(inPipe) != 0 || pipe(outPipe) != 0 || pipe(errPipe) != 0) {
|
|
free(argv);
|
|
if (builtEnv != NULL) {
|
|
for (i = 0; builtEnv[i] != NULL; i++) {
|
|
free(builtEnv[i]);
|
|
}
|
|
free(builtEnv);
|
|
}
|
|
return calogFail(result, calogErrArgE, "procRun: could not create pipes");
|
|
}
|
|
posix_spawn_file_actions_init(&actions);
|
|
actionsReady = true;
|
|
if (cwd != NULL) {
|
|
posix_spawn_file_actions_addchdir_np(&actions, cwd);
|
|
}
|
|
posix_spawn_file_actions_adddup2(&actions, inPipe[0], 0);
|
|
posix_spawn_file_actions_adddup2(&actions, outPipe[1], 1);
|
|
posix_spawn_file_actions_adddup2(&actions, errPipe[1], 2);
|
|
posix_spawn_file_actions_addclose(&actions, inPipe[1]);
|
|
posix_spawn_file_actions_addclose(&actions, outPipe[0]);
|
|
posix_spawn_file_actions_addclose(&actions, errPipe[0]);
|
|
spawnRc = posix_spawnp(&pid, argv[0], &actions, NULL, argv, envp);
|
|
posix_spawn_file_actions_destroy(&actions);
|
|
(void)actionsReady;
|
|
// Parent keeps the outer ends: write inPipe[1], read outPipe[0]/errPipe[0].
|
|
close(inPipe[0]);
|
|
close(outPipe[1]);
|
|
close(errPipe[1]);
|
|
free(argv);
|
|
if (builtEnv != NULL) {
|
|
for (i = 0; builtEnv[i] != NULL; i++) {
|
|
free(builtEnv[i]);
|
|
}
|
|
free(builtEnv);
|
|
}
|
|
if (spawnRc != 0) {
|
|
close(inPipe[1]);
|
|
close(outPipe[0]);
|
|
close(errPipe[0]);
|
|
return calogFail(result, calogErrArgE, strerror(spawnRc));
|
|
}
|
|
fcntl(inPipe[1], F_SETFL, fcntl(inPipe[1], F_GETFL, 0) | O_NONBLOCK);
|
|
fcntl(outPipe[0], F_SETFL, fcntl(outPipe[0], F_GETFL, 0) | O_NONBLOCK);
|
|
fcntl(errPipe[0], F_SETFL, fcntl(errPipe[0], F_GETFL, 0) | O_NONBLOCK);
|
|
memset(&outBuf, 0, sizeof(outBuf));
|
|
memset(&errBuf, 0, sizeof(errBuf));
|
|
stdinPos = 0;
|
|
if (stdinLen == 0) {
|
|
close(inPipe[1]);
|
|
inPipe[1] = -1;
|
|
}
|
|
status = calogOkE;
|
|
while (inPipe[1] >= 0 || outPipe[0] >= 0 || errPipe[0] >= 0) {
|
|
struct pollfd fds[3];
|
|
int nfds;
|
|
int inIdx;
|
|
int outIdx;
|
|
int errIdx;
|
|
nfds = 0;
|
|
inIdx = -1;
|
|
outIdx = -1;
|
|
errIdx = -1;
|
|
if (inPipe[1] >= 0) {
|
|
fds[nfds].fd = inPipe[1];
|
|
fds[nfds].events = POLLOUT;
|
|
inIdx = nfds++;
|
|
}
|
|
if (outPipe[0] >= 0) {
|
|
fds[nfds].fd = outPipe[0];
|
|
fds[nfds].events = POLLIN;
|
|
outIdx = nfds++;
|
|
}
|
|
if (errPipe[0] >= 0) {
|
|
fds[nfds].fd = errPipe[0];
|
|
fds[nfds].events = POLLIN;
|
|
errIdx = nfds++;
|
|
}
|
|
if (poll(fds, (nfds_t)nfds, -1) < 0) {
|
|
if (errno == EINTR) {
|
|
continue;
|
|
}
|
|
status = calogFail(result, calogErrArgE, strerror(errno));
|
|
break;
|
|
}
|
|
if (inIdx >= 0 && (fds[inIdx].revents & (POLLOUT | POLLERR | POLLHUP))) {
|
|
ssize_t wrote;
|
|
wrote = write(inPipe[1], stdinBytes + stdinPos, (size_t)(stdinLen - stdinPos));
|
|
if (wrote > 0) {
|
|
stdinPos += wrote;
|
|
}
|
|
if (wrote < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
|
|
close(inPipe[1]);
|
|
inPipe[1] = -1;
|
|
} else if (stdinPos >= stdinLen) {
|
|
close(inPipe[1]);
|
|
inPipe[1] = -1;
|
|
}
|
|
}
|
|
if (outIdx >= 0 && (fds[outIdx].revents & (POLLIN | POLLERR | POLLHUP))) {
|
|
char chunk[65536];
|
|
ssize_t got;
|
|
got = read(outPipe[0], chunk, sizeof(chunk));
|
|
if (got > 0) {
|
|
if (outBuf.len + (size_t)got > PROC_MAX || procBufAppend(&outBuf, chunk, (size_t)got) != calogOkE) {
|
|
status = calogFail(result, calogErrRangeE, "procRun: stdout exceeds the size cap");
|
|
close(outPipe[0]);
|
|
outPipe[0] = -1;
|
|
}
|
|
} else if (got == 0 || (got < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) {
|
|
close(outPipe[0]);
|
|
outPipe[0] = -1;
|
|
}
|
|
}
|
|
if (errIdx >= 0 && (fds[errIdx].revents & (POLLIN | POLLERR | POLLHUP))) {
|
|
char chunk[65536];
|
|
ssize_t got;
|
|
got = read(errPipe[0], chunk, sizeof(chunk));
|
|
if (got > 0) {
|
|
if (errBuf.len + (size_t)got > PROC_MAX || procBufAppend(&errBuf, chunk, (size_t)got) != calogOkE) {
|
|
status = calogFail(result, calogErrRangeE, "procRun: stderr exceeds the size cap");
|
|
close(errPipe[0]);
|
|
errPipe[0] = -1;
|
|
}
|
|
} else if (got == 0 || (got < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) {
|
|
close(errPipe[0]);
|
|
errPipe[0] = -1;
|
|
}
|
|
}
|
|
if (status != calogOkE) {
|
|
break;
|
|
}
|
|
}
|
|
if (inPipe[1] >= 0) {
|
|
close(inPipe[1]);
|
|
}
|
|
if (outPipe[0] >= 0) {
|
|
close(outPipe[0]);
|
|
}
|
|
if (errPipe[0] >= 0) {
|
|
close(errPipe[0]);
|
|
}
|
|
while (waitpid(pid, &waitStatus, 0) < 0 && errno == EINTR) {
|
|
continue;
|
|
}
|
|
if (status != calogOkE) {
|
|
free(outBuf.data);
|
|
free(errBuf.data);
|
|
return status;
|
|
}
|
|
if (WIFEXITED(waitStatus)) {
|
|
exitCode = (int32_t)WEXITSTATUS(waitStatus);
|
|
} else if (WIFSIGNALED(waitStatus)) {
|
|
exitCode = -(int32_t)WTERMSIG(waitStatus);
|
|
} else {
|
|
exitCode = -1;
|
|
}
|
|
status = procResult(result, exitCode, &outBuf, &errBuf);
|
|
free(outBuf.data);
|
|
free(errBuf.data);
|
|
return status;
|
|
}
|
|
|
|
|
|
#endif
|