- TUI: centered home screen with logo, boxed input with mode line, sidebar (>=120 cols), Ctrl+P palette, Tab Build/Plan, Shift+Tab approval cycle, mouse (cards, cursor, pickers, wheel), /sidebar /mouse /approval - /models probing: key check on entry (401 re-asks), API models in /model - Shell sandbox in C: Linux Landlock (writes limited to workspace/tmp, ABI>=4 blocks TCP), macOS sandbox_init, rules fallback elsewhere - Approval modes ask/auto/yolo (default auto); read-only command whitelist; ask to rerun outside the sandbox when a command looks blocked - Upgradable bootstrap scripts (git pull --ff-only, rebuild, reinstall) and cst-agent-update; cst-agent --version - tests: test_sandbox, test_models
796 lines
20 KiB
C
796 lines
20 KiB
C
#ifndef _DEFAULT_SOURCE
|
|
#define _DEFAULT_SOURCE
|
|
#endif
|
|
#ifndef _GNU_SOURCE
|
|
#define _GNU_SOURCE
|
|
#endif
|
|
|
|
#ifdef __APPLE__
|
|
#include <mach-o/dyld.h>
|
|
#include <stdint.h>
|
|
#endif
|
|
#include "os_compat.h"
|
|
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#ifdef _WIN32
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#include <direct.h>
|
|
#include <fcntl.h>
|
|
#include <io.h>
|
|
#include <process.h>
|
|
#include <windows.h>
|
|
#else
|
|
#include <fcntl.h>
|
|
#include <poll.h>
|
|
#include <signal.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
void os_run_result_free(os_run_result_t *r) {
|
|
if (!r)
|
|
return;
|
|
free(r->out);
|
|
r->out = NULL;
|
|
r->out_len = 0;
|
|
}
|
|
|
|
int os_path_is_abs(const char *p) {
|
|
if (!p || !p[0])
|
|
return 0;
|
|
#ifdef _WIN32
|
|
if (p[0] == '/' || p[0] == '\\')
|
|
return 1;
|
|
if (((p[0] >= 'A' && p[0] <= 'Z') || (p[0] >= 'a' && p[0] <= 'z')) &&
|
|
p[1] == ':' && (p[2] == '/' || p[2] == '\\' || p[2] == '\0'))
|
|
return 1;
|
|
return 0;
|
|
#else
|
|
return p[0] == '/';
|
|
#endif
|
|
}
|
|
|
|
void os_path_norm_slashes(char *p) {
|
|
if (!p)
|
|
return;
|
|
for (; *p; p++) {
|
|
if (*p == '\\')
|
|
*p = '/';
|
|
}
|
|
}
|
|
|
|
int os_path_join(char *out, size_t out_sz, const char *a, const char *b) {
|
|
if (!out || out_sz < 2 || !a)
|
|
return -1;
|
|
if (!b || !b[0]) {
|
|
snprintf(out, out_sz, "%s", a);
|
|
return 0;
|
|
}
|
|
if (os_path_is_abs(b)) {
|
|
snprintf(out, out_sz, "%s", b);
|
|
return 0;
|
|
}
|
|
size_t al = strlen(a);
|
|
while (al > 0 && (a[al - 1] == '/' || a[al - 1] == '\\'))
|
|
al--;
|
|
#ifdef _WIN32
|
|
int n = snprintf(out, out_sz, "%.*s\\%s", (int)al, a, b);
|
|
#else
|
|
int n = snprintf(out, out_sz, "%.*s/%s", (int)al, a, b);
|
|
#endif
|
|
if (n < 0 || (size_t)n >= out_sz)
|
|
return -1;
|
|
return 0;
|
|
}
|
|
|
|
int os_realpath(const char *path, char *out, size_t out_sz) {
|
|
if (!path || !out || out_sz < 2)
|
|
return -1;
|
|
#ifdef _WIN32
|
|
char buf[OS_PATH_MAX];
|
|
DWORD n = GetFullPathNameA(path, (DWORD)sizeof(buf), buf, NULL);
|
|
if (n == 0 || n >= sizeof(buf))
|
|
return -1;
|
|
/* Prefer forward slashes internally for sandbox compares. */
|
|
os_path_norm_slashes(buf);
|
|
if (strlen(buf) + 1 > out_sz)
|
|
return -1;
|
|
memcpy(out, buf, strlen(buf) + 1);
|
|
/* Lowercase drive letter for stable compares */
|
|
if (out[0] && out[1] == ':') {
|
|
if (out[0] >= 'A' && out[0] <= 'Z')
|
|
out[0] = (char)(out[0] - 'A' + 'a');
|
|
}
|
|
return 0;
|
|
#else
|
|
char *r = realpath(path, NULL);
|
|
if (!r)
|
|
return -1;
|
|
if (strlen(r) + 1 > out_sz) {
|
|
free(r);
|
|
return -1;
|
|
}
|
|
memcpy(out, r, strlen(r) + 1);
|
|
free(r);
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
int os_path_under(const char *root, const char *path) {
|
|
if (!root || !path)
|
|
return 0;
|
|
size_t rl = strlen(root);
|
|
if (strncmp(path, root, rl) != 0)
|
|
return 0;
|
|
if (path[rl] == '\0')
|
|
return 1;
|
|
#ifdef _WIN32
|
|
return path[rl] == '/' || path[rl] == '\\';
|
|
#else
|
|
return path[rl] == '/';
|
|
#endif
|
|
}
|
|
|
|
int os_mkdir(const char *path, int mode) {
|
|
if (!path)
|
|
return -1;
|
|
#ifdef _WIN32
|
|
(void)mode;
|
|
if (_mkdir(path) != 0 && errno != EEXIST)
|
|
return -1;
|
|
return 0;
|
|
#else
|
|
if (mkdir(path, (mode_t)mode) != 0 && errno != EEXIST)
|
|
return -1;
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
int os_mkdir_p(const char *path, int mode) {
|
|
if (!path || !path[0])
|
|
return -1;
|
|
char tmp[OS_PATH_MAX];
|
|
snprintf(tmp, sizeof(tmp), "%s", path);
|
|
for (char *p = tmp + 1; *p; p++) {
|
|
if (*p == '/' || *p == '\\') {
|
|
char c = *p;
|
|
*p = 0;
|
|
os_mkdir(tmp, mode);
|
|
*p = c;
|
|
}
|
|
}
|
|
return os_mkdir(tmp, mode);
|
|
}
|
|
|
|
int os_config_dir(char *out, size_t out_sz) {
|
|
if (!out || out_sz < 8)
|
|
return -1;
|
|
#ifdef _WIN32
|
|
const char *base = getenv("APPDATA");
|
|
if (!base || !base[0])
|
|
base = getenv("LOCALAPPDATA");
|
|
if (!base || !base[0])
|
|
return -1;
|
|
int n = snprintf(out, out_sz, "%s\\cst-agent", base);
|
|
#else
|
|
const char *xdg = getenv("XDG_CONFIG_HOME");
|
|
const char *home = getenv("HOME");
|
|
int n;
|
|
if (xdg && xdg[0] == '/')
|
|
n = snprintf(out, out_sz, "%s/cst-agent", xdg);
|
|
else if (home && home[0])
|
|
n = snprintf(out, out_sz, "%s/.config/cst-agent", home);
|
|
else
|
|
return -1;
|
|
#endif
|
|
if (n < 0 || (size_t)n >= out_sz)
|
|
return -1;
|
|
return 0;
|
|
}
|
|
|
|
int os_temp_dir(char *out, size_t out_sz) {
|
|
if (!out || out_sz < 2)
|
|
return -1;
|
|
#ifdef _WIN32
|
|
DWORD n = GetTempPathA((DWORD)out_sz, out);
|
|
if (n == 0 || n >= out_sz)
|
|
return -1;
|
|
os_path_norm_slashes(out);
|
|
size_t L = strlen(out);
|
|
while (L > 0 && out[L - 1] == '/')
|
|
out[--L] = 0;
|
|
return 0;
|
|
#else
|
|
const char *t = getenv("TMPDIR");
|
|
if (!t || !t[0])
|
|
t = "/tmp";
|
|
snprintf(out, out_sz, "%s", t);
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
int os_mkstemp(char *out, size_t out_sz, const char *prefix) {
|
|
if (!out || out_sz < 16)
|
|
return -1;
|
|
char dir[OS_PATH_MAX];
|
|
if (os_temp_dir(dir, sizeof(dir)) != 0)
|
|
return -1;
|
|
if (!prefix)
|
|
prefix = "cst";
|
|
#ifdef _WIN32
|
|
char name[MAX_PATH];
|
|
if (GetTempFileNameA(dir, prefix, 0, name) == 0)
|
|
return -1;
|
|
os_path_norm_slashes(name);
|
|
if (strlen(name) + 1 > out_sz)
|
|
return -1;
|
|
memcpy(out, name, strlen(name) + 1);
|
|
int fd = _open(out, _O_RDWR | _O_BINARY);
|
|
return fd;
|
|
#else
|
|
int n = snprintf(out, out_sz, "%s/%s-XXXXXX", dir, prefix);
|
|
if (n < 0 || (size_t)n >= out_sz)
|
|
return -1;
|
|
int fd = mkstemp(out);
|
|
return fd;
|
|
#endif
|
|
}
|
|
|
|
/* Optional: main() may set this for Termux / argv0 fallback when
|
|
* /proc/self/exe is unavailable. */
|
|
static char g_argv0_dir[OS_PATH_MAX];
|
|
|
|
void os_set_prog_path(const char *argv0) {
|
|
g_argv0_dir[0] = 0;
|
|
if (!argv0 || !argv0[0])
|
|
return;
|
|
char buf[OS_PATH_MAX];
|
|
snprintf(buf, sizeof(buf), "%s", argv0);
|
|
os_path_norm_slashes(buf);
|
|
char *slash = strrchr(buf, '/');
|
|
if (!slash)
|
|
return;
|
|
*slash = 0;
|
|
/* Relative "./foo" → keep as-is; caller may pass absolute. */
|
|
snprintf(g_argv0_dir, sizeof(g_argv0_dir), "%s", buf);
|
|
}
|
|
|
|
int os_exe_dir(char *out, size_t out_sz) {
|
|
if (!out || out_sz < 2)
|
|
return -1;
|
|
const char *env = getenv("CST_EXE_DIR");
|
|
if (env && env[0]) {
|
|
snprintf(out, out_sz, "%s", env);
|
|
return 0;
|
|
}
|
|
#ifdef _WIN32
|
|
char buf[OS_PATH_MAX];
|
|
DWORD n = GetModuleFileNameA(NULL, buf, (DWORD)sizeof(buf));
|
|
if (n == 0 || n >= sizeof(buf))
|
|
return -1;
|
|
os_path_norm_slashes(buf);
|
|
char *slash = strrchr(buf, '/');
|
|
if (!slash)
|
|
return -1;
|
|
*slash = 0;
|
|
if (strlen(buf) + 1 > out_sz)
|
|
return -1;
|
|
memcpy(out, buf, strlen(buf) + 1);
|
|
return 0;
|
|
#else
|
|
char buf[OS_PATH_MAX];
|
|
#ifdef __APPLE__
|
|
uint32_t asz = (uint32_t)sizeof(buf);
|
|
ssize_t n = _NSGetExecutablePath(buf, &asz) == 0 ? (ssize_t)strlen(buf) : -1;
|
|
#else
|
|
ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
|
|
#endif
|
|
if (n >= 0) {
|
|
buf[n] = 0;
|
|
char *slash = strrchr(buf, '/');
|
|
if (slash) {
|
|
*slash = 0;
|
|
if (strlen(buf) + 1 <= out_sz) {
|
|
memcpy(out, buf, strlen(buf) + 1);
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
/* Termux / restricted Android: fall back to argv0 dir. */
|
|
if (g_argv0_dir[0]) {
|
|
snprintf(out, out_sz, "%s", g_argv0_dir);
|
|
return 0;
|
|
}
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
static int file_exists(const char *p) {
|
|
#ifdef _WIN32
|
|
DWORD a = GetFileAttributesA(p);
|
|
return a != INVALID_FILE_ATTRIBUTES && !(a & FILE_ATTRIBUTE_DIRECTORY);
|
|
#else
|
|
return access(p, X_OK) == 0 || access(p, F_OK) == 0;
|
|
#endif
|
|
}
|
|
|
|
int os_resolve_shell(char *out, size_t out_sz) {
|
|
if (!out || out_sz < 2)
|
|
return -1;
|
|
const char *env = getenv("CST_SHELL");
|
|
if (env && env[0]) {
|
|
snprintf(out, out_sz, "%s", env);
|
|
return 0;
|
|
}
|
|
char dir[OS_PATH_MAX];
|
|
if (os_exe_dir(dir, sizeof(dir)) == 0) {
|
|
#ifdef _WIN32
|
|
char cand[OS_PATH_MAX];
|
|
if (os_path_join(cand, sizeof(cand), dir, "busybox.exe") == 0 &&
|
|
file_exists(cand)) {
|
|
snprintf(out, out_sz, "%s", cand);
|
|
return 0;
|
|
}
|
|
#else
|
|
char cand[OS_PATH_MAX];
|
|
if (os_path_join(cand, sizeof(cand), dir, "busybox") == 0 &&
|
|
file_exists(cand)) {
|
|
snprintf(out, out_sz, "%s", cand);
|
|
return 0;
|
|
}
|
|
#endif
|
|
}
|
|
#ifdef _WIN32
|
|
snprintf(out, out_sz, "%s", "busybox.exe");
|
|
return 0;
|
|
#else
|
|
/* Termux: $PREFIX/bin/sh */
|
|
{
|
|
const char *prefix = getenv("PREFIX");
|
|
if (prefix && prefix[0]) {
|
|
char cand[OS_PATH_MAX];
|
|
if (os_path_join(cand, sizeof(cand), prefix, "bin/sh") == 0 &&
|
|
file_exists(cand)) {
|
|
snprintf(out, out_sz, "%s", cand);
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
if (file_exists("/bin/sh")) {
|
|
snprintf(out, out_sz, "%s", "/bin/sh");
|
|
return 0;
|
|
}
|
|
if (file_exists("/system/bin/sh")) {
|
|
snprintf(out, out_sz, "%s", "/system/bin/sh");
|
|
return 0;
|
|
}
|
|
snprintf(out, out_sz, "%s", "sh");
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
static volatile int *g_run_cancel;
|
|
static int g_run_timeout_s;
|
|
|
|
void os_set_cancel_flag(volatile int *flag) { g_run_cancel = flag; }
|
|
|
|
void os_set_run_timeout(int seconds) { g_run_timeout_s = seconds; }
|
|
|
|
static int run_timeout_s(void) {
|
|
if (g_run_timeout_s > 0)
|
|
return g_run_timeout_s;
|
|
const char *e = getenv("CST_SHELL_TIMEOUT");
|
|
if (e && atoi(e) > 0)
|
|
return atoi(e);
|
|
return 120;
|
|
}
|
|
|
|
double os_now_ms(void) {
|
|
#ifdef _WIN32
|
|
static LARGE_INTEGER freq;
|
|
LARGE_INTEGER now;
|
|
if (!freq.QuadPart)
|
|
QueryPerformanceFrequency(&freq);
|
|
QueryPerformanceCounter(&now);
|
|
return (double)now.QuadPart * 1000.0 / (double)freq.QuadPart;
|
|
#else
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
|
|
#endif
|
|
}
|
|
|
|
/* Append to capture buffer, keeping at most max_out bytes. */
|
|
static int cap_append(char **buf, size_t *len, size_t *cap, size_t max_out,
|
|
const char *data, size_t n, int *truncated) {
|
|
if (*len >= max_out) {
|
|
*truncated = 1;
|
|
return 0;
|
|
}
|
|
if (*len + n > max_out) {
|
|
n = max_out - *len;
|
|
*truncated = 1;
|
|
}
|
|
if (*len + n + 1 > *cap) {
|
|
size_t nc = *cap ? *cap * 2 : 4096;
|
|
while (nc < *len + n + 1)
|
|
nc *= 2;
|
|
char *nb = realloc(*buf, nc);
|
|
if (!nb)
|
|
return -1;
|
|
*buf = nb;
|
|
*cap = nc;
|
|
}
|
|
memcpy(*buf + *len, data, n);
|
|
*len += n;
|
|
(*buf)[*len] = 0;
|
|
return 0;
|
|
}
|
|
|
|
static void cap_note(char **buf, size_t *len, size_t *cap, const char *note) {
|
|
size_t n = strlen(note);
|
|
if (*len + n + 1 > *cap) {
|
|
char *nb = realloc(*buf, *len + n + 1);
|
|
if (!nb)
|
|
return;
|
|
*buf = nb;
|
|
*cap = *len + n + 1;
|
|
}
|
|
memcpy(*buf + *len, note, n + 1);
|
|
*len += n;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
int os_run_capture(const char *cwd, const char *const argv[], size_t max_out,
|
|
os_run_result_t *r) {
|
|
if (!argv || !argv[0] || !r)
|
|
return -1;
|
|
if (max_out == 0)
|
|
max_out = 512 * 1024;
|
|
memset(r, 0, sizeof(*r));
|
|
|
|
char cmdline[8192];
|
|
size_t pos = 0;
|
|
cmdline[0] = 0;
|
|
for (int i = 0; argv[i]; i++) {
|
|
const char *a = argv[i];
|
|
int need_q = strchr(a, ' ') || strchr(a, '\t') || strchr(a, '"');
|
|
if (i && pos + 1 < sizeof(cmdline))
|
|
cmdline[pos++] = ' ';
|
|
if (need_q) {
|
|
if (pos + 1 < sizeof(cmdline))
|
|
cmdline[pos++] = '"';
|
|
for (const char *p = a; *p && pos + 2 < sizeof(cmdline); p++) {
|
|
if (*p == '"') {
|
|
cmdline[pos++] = '\\';
|
|
cmdline[pos++] = '"';
|
|
} else {
|
|
cmdline[pos++] = *p;
|
|
}
|
|
}
|
|
if (pos + 1 < sizeof(cmdline))
|
|
cmdline[pos++] = '"';
|
|
} else {
|
|
size_t L = strlen(a);
|
|
if (pos + L >= sizeof(cmdline))
|
|
break;
|
|
memcpy(cmdline + pos, a, L);
|
|
pos += L;
|
|
}
|
|
cmdline[pos] = 0;
|
|
}
|
|
|
|
SECURITY_ATTRIBUTES sa = {0};
|
|
sa.nLength = sizeof(sa);
|
|
sa.bInheritHandle = TRUE;
|
|
HANDLE rd = NULL, wr = NULL;
|
|
if (!CreatePipe(&rd, &wr, &sa, 0))
|
|
return -1;
|
|
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
|
|
|
|
STARTUPINFOA si = {0};
|
|
si.cb = sizeof(si);
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
si.hStdOutput = wr;
|
|
si.hStdError = wr;
|
|
HANDLE nul = CreateFileA("NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
&sa, OPEN_EXISTING, 0, NULL);
|
|
si.hStdInput = nul != INVALID_HANDLE_VALUE ? nul : GetStdHandle(STD_INPUT_HANDLE);
|
|
PROCESS_INFORMATION pi = {0};
|
|
|
|
char *cmd_mut = _strdup(cmdline);
|
|
if (!cmd_mut) {
|
|
CloseHandle(rd);
|
|
CloseHandle(wr);
|
|
return -1;
|
|
}
|
|
BOOL ok = CreateProcessA(NULL, cmd_mut, NULL, NULL, TRUE, 0, NULL,
|
|
cwd && cwd[0] ? cwd : NULL, &si, &pi);
|
|
free(cmd_mut);
|
|
CloseHandle(wr);
|
|
if (nul != INVALID_HANDLE_VALUE)
|
|
CloseHandle(nul);
|
|
if (!ok) {
|
|
CloseHandle(rd);
|
|
return -1;
|
|
}
|
|
|
|
size_t cap = 4096, len = 0;
|
|
char *buf = malloc(cap);
|
|
if (!buf) {
|
|
CloseHandle(rd);
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
return -1;
|
|
}
|
|
buf[0] = 0;
|
|
int truncated = 0, killed = 0;
|
|
double deadline = os_now_ms() + run_timeout_s() * 1000.0;
|
|
for (;;) {
|
|
DWORD avail = 0;
|
|
if (!PeekNamedPipe(rd, NULL, 0, NULL, &avail, NULL))
|
|
break; /* writer closed */
|
|
if (avail > 0) {
|
|
char tmp[4096];
|
|
DWORD n = 0;
|
|
if (!ReadFile(rd, tmp, sizeof(tmp), &n, NULL) || n == 0)
|
|
break;
|
|
cap_append(&buf, &len, &cap, max_out, tmp, n, &truncated);
|
|
continue;
|
|
}
|
|
if (WaitForSingleObject(pi.hProcess, 50) == WAIT_OBJECT_0) {
|
|
/* drain what is left */
|
|
while (PeekNamedPipe(rd, NULL, 0, NULL, &avail, NULL) && avail > 0) {
|
|
char tmp[4096];
|
|
DWORD n = 0;
|
|
if (!ReadFile(rd, tmp, sizeof(tmp), &n, NULL) || n == 0)
|
|
break;
|
|
cap_append(&buf, &len, &cap, max_out, tmp, n, &truncated);
|
|
}
|
|
break;
|
|
}
|
|
if ((g_run_cancel && *g_run_cancel) || os_now_ms() > deadline) {
|
|
killed = (g_run_cancel && *g_run_cancel) ? 2 : 1;
|
|
TerminateProcess(pi.hProcess, killed == 1 ? 124 : 130);
|
|
break;
|
|
}
|
|
}
|
|
CloseHandle(rd);
|
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
|
DWORD code = 1;
|
|
GetExitCodeProcess(pi.hProcess, &code);
|
|
if (truncated)
|
|
cap_note(&buf, &len, &cap, "\n...(output truncated)\n");
|
|
if (killed == 1) {
|
|
char note[96];
|
|
snprintf(note, sizeof(note), "\n[cst: killed after %ds timeout]\n",
|
|
run_timeout_s());
|
|
cap_note(&buf, &len, &cap, note);
|
|
code = 124;
|
|
} else if (killed == 2) {
|
|
cap_note(&buf, &len, &cap, "\n[cst: cancelled]\n");
|
|
code = 130;
|
|
}
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
buf[len] = 0;
|
|
r->out = buf;
|
|
r->out_len = len;
|
|
r->exit_code = (int)code;
|
|
return 0;
|
|
}
|
|
|
|
int os_run_capture_ex(const char *cwd, const char *const argv[],
|
|
size_t max_out, os_run_result_t *r,
|
|
os_child_hook_fn child_hook, void *ud) {
|
|
(void)child_hook;
|
|
(void)ud;
|
|
return os_run_capture(cwd, argv, max_out, r);
|
|
}
|
|
#else
|
|
int os_run_capture(const char *cwd, const char *const argv[], size_t max_out,
|
|
os_run_result_t *r) {
|
|
return os_run_capture_ex(cwd, argv, max_out, r, NULL, NULL);
|
|
}
|
|
|
|
int os_run_capture_ex(const char *cwd, const char *const argv[],
|
|
size_t max_out, os_run_result_t *r,
|
|
os_child_hook_fn child_hook, void *ud) {
|
|
if (!argv || !argv[0] || !r)
|
|
return -1;
|
|
if (max_out == 0)
|
|
max_out = 512 * 1024;
|
|
memset(r, 0, sizeof(*r));
|
|
|
|
int pipefd[2];
|
|
if (pipe(pipefd) != 0)
|
|
return -1;
|
|
pid_t pid = fork();
|
|
if (pid < 0) {
|
|
close(pipefd[0]);
|
|
close(pipefd[1]);
|
|
return -1;
|
|
}
|
|
if (pid == 0) {
|
|
setpgid(0, 0);
|
|
close(pipefd[0]);
|
|
int devnull = open("/dev/null", O_RDONLY);
|
|
if (devnull >= 0) {
|
|
dup2(devnull, STDIN_FILENO);
|
|
close(devnull);
|
|
}
|
|
dup2(pipefd[1], STDOUT_FILENO);
|
|
dup2(pipefd[1], STDERR_FILENO);
|
|
close(pipefd[1]);
|
|
if (cwd && cwd[0] && chdir(cwd) != 0)
|
|
_exit(127);
|
|
if (child_hook && child_hook(ud) != 0) {
|
|
static const char msg[] = "[cst: sandbox setup failed; command not "
|
|
"run]\n";
|
|
ssize_t w = write(STDERR_FILENO, msg, sizeof(msg) - 1);
|
|
(void)w;
|
|
_exit(126);
|
|
}
|
|
execvp(argv[0], (char *const *)argv);
|
|
_exit(127);
|
|
}
|
|
setpgid(pid, pid); /* also in parent: avoid racing the child */
|
|
close(pipefd[1]);
|
|
size_t cap = 4096, len = 0;
|
|
char *buf = malloc(cap);
|
|
if (!buf) {
|
|
close(pipefd[0]);
|
|
kill(-pid, SIGKILL);
|
|
waitpid(pid, NULL, 0);
|
|
return -1;
|
|
}
|
|
buf[0] = 0;
|
|
int truncated = 0, killed = 0;
|
|
int status = 0, reaped = 0, bg_killed = 0;
|
|
double reaped_at = 0, killed_at = 0;
|
|
double deadline = os_now_ms() + run_timeout_s() * 1000.0;
|
|
for (;;) {
|
|
struct pollfd pfd = {pipefd[0], POLLIN, 0};
|
|
int pr = poll(&pfd, 1, 100);
|
|
if (pr < 0 && errno != EINTR)
|
|
break;
|
|
if (pr > 0) {
|
|
char tmp[4096];
|
|
ssize_t n = read(pipefd[0], tmp, sizeof(tmp));
|
|
if (n == 0)
|
|
break; /* every writer closed */
|
|
if (n > 0)
|
|
cap_append(&buf, &len, &cap, max_out, tmp, (size_t)n, &truncated);
|
|
else if (errno != EINTR && errno != EAGAIN)
|
|
break;
|
|
}
|
|
double now = os_now_ms();
|
|
if (!reaped && waitpid(pid, &status, WNOHANG) == pid) {
|
|
reaped = 1;
|
|
reaped_at = now;
|
|
}
|
|
/* The shell exited but a background child still holds the pipe
|
|
* (`server &`): give it a moment, then end the group. */
|
|
if (reaped && !killed && !bg_killed && now - reaped_at > 1000) {
|
|
kill(-pid, SIGKILL);
|
|
bg_killed = 1;
|
|
killed_at = now;
|
|
}
|
|
/* Checked on every pass, so a chatty command still stops promptly. */
|
|
if (!reaped && !killed &&
|
|
((g_run_cancel && *g_run_cancel) || now > deadline)) {
|
|
killed = (g_run_cancel && *g_run_cancel) ? 2 : 1;
|
|
killed_at = now;
|
|
kill(-pid, SIGTERM);
|
|
}
|
|
if (killed && !reaped && now - killed_at > 1000)
|
|
kill(-pid, SIGKILL);
|
|
/* a writer that left the process group can keep the pipe open */
|
|
if ((killed || bg_killed) && now - killed_at > 3000)
|
|
break;
|
|
}
|
|
close(pipefd[0]);
|
|
if (pid > 0 && !reaped) {
|
|
if (killed)
|
|
kill(-pid, SIGKILL);
|
|
waitpid(pid, &status, 0);
|
|
}
|
|
if (truncated)
|
|
cap_note(&buf, &len, &cap, "\n...(output truncated)\n");
|
|
if (bg_killed)
|
|
cap_note(&buf, &len, &cap,
|
|
"\n[cst: background processes left running were stopped]\n");
|
|
r->out = buf;
|
|
r->out_len = len;
|
|
r->exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : 1;
|
|
if (killed == 1) {
|
|
char note[96];
|
|
snprintf(note, sizeof(note), "\n[cst: killed after %ds timeout]\n",
|
|
run_timeout_s());
|
|
cap_note(&r->out, &r->out_len, &cap, note);
|
|
r->exit_code = 124;
|
|
} else if (killed == 2) {
|
|
cap_note(&r->out, &r->out_len, &cap, "\n[cst: cancelled]\n");
|
|
r->exit_code = 130;
|
|
}
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
int os_run_shell_c(const char *cwd, const char *cmd, size_t max_out,
|
|
os_run_result_t *r) {
|
|
return os_run_shell_ex(cwd, cmd, max_out, r, NULL, NULL);
|
|
}
|
|
|
|
int os_run_shell_ex(const char *cwd, const char *cmd, size_t max_out,
|
|
os_run_result_t *r, os_child_hook_fn child_hook,
|
|
void *ud) {
|
|
if (!cmd || !r)
|
|
return -1;
|
|
char shell[OS_PATH_MAX];
|
|
if (os_resolve_shell(shell, sizeof(shell)) != 0)
|
|
return -1;
|
|
|
|
/* busybox: busybox sh -c cmd ; /bin/sh: sh -c cmd */
|
|
int is_busybox = 0;
|
|
{
|
|
const char *base = shell;
|
|
const char *s = strrchr(shell, '/');
|
|
#ifdef _WIN32
|
|
const char *s2 = strrchr(shell, '\\');
|
|
if (s2 && (!s || s2 > s))
|
|
s = s2;
|
|
#endif
|
|
if (s)
|
|
base = s + 1;
|
|
if (strncmp(base, "busybox", 7) == 0)
|
|
is_busybox = 1;
|
|
}
|
|
|
|
if (is_busybox) {
|
|
const char *argv[] = {shell, "sh", "-c", cmd, NULL};
|
|
return os_run_capture_ex(cwd, argv, max_out, r, child_hook, ud);
|
|
} else {
|
|
const char *argv[] = {shell, "-c", cmd, NULL};
|
|
return os_run_capture_ex(cwd, argv, max_out, r, child_hook, ud);
|
|
}
|
|
}
|
|
|
|
void os_localtime(const time_t *t, struct tm *out) {
|
|
if (!t || !out)
|
|
return;
|
|
#ifdef _WIN32
|
|
localtime_s(out, t);
|
|
#else
|
|
localtime_r(t, out);
|
|
#endif
|
|
}
|
|
|
|
int os_getpid(void) {
|
|
#ifdef _WIN32
|
|
return (int)GetCurrentProcessId();
|
|
#else
|
|
return (int)getpid();
|
|
#endif
|
|
}
|
|
|
|
int os_chmod_private(const char *path) {
|
|
if (!path)
|
|
return -1;
|
|
#ifdef _WIN32
|
|
(void)path;
|
|
return 0;
|
|
#else
|
|
return chmod(path, 0600);
|
|
#endif
|
|
}
|