os_temp_dir() used $TMPDIR or "/tmp" without checking either. On Termux
/tmp usually does not exist (or SELinux denies it) unless TMPDIR points at
$PREFIX/tmp, so plugin and MCP temp files failed and shell children that
need a temp file (heredocs) broke.
* os_temp_dir() picks, once, the first of $TMPDIR, $PREFIX/tmp, /tmp,
/var/tmp and ~/.cache/cst-agent/tmp where a file can really be
created; if none works it keeps the old answer
* os_temp_dir_export() at startup writes the choice back to TMPDIR, so
shell children and the sandbox's writable temp dir agree
* tests/test_tmpdir.c covers the fallbacks in fresh forks
1160 lines
28 KiB
C
1160 lines
28 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 "os_internal.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 <dirent.h>
|
|
#include <fcntl.h>
|
|
#include <poll.h>
|
|
#include <pthread.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;
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
int os_realpath(const char *path, char *out, size_t out_sz) {
|
|
if (!path || !out || out_sz < 2)
|
|
return -1;
|
|
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;
|
|
}
|
|
|
|
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;
|
|
return path[rl] == '\0' || 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);
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
struct os_dir {
|
|
DIR *d;
|
|
char path[OS_PATH_MAX];
|
|
os_dirent_t ent;
|
|
};
|
|
|
|
os_dir_t *os_dir_open(const char *path) {
|
|
if (!path)
|
|
return NULL;
|
|
os_dir_t *d = calloc(1, sizeof(*d));
|
|
if (!d)
|
|
return NULL;
|
|
snprintf(d->path, sizeof(d->path), "%s", path);
|
|
d->d = opendir(path);
|
|
if (!d->d) {
|
|
free(d);
|
|
return NULL;
|
|
}
|
|
return d;
|
|
}
|
|
|
|
const os_dirent_t *os_dir_next(os_dir_t *d) {
|
|
struct dirent *de;
|
|
while (d && (de = readdir(d->d)) != NULL) {
|
|
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
|
|
continue;
|
|
d->ent.name = de->d_name;
|
|
d->ent.is_dir = d->ent.is_link = 0;
|
|
#ifdef DT_DIR
|
|
if (de->d_type != DT_UNKNOWN) {
|
|
d->ent.is_dir = de->d_type == DT_DIR;
|
|
d->ent.is_link = de->d_type == DT_LNK;
|
|
return &d->ent;
|
|
}
|
|
#endif
|
|
char p[OS_PATH_MAX];
|
|
struct stat st;
|
|
if (snprintf(p, sizeof(p), "%s/%s", d->path, de->d_name) <
|
|
(int)sizeof(p) &&
|
|
lstat(p, &st) == 0) {
|
|
d->ent.is_dir = S_ISDIR(st.st_mode);
|
|
d->ent.is_link = S_ISLNK(st.st_mode);
|
|
}
|
|
return &d->ent;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
void os_dir_close(os_dir_t *d) {
|
|
if (!d)
|
|
return;
|
|
closedir(d->d);
|
|
free(d);
|
|
}
|
|
|
|
int os_lstat(const char *path, os_stat_t *st) {
|
|
struct stat s;
|
|
memset(st, 0, sizeof(*st));
|
|
if (!path || lstat(path, &s) != 0)
|
|
return -1;
|
|
st->is_dir = S_ISDIR(s.st_mode);
|
|
st->is_reg = S_ISREG(s.st_mode);
|
|
st->is_link = S_ISLNK(s.st_mode);
|
|
st->size = (long long)s.st_size;
|
|
st->mtime = (double)s.st_mtime;
|
|
return 0;
|
|
}
|
|
|
|
int os_rename_replace(const char *from, const char *to) {
|
|
return rename(from, to) == 0 ? 0 : -1;
|
|
}
|
|
#endif
|
|
|
|
int os_config_dir(char *out, size_t out_sz) {
|
|
if (!out || out_sz < 8)
|
|
return -1;
|
|
#ifdef _WIN32
|
|
const char *base = getenv("XDG_CONFIG_HOME");
|
|
if (!base || !os_path_is_abs(base))
|
|
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);
|
|
os_path_norm_slashes(out);
|
|
#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;
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
/* $TMPDIR is not always usable: Termux has no /tmp unless TMPDIR points at
|
|
* $PREFIX/tmp, and Android may deny /tmp or /data/local/tmp outright. Take
|
|
* the first candidate where a file can actually be created. */
|
|
static char g_tmp[OS_PATH_MAX];
|
|
static pthread_once_t g_tmp_once = PTHREAD_ONCE_INIT;
|
|
|
|
static int tmp_writable(const char *dir) {
|
|
char probe[OS_PATH_MAX];
|
|
struct stat st;
|
|
if (!dir || dir[0] != '/' || stat(dir, &st) != 0 || !S_ISDIR(st.st_mode))
|
|
return 0;
|
|
int n = snprintf(probe, sizeof(probe), "%s/.cst-probe-XXXXXX", dir);
|
|
if (n < 0 || (size_t)n >= sizeof(probe))
|
|
return 0;
|
|
int fd = mkstemp(probe);
|
|
if (fd < 0)
|
|
return 0;
|
|
close(fd);
|
|
unlink(probe);
|
|
return 1;
|
|
}
|
|
|
|
static void tmp_trim(char *s) {
|
|
size_t n = strlen(s);
|
|
while (n > 1 && s[n - 1] == '/')
|
|
s[--n] = 0;
|
|
}
|
|
|
|
static void tmp_pick(void) {
|
|
char cand[5][OS_PATH_MAX];
|
|
int nc = 0;
|
|
const char *env = getenv("TMPDIR");
|
|
const char *prefix = getenv("PREFIX");
|
|
const char *home = getenv("HOME");
|
|
const char *cache = getenv("XDG_CACHE_HOME");
|
|
if (env && env[0])
|
|
snprintf(cand[nc++], OS_PATH_MAX, "%s", env);
|
|
if (prefix && prefix[0] == '/')
|
|
snprintf(cand[nc++], OS_PATH_MAX, "%s/tmp", prefix);
|
|
snprintf(cand[nc++], OS_PATH_MAX, "/tmp");
|
|
snprintf(cand[nc++], OS_PATH_MAX, "/var/tmp");
|
|
if (cache && cache[0] == '/')
|
|
snprintf(cand[nc++], OS_PATH_MAX, "%s/cst-agent/tmp", cache);
|
|
else if (home && home[0] == '/')
|
|
snprintf(cand[nc++], OS_PATH_MAX, "%s/.cache/cst-agent/tmp", home);
|
|
for (int i = 0; i < nc; i++) {
|
|
tmp_trim(cand[i]);
|
|
if (i == nc - 1)
|
|
os_mkdir_p(cand[i], 0700);
|
|
if (tmp_writable(cand[i])) {
|
|
memcpy(g_tmp, cand[i], strlen(cand[i]) + 1);
|
|
return;
|
|
}
|
|
}
|
|
/* nothing works: keep the old answer so callers fail as before */
|
|
memcpy(g_tmp, cand[0], strlen(cand[0]) + 1);
|
|
}
|
|
#endif
|
|
|
|
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
|
|
pthread_once(&g_tmp_once, tmp_pick);
|
|
int n = snprintf(out, out_sz, "%s", g_tmp);
|
|
return n < 0 || (size_t)n >= out_sz ? -1 : 0;
|
|
#endif
|
|
}
|
|
|
|
void os_temp_dir_export(void) {
|
|
#ifndef _WIN32
|
|
char d[OS_PATH_MAX];
|
|
const char *t = getenv("TMPDIR");
|
|
if (os_temp_dir(d, sizeof(d)) == 0 && (!t || strcmp(t, d) != 0))
|
|
setenv("TMPDIR", d, 1);
|
|
#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
|
|
}
|
|
|
|
/* Per thread: parallel tool runs and sub-agents each watch their own flag. */
|
|
static _Thread_local volatile int *g_run_cancel;
|
|
static _Thread_local 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; }
|
|
|
|
int os_get_run_timeout(void) { return g_run_timeout_s; }
|
|
|
|
int os_run_timeout_effective(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. */
|
|
int os_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;
|
|
}
|
|
|
|
void os_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;
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
static void cloexec(int fd) {
|
|
int fl = fcntl(fd, F_GETFD);
|
|
if (fl >= 0)
|
|
fcntl(fd, F_SETFD, fl | FD_CLOEXEC);
|
|
}
|
|
|
|
/* Every pipe end is close-on-exec and pipe+fork happen under one lock, so a
|
|
* child forked on one thread never inherits the pipes of a spawn running on
|
|
* another (which would keep that pipe open past its process's exit). */
|
|
static pthread_mutex_t g_spawn_mu = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
static int pipe_cloexec(int fd[2]) {
|
|
if (pipe(fd) != 0)
|
|
return -1;
|
|
cloexec(fd[0]);
|
|
cloexec(fd[1]);
|
|
return 0;
|
|
}
|
|
|
|
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];
|
|
pthread_mutex_lock(&g_spawn_mu);
|
|
if (pipe_cloexec(pipefd) != 0) {
|
|
pthread_mutex_unlock(&g_spawn_mu);
|
|
return -1;
|
|
}
|
|
pid_t pid = fork();
|
|
if (pid < 0) {
|
|
pthread_mutex_unlock(&g_spawn_mu);
|
|
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);
|
|
}
|
|
pthread_mutex_unlock(&g_spawn_mu);
|
|
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() + os_run_timeout_effective() * 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)
|
|
os_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)
|
|
os_cap_note(&buf, &len, &cap, "\n...(output truncated)\n");
|
|
if (bg_killed)
|
|
os_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",
|
|
os_run_timeout_effective());
|
|
os_cap_note(&r->out, &r->out_len, &cap, note);
|
|
r->exit_code = 124;
|
|
} else if (killed == 2) {
|
|
os_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);
|
|
}
|
|
|
|
/* busybox: busybox sh -c cmd ; /bin/sh: sh -c cmd. argv needs 5 slots and
|
|
* points into shell. */
|
|
static int shell_argv(char *shell, size_t shell_sz, const char *cmd,
|
|
const char *argv[5]) {
|
|
if (!cmd || os_resolve_shell(shell, shell_sz) != 0)
|
|
return -1;
|
|
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;
|
|
int i = 0;
|
|
argv[i++] = shell;
|
|
if (strncmp(base, "busybox", 7) == 0)
|
|
argv[i++] = "sh";
|
|
argv[i++] = "-c";
|
|
argv[i++] = cmd;
|
|
argv[i] = NULL;
|
|
return 0;
|
|
}
|
|
|
|
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) {
|
|
char shell[OS_PATH_MAX];
|
|
const char *argv[5];
|
|
if (!r || shell_argv(shell, sizeof(shell), cmd, argv) != 0)
|
|
return -1;
|
|
return os_run_capture_ex(cwd, argv, max_out, r, child_hook, ud);
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
int os_run_shell_token(const char *cwd, const char *cmd, size_t max_out,
|
|
os_run_result_t *r, void *token,
|
|
const char *const env_add[]) {
|
|
char shell[OS_PATH_MAX];
|
|
const char *argv[5];
|
|
if (!r || shell_argv(shell, sizeof(shell), cmd, argv) != 0)
|
|
return -1;
|
|
return os_run_capture_token(cwd, argv, max_out, r, token, env_add);
|
|
}
|
|
#endif
|
|
|
|
#ifndef _WIN32
|
|
int os_console_init(void) { return 0; }
|
|
|
|
char *os_output_to_utf8(const char *s, size_t n, size_t *out_len) {
|
|
(void)s;
|
|
(void)n;
|
|
(void)out_len;
|
|
return NULL;
|
|
}
|
|
#endif
|
|
|
|
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
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
int os_chmod_private(const char *path) {
|
|
if (!path)
|
|
return -1;
|
|
return chmod(path, 0600);
|
|
}
|
|
#endif
|
|
|
|
typedef struct {
|
|
os_task_fn fn;
|
|
void *arg;
|
|
} os_task_t;
|
|
|
|
#ifdef _WIN32
|
|
static DWORD WINAPI task_main(LPVOID p) {
|
|
os_task_t *t = p;
|
|
t->fn(t->arg);
|
|
return 0;
|
|
}
|
|
#else
|
|
static void *task_main(void *p) {
|
|
os_task_t *t = p;
|
|
t->fn(t->arg);
|
|
return NULL;
|
|
}
|
|
#endif
|
|
|
|
void os_run_parallel(os_task_fn fn, void *const args[], int n) {
|
|
if (n <= 0)
|
|
return;
|
|
if (n == 1) {
|
|
fn(args[0]);
|
|
return;
|
|
}
|
|
os_task_t *tasks = calloc((size_t)n, sizeof(*tasks));
|
|
#ifdef _WIN32
|
|
HANDLE *th = calloc((size_t)n, sizeof(*th));
|
|
#else
|
|
pthread_t *th = calloc((size_t)n, sizeof(*th));
|
|
char *started = calloc((size_t)n, 1);
|
|
#endif
|
|
if (!tasks || !th
|
|
#ifndef _WIN32
|
|
|| !started
|
|
#endif
|
|
) {
|
|
for (int i = 0; i < n; i++)
|
|
fn(args[i]);
|
|
free(tasks);
|
|
free(th);
|
|
#ifndef _WIN32
|
|
free(started);
|
|
#endif
|
|
return;
|
|
}
|
|
for (int i = 0; i < n; i++) {
|
|
tasks[i].fn = fn;
|
|
tasks[i].arg = args[i];
|
|
#ifdef _WIN32
|
|
th[i] = CreateThread(NULL, 0, task_main, &tasks[i], 0, NULL);
|
|
if (!th[i])
|
|
fn(args[i]);
|
|
#else
|
|
if (pthread_create(&th[i], NULL, task_main, &tasks[i]) == 0)
|
|
started[i] = 1;
|
|
else
|
|
fn(args[i]);
|
|
#endif
|
|
}
|
|
for (int i = 0; i < n; i++) {
|
|
#ifdef _WIN32
|
|
if (th[i]) {
|
|
WaitForSingleObject(th[i], INFINITE);
|
|
CloseHandle(th[i]);
|
|
}
|
|
#else
|
|
if (started[i])
|
|
pthread_join(th[i], NULL);
|
|
#endif
|
|
}
|
|
#ifndef _WIN32
|
|
free(started);
|
|
#endif
|
|
free(th);
|
|
free(tasks);
|
|
}
|
|
|
|
int os_cancel_requested(void) { return g_run_cancel && *g_run_cancel; }
|
|
|
|
volatile int *os_get_cancel_flag(void) { return g_run_cancel; }
|
|
|
|
#ifndef _WIN32
|
|
int os_proc_spawn(const char *cwd, const char *const argv[],
|
|
const char *const env_add[], const char *stderr_path,
|
|
os_proc_t *p) {
|
|
memset(p, 0, sizeof(*p));
|
|
p->in_fd = p->out_fd = -1;
|
|
if (!argv || !argv[0])
|
|
return -1;
|
|
signal(SIGPIPE, SIG_IGN);
|
|
int in[2], out[2];
|
|
pthread_mutex_lock(&g_spawn_mu);
|
|
if (pipe_cloexec(in) != 0) {
|
|
pthread_mutex_unlock(&g_spawn_mu);
|
|
return -1;
|
|
}
|
|
if (pipe_cloexec(out) != 0) {
|
|
pthread_mutex_unlock(&g_spawn_mu);
|
|
close(in[0]);
|
|
close(in[1]);
|
|
return -1;
|
|
}
|
|
pid_t pid = fork();
|
|
if (pid < 0) {
|
|
pthread_mutex_unlock(&g_spawn_mu);
|
|
close(in[0]);
|
|
close(in[1]);
|
|
close(out[0]);
|
|
close(out[1]);
|
|
return -1;
|
|
}
|
|
if (pid == 0) {
|
|
setpgid(0, 0);
|
|
dup2(in[0], STDIN_FILENO);
|
|
dup2(out[1], STDOUT_FILENO);
|
|
int err = open(stderr_path ? stderr_path : "/dev/null",
|
|
O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
|
if (err >= 0) {
|
|
dup2(err, STDERR_FILENO);
|
|
close(err);
|
|
}
|
|
close(in[0]);
|
|
close(in[1]);
|
|
close(out[0]);
|
|
close(out[1]);
|
|
signal(SIGPIPE, SIG_DFL);
|
|
for (int i = 0; env_add && env_add[i]; i++)
|
|
putenv((char *)env_add[i]);
|
|
if (cwd && cwd[0] && chdir(cwd) != 0)
|
|
_exit(127);
|
|
execvp(argv[0], (char *const *)argv);
|
|
_exit(127);
|
|
}
|
|
pthread_mutex_unlock(&g_spawn_mu);
|
|
setpgid(pid, pid);
|
|
close(in[0]);
|
|
close(out[1]);
|
|
p->pid = (int)pid;
|
|
p->in_fd = in[1];
|
|
p->out_fd = out[0];
|
|
return 0;
|
|
}
|
|
|
|
int os_proc_write(os_proc_t *p, const char *data, size_t n) {
|
|
while (n > 0) {
|
|
if (p->in_fd < 0)
|
|
return -1;
|
|
ssize_t w = write(p->in_fd, data, n);
|
|
if (w < 0 && errno == EINTR)
|
|
continue;
|
|
if (w <= 0)
|
|
return -1;
|
|
data += w;
|
|
n -= (size_t)w;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
long os_proc_read(os_proc_t *p, char *buf, size_t n, int timeout_ms) {
|
|
for (;;) {
|
|
if (p->out_fd < 0)
|
|
return -1;
|
|
struct pollfd pfd = {p->out_fd, POLLIN, 0};
|
|
int pr = poll(&pfd, 1, timeout_ms);
|
|
if (pr < 0 && errno == EINTR)
|
|
continue;
|
|
if (pr == 0)
|
|
return -2;
|
|
ssize_t r = read(p->out_fd, buf, n);
|
|
if (r < 0 && errno == EINTR)
|
|
continue;
|
|
return (long)r;
|
|
}
|
|
}
|
|
|
|
void os_proc_close_stdin(os_proc_t *p) {
|
|
if (p->in_fd >= 0) {
|
|
close(p->in_fd);
|
|
p->in_fd = -1;
|
|
}
|
|
}
|
|
|
|
void os_proc_kill(os_proc_t *p) {
|
|
os_proc_close_stdin(p);
|
|
if (p->pid > 0) {
|
|
int status;
|
|
/* polite first: most servers exit when stdin closes */
|
|
for (int i = 0; i < 10; i++) {
|
|
if (waitpid(p->pid, &status, WNOHANG) == p->pid) {
|
|
p->pid = 0;
|
|
break;
|
|
}
|
|
struct timespec ts = {0, 50 * 1000000L};
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
if (p->pid > 0) {
|
|
kill(-p->pid, SIGTERM);
|
|
for (int i = 0; i < 20; i++) {
|
|
if (waitpid(p->pid, &status, WNOHANG) == p->pid) {
|
|
p->pid = 0;
|
|
break;
|
|
}
|
|
struct timespec ts = {0, 50 * 1000000L};
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
}
|
|
if (p->pid > 0) {
|
|
kill(-p->pid, SIGKILL);
|
|
waitpid(p->pid, &status, 0);
|
|
p->pid = 0;
|
|
}
|
|
}
|
|
if (p->out_fd >= 0) {
|
|
close(p->out_fd);
|
|
p->out_fd = -1;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* --- UTF-8 ---------------------------------------------------------------
|
|
* See the declaration in os_compat.h for why wcrtomb() is not used. */
|
|
int os_utf8_encode(unsigned long cp, char out[4]) {
|
|
if (cp < 0x80) {
|
|
out[0] = (char)cp;
|
|
return 1;
|
|
}
|
|
if (cp < 0x800) {
|
|
out[0] = (char)(0xC0 | (cp >> 6));
|
|
out[1] = (char)(0x80 | (cp & 0x3F));
|
|
return 2;
|
|
}
|
|
if (cp >= 0xD800 && cp <= 0xDFFF)
|
|
return 0; /* lone surrogate half from a 16-bit wchar_t */
|
|
if (cp < 0x10000) {
|
|
out[0] = (char)(0xE0 | (cp >> 12));
|
|
out[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
|
|
out[2] = (char)(0x80 | (cp & 0x3F));
|
|
return 3;
|
|
}
|
|
if (cp <= 0x10FFFF) {
|
|
out[0] = (char)(0xF0 | (cp >> 18));
|
|
out[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
|
|
out[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
|
|
out[3] = (char)(0x80 | (cp & 0x3F));
|
|
return 4;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* See the declaration in os_compat.h for why mbrtowc() is not used. */
|
|
int os_utf8_decode(const char *s, size_t avail, unsigned long *cp) {
|
|
const unsigned char *p = (const unsigned char *)s;
|
|
int need, i;
|
|
unsigned long v;
|
|
static const unsigned long MINV[4] = {0, 0x80, 0x800, 0x10000};
|
|
|
|
if (!s || !cp || avail == 0)
|
|
return 0;
|
|
if (p[0] < 0x80) {
|
|
*cp = p[0];
|
|
return 1;
|
|
}
|
|
if ((p[0] & 0xE0) == 0xC0) {
|
|
need = 1;
|
|
v = p[0] & 0x1Fu;
|
|
} else if ((p[0] & 0xF0) == 0xE0) {
|
|
need = 2;
|
|
v = p[0] & 0x0Fu;
|
|
} else if ((p[0] & 0xF8) == 0xF0) {
|
|
need = 3;
|
|
v = p[0] & 0x07u;
|
|
} else {
|
|
return 0; /* not a UTF-8 lead byte */
|
|
}
|
|
if (avail < (size_t)need + 1)
|
|
return 0; /* truncated */
|
|
for (i = 1; i <= need; i++) {
|
|
if ((p[i] & 0xC0) != 0x80)
|
|
return 0; /* bad continuation byte */
|
|
v = (v << 6) | (p[i] & 0x3Fu);
|
|
}
|
|
if (v < MINV[need] || (v >= 0xD800 && v <= 0xDFFF) || v > 0x10FFFF)
|
|
return 0; /* overlong, surrogate, or out of range */
|
|
*cp = v;
|
|
return need + 1;
|
|
}
|
|
|
|
/* --- libc gaps on Windows -----------------------------------------------
|
|
* strcasestr() is a GNU extension and setenv()/unsetenv() are POSIX; glibc
|
|
* has all three, MinGW-w64 has none of them. src/tui.c needs them (command
|
|
* filtering and ESCDELAY), so the Windows build supplies its own.
|
|
*
|
|
* The case folding here is ASCII-only, unlike glibc's locale-aware version.
|
|
* That is deliberate: these are used to match command and setting names,
|
|
* which are ASCII.
|
|
*/
|
|
#if defined(_WIN32)
|
|
|
|
static int ascii_lower(int c) {
|
|
return (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c;
|
|
}
|
|
|
|
char *strcasestr(const char *haystack, const char *needle) {
|
|
if (!haystack || !needle)
|
|
return NULL;
|
|
if (!*needle)
|
|
return (char *)haystack;
|
|
for (; *haystack; haystack++) {
|
|
const char *h = haystack;
|
|
const char *n = needle;
|
|
while (*h && *n && ascii_lower((unsigned char)*h) == ascii_lower((unsigned char)*n)) {
|
|
h++;
|
|
n++;
|
|
}
|
|
if (!*n)
|
|
return (char *)haystack;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
int setenv(const char *name, const char *value, int overwrite) {
|
|
if (!name || !*name || strchr(name, '='))
|
|
return -1;
|
|
if (!overwrite && getenv(name))
|
|
return 0;
|
|
return _putenv_s(name, value ? value : "");
|
|
}
|
|
|
|
int unsetenv(const char *name) {
|
|
if (!name || !*name || strchr(name, '='))
|
|
return -1;
|
|
return _putenv_s(name, "");
|
|
}
|
|
|
|
#endif /* _WIN32 */
|