calog/libs/calogProc.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