// 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 #include #include #ifndef _WIN32 #include #include #include #include #include #include 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