717 lines
25 KiB
C
717 lines
25 KiB
C
// calogProc.c -- calog subprocess library (see calogProc.h). Spawns a child, feeds its stdin while
|
|
// draining stdout/stderr so a large transfer cannot deadlock, and returns its exit code + captured
|
|
// output. POSIX spawns with posix_spawn (NOT fork -- calog runs many pthreads) and multiplexes the
|
|
// three pipes with one poll loop. Windows spawns with CreateProcess and, because anonymous pipes
|
|
// have no poll, writes stdin on a helper thread while the main thread drains stdout/stderr.
|
|
|
|
#define _GNU_SOURCE
|
|
|
|
#include "calogProc.h"
|
|
|
|
#include "calogInternal.h"
|
|
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#else
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <poll.h>
|
|
#include <signal.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)
|
|
// Transfer granularity for a single read/write while draining the child's pipes.
|
|
#define PROC_CHUNK (64 * 1024)
|
|
|
|
typedef struct ProcBufT {
|
|
char *data;
|
|
size_t len;
|
|
size_t cap;
|
|
} ProcBufT;
|
|
|
|
#ifdef _WIN32
|
|
// Payload handed to the stdin-writer thread (see procWinStdinThread).
|
|
typedef struct ProcWinStdinT {
|
|
HANDLE handle;
|
|
const char *bytes;
|
|
size_t length;
|
|
} ProcWinStdinT;
|
|
#endif
|
|
|
|
static int32_t procBufAppend(ProcBufT *buffer, const void *bytes, size_t length);
|
|
static CalogValueT *procOpt(CalogAggT *opts, const char *name);
|
|
static int32_t procResult(CalogValueT *result, int32_t exitCode, ProcBufT *out, ProcBufT *err);
|
|
static int32_t procRun(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
#ifdef _WIN32
|
|
static int32_t procWinAppendArg(ProcBufT *cmd, const char *arg);
|
|
static DWORD WINAPI procWinStdinThread(LPVOID param);
|
|
#endif
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
// 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;
|
|
}
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
|
// Spawn a child with CreateProcess. argvList is quoted into a single command line; opts may carry a
|
|
// stdin string, a cwd, and an env map. stdin is written on a helper thread (anonymous pipes have no
|
|
// poll, and a blocking write would otherwise deadlock the stdout/stderr drain done here). Output is
|
|
// capped at PROC_MAX. Returns { exit, stdout, stderr }; exit is the process exit code. Build-verified
|
|
// via the zig Windows cross build; not run-verified (no Windows host here).
|
|
static int32_t procRun(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
STARTUPINFOA si;
|
|
PROCESS_INFORMATION pi;
|
|
SECURITY_ATTRIBUTES sa;
|
|
ProcWinStdinT stdinJob;
|
|
ProcBufT cmd;
|
|
ProcBufT envBlock;
|
|
ProcBufT outBuf;
|
|
ProcBufT errBuf;
|
|
HANDLE inRead;
|
|
HANDLE inWrite;
|
|
HANDLE outRead;
|
|
HANDLE outWrite;
|
|
HANDLE errRead;
|
|
HANDLE errWrite;
|
|
HANDLE stdinThread;
|
|
const char *stdinBytes;
|
|
const char *cwd;
|
|
char *envArg;
|
|
CalogValueT *field;
|
|
int64_t stdinLen;
|
|
int64_t argListCount;
|
|
int64_t i;
|
|
int32_t status;
|
|
DWORD exitStatus;
|
|
bool outOpen;
|
|
bool errOpen;
|
|
|
|
(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");
|
|
}
|
|
argListCount = args[0].as.agg->arrayCount;
|
|
if (argListCount < 1) {
|
|
return calogFail(result, calogErrArgE, "procRun: argvList must have at least the program");
|
|
}
|
|
// Quote each argv element into one command line (CreateProcess takes a single string).
|
|
memset(&cmd, 0, sizeof(cmd));
|
|
for (i = 0; i < argListCount; i++) {
|
|
if (args[0].as.agg->array[i].type != calogStringE) {
|
|
free(cmd.data);
|
|
return calogFail(result, calogErrArgE, "procRun: every argv element must be a string");
|
|
}
|
|
if ((i > 0 && procBufAppend(&cmd, " ", 1) != calogOkE) ||
|
|
procWinAppendArg(&cmd, args[0].as.agg->array[i].as.s.bytes) != calogOkE) {
|
|
free(cmd.data);
|
|
return calogFail(result, calogErrOomE, "procRun: out of memory");
|
|
}
|
|
}
|
|
if (procBufAppend(&cmd, "\0", 1) != calogOkE) {
|
|
free(cmd.data);
|
|
return calogFail(result, calogErrOomE, "procRun: out of memory");
|
|
}
|
|
stdinBytes = NULL;
|
|
stdinLen = 0;
|
|
cwd = NULL;
|
|
envArg = NULL;
|
|
memset(&envBlock, 0, sizeof(envBlock));
|
|
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 e;
|
|
|
|
envMap = field->as.agg;
|
|
for (e = 0; e < envMap->pairCount; e++) {
|
|
if (envMap->pairs[e].key.type != calogStringE || envMap->pairs[e].value.type != calogStringE) {
|
|
continue;
|
|
}
|
|
if (procBufAppend(&envBlock, envMap->pairs[e].key.as.s.bytes, (size_t)envMap->pairs[e].key.as.s.length) != calogOkE ||
|
|
procBufAppend(&envBlock, "=", 1) != calogOkE ||
|
|
procBufAppend(&envBlock, envMap->pairs[e].value.as.s.bytes, (size_t)envMap->pairs[e].value.as.s.length) != calogOkE ||
|
|
procBufAppend(&envBlock, "\0", 1) != calogOkE) {
|
|
free(cmd.data);
|
|
free(envBlock.data);
|
|
return calogFail(result, calogErrOomE, "procRun: out of memory");
|
|
}
|
|
}
|
|
// An environment block is terminated by a final extra NUL (a double NUL overall).
|
|
if (procBufAppend(&envBlock, "\0", 1) != calogOkE) {
|
|
free(cmd.data);
|
|
free(envBlock.data);
|
|
return calogFail(result, calogErrOomE, "procRun: out of memory");
|
|
}
|
|
envArg = envBlock.data;
|
|
}
|
|
}
|
|
// Three inheritable pipes; the parent's own ends are made non-inheritable so the child cannot
|
|
// keep them open (which would keep our reads from ever seeing end-of-file).
|
|
sa.nLength = sizeof(sa);
|
|
sa.bInheritHandle = TRUE;
|
|
sa.lpSecurityDescriptor = NULL;
|
|
// Create the three pipes one at a time so a partial failure can close the ends already opened
|
|
// (a single short-circuited guard would leak them).
|
|
if (!CreatePipe(&inRead, &inWrite, &sa, 0)) {
|
|
free(cmd.data);
|
|
free(envBlock.data);
|
|
return calogFail(result, calogErrArgE, "procRun: could not create pipes");
|
|
}
|
|
if (!CreatePipe(&outRead, &outWrite, &sa, 0)) {
|
|
CloseHandle(inRead);
|
|
CloseHandle(inWrite);
|
|
free(cmd.data);
|
|
free(envBlock.data);
|
|
return calogFail(result, calogErrArgE, "procRun: could not create pipes");
|
|
}
|
|
if (!CreatePipe(&errRead, &errWrite, &sa, 0)) {
|
|
CloseHandle(inRead);
|
|
CloseHandle(inWrite);
|
|
CloseHandle(outRead);
|
|
CloseHandle(outWrite);
|
|
free(cmd.data);
|
|
free(envBlock.data);
|
|
return calogFail(result, calogErrArgE, "procRun: could not create pipes");
|
|
}
|
|
SetHandleInformation(inWrite, HANDLE_FLAG_INHERIT, 0);
|
|
SetHandleInformation(outRead, HANDLE_FLAG_INHERIT, 0);
|
|
SetHandleInformation(errRead, HANDLE_FLAG_INHERIT, 0);
|
|
memset(&si, 0, sizeof(si));
|
|
si.cb = sizeof(si);
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
si.hStdInput = inRead;
|
|
si.hStdOutput = outWrite;
|
|
si.hStdError = errWrite;
|
|
memset(&pi, 0, sizeof(pi));
|
|
if (!CreateProcessA(NULL, cmd.data, NULL, NULL, TRUE, 0, envArg, cwd, &si, &pi)) {
|
|
CloseHandle(inRead);
|
|
CloseHandle(inWrite);
|
|
CloseHandle(outRead);
|
|
CloseHandle(outWrite);
|
|
CloseHandle(errRead);
|
|
CloseHandle(errWrite);
|
|
free(cmd.data);
|
|
free(envBlock.data);
|
|
return calogFail(result, calogErrArgE, "procRun: CreateProcess failed");
|
|
}
|
|
free(cmd.data);
|
|
free(envBlock.data);
|
|
// The child owns the far ends now; close ours so end-of-file becomes observable.
|
|
CloseHandle(inRead);
|
|
CloseHandle(outWrite);
|
|
CloseHandle(errWrite);
|
|
stdinThread = NULL;
|
|
if (stdinLen > 0) {
|
|
stdinJob.handle = inWrite;
|
|
stdinJob.bytes = stdinBytes;
|
|
stdinJob.length = (size_t)stdinLen;
|
|
stdinThread = CreateThread(NULL, 0, procWinStdinThread, &stdinJob, 0, NULL);
|
|
}
|
|
if (stdinThread == NULL) {
|
|
CloseHandle(inWrite); // nothing to send, or the thread could not start
|
|
}
|
|
memset(&outBuf, 0, sizeof(outBuf));
|
|
memset(&errBuf, 0, sizeof(errBuf));
|
|
status = calogOkE;
|
|
outOpen = true;
|
|
errOpen = true;
|
|
while (status == calogOkE && (outOpen || errOpen)) {
|
|
bool didRead;
|
|
HANDLE pipes[2];
|
|
bool *open[2];
|
|
ProcBufT *bufs[2];
|
|
int p;
|
|
|
|
didRead = false;
|
|
pipes[0] = outRead;
|
|
pipes[1] = errRead;
|
|
open[0] = &outOpen;
|
|
open[1] = &errOpen;
|
|
bufs[0] = &outBuf;
|
|
bufs[1] = &errBuf;
|
|
for (p = 0; p < 2; p++) {
|
|
DWORD avail;
|
|
DWORD got;
|
|
char chunk[PROC_CHUNK];
|
|
|
|
if (!*open[p]) {
|
|
continue;
|
|
}
|
|
if (!PeekNamedPipe(pipes[p], NULL, 0, NULL, &avail, NULL)) {
|
|
*open[p] = false; // broken pipe: the child closed this stream and it is drained
|
|
continue;
|
|
}
|
|
if (avail == 0) {
|
|
continue; // open but idle; the Sleep below yields before we retry
|
|
}
|
|
if (!ReadFile(pipes[p], chunk, (avail > PROC_CHUNK) ? PROC_CHUNK : avail, &got, NULL) || got == 0) {
|
|
*open[p] = false;
|
|
continue;
|
|
}
|
|
if (bufs[p]->len + got > PROC_MAX || procBufAppend(bufs[p], chunk, got) != calogOkE) {
|
|
status = calogFail(result, calogErrRangeE, (p == 0) ? "procRun: stdout exceeds the size cap" : "procRun: stderr exceeds the size cap");
|
|
break;
|
|
}
|
|
didRead = true;
|
|
}
|
|
if (status == calogOkE && !didRead && (outOpen || errOpen)) {
|
|
Sleep(1);
|
|
}
|
|
}
|
|
if (status != calogOkE) {
|
|
TerminateProcess(pi.hProcess, 1);
|
|
}
|
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
|
GetExitCodeProcess(pi.hProcess, &exitStatus);
|
|
if (stdinThread != NULL) {
|
|
WaitForSingleObject(stdinThread, INFINITE);
|
|
CloseHandle(stdinThread);
|
|
}
|
|
CloseHandle(outRead);
|
|
CloseHandle(errRead);
|
|
CloseHandle(pi.hThread);
|
|
CloseHandle(pi.hProcess);
|
|
if (status != calogOkE) {
|
|
free(outBuf.data);
|
|
free(errBuf.data);
|
|
return status;
|
|
}
|
|
status = procResult(result, (int32_t)exitStatus, &outBuf, &errBuf);
|
|
free(outBuf.data);
|
|
free(errBuf.data);
|
|
return status;
|
|
}
|
|
|
|
|
|
// Append arg to cmd as one command-line token, quoted per the CommandLineToArgvW rules: a run of
|
|
// backslashes is doubled only when it precedes a double quote or ends the (quoted) token, and the
|
|
// token is wrapped in quotes when it is empty or contains whitespace or a quote.
|
|
static int32_t procWinAppendArg(ProcBufT *cmd, const char *arg) {
|
|
size_t len;
|
|
size_t i;
|
|
bool quote;
|
|
|
|
len = strlen(arg);
|
|
quote = (len == 0);
|
|
for (i = 0; i < len && !quote; i++) {
|
|
if (arg[i] == ' ' || arg[i] == '\t' || arg[i] == '\n' || arg[i] == '\v' || arg[i] == '"') {
|
|
quote = true;
|
|
}
|
|
}
|
|
if (!quote) {
|
|
return procBufAppend(cmd, arg, len);
|
|
}
|
|
if (procBufAppend(cmd, "\"", 1) != calogOkE) {
|
|
return calogErrOomE;
|
|
}
|
|
i = 0;
|
|
while (i < len) {
|
|
size_t slashes;
|
|
size_t s;
|
|
|
|
slashes = 0;
|
|
while (i < len && arg[i] == '\\') {
|
|
slashes++;
|
|
i++;
|
|
}
|
|
if (i == len) {
|
|
slashes *= 2; // trailing backslashes precede the closing quote
|
|
} else if (arg[i] == '"') {
|
|
slashes = slashes * 2 + 1; // backslashes then the escaped quote
|
|
}
|
|
for (s = 0; s < slashes; s++) {
|
|
if (procBufAppend(cmd, "\\", 1) != calogOkE) {
|
|
return calogErrOomE;
|
|
}
|
|
}
|
|
if (i < len) {
|
|
if (procBufAppend(cmd, &arg[i], 1) != calogOkE) {
|
|
return calogErrOomE;
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
return procBufAppend(cmd, "\"", 1);
|
|
}
|
|
|
|
|
|
// Write the whole stdin payload, then close the pipe. Runs on its own thread so a blocking WriteFile
|
|
// on a full pipe cannot stall the main thread's stdout/stderr drain.
|
|
static DWORD WINAPI procWinStdinThread(LPVOID param) {
|
|
ProcWinStdinT *job;
|
|
size_t pos;
|
|
|
|
job = (ProcWinStdinT *)param;
|
|
pos = 0;
|
|
while (pos < job->length) {
|
|
DWORD wrote;
|
|
DWORD chunk;
|
|
|
|
chunk = (job->length - pos > PROC_CHUNK) ? PROC_CHUNK : (DWORD)(job->length - pos);
|
|
if (!WriteFile(job->handle, job->bytes + pos, chunk, &wrote, NULL) || wrote == 0) {
|
|
break; // the child closed its stdin (or exited); stop feeding it
|
|
}
|
|
pos += wrote;
|
|
}
|
|
CloseHandle(job->handle);
|
|
return 0;
|
|
}
|
|
|
|
|
|
#else
|
|
|
|
|
|
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[PROC_CHUNK];
|
|
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[PROC_CHUNK];
|
|
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]);
|
|
}
|
|
// If we aborted early (e.g. the output cap was exceeded), the child may still be running and
|
|
// blocked writing to the pipe ends we just closed; kill it so waitpid cannot hang forever.
|
|
if (status != calogOkE) {
|
|
kill(pid, SIGKILL);
|
|
}
|
|
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
|