Files
Hermit/src/os_compat.c
T
WHC adf3e9f024
ci / linux (clang) (push) Failing after 31s
ci / linux (gcc) (push) Failing after 32s
ci / musl (push) Failing after 39s
ci / windows-cross (push) Canceled after 2m32s
v0.8.0: WebUI redesign, /web, workspace switch, 75% budget, no shell timeout
The front end is rebuilt on the design tokens the desktop app ships (surfaces,
color-mix borders, alpha hovers, accent): a 30-icon inline SVG sprite, dark and
light themes, layout switches as deep links (?side=0, ?panel=0, ?theme=light)
resolved before the first paint. The empty page is one line and leaves with the
first turn; the right rail keeps three cards - todo, file changes (+N -M) and
context (tokens, prompt cache hit rate). Both rails animate in and out, and a
collapsed rail widens its grid track to 0 instead of being removed, so the
transcript never slides into a zero-width column.

- src/webui.c: POST /api/workspace - re-root the agent at another directory
  (permission rules, metrics, AGENTS.md follow) and start a session there, as
  sessions live per directory; state also reports cached/cache_miss tokens
- src/agent_loop.c: cst_agent_set_workspace(), rebuilt prompt and re-measured
  budget; the auto-compact threshold is 75% of the window (was 60%)
- src/tui.c: /web forks a WebUI next to the TUI - separate process and a new
  session, because cst_webui_run() blocks and two writers corrupt one session
- src/os_compat.c, src/os_win.c: run_shell runs without a time limit unless
  CST_SHELL_TIMEOUT asks for one, so builds and long clones finish
- src/main.c, include/cst_webui.h: one CST_WEBUI_DEFAULT_PORT for --help, the
  docs and /web; Termux opens the browser through termux-open-url
- README.md, docs/webui.md, docs/env_openai.md: /web, /api/workspace, the new
  state fields, the 75% threshold and the layout deep links

tests/e2e_webui.sh green (permission round-trip, token paths, --web-dev);
make check green.
2026-10-09 11:50:13 +08:00

1161 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 0; /* no limit: a build, a test suite or a long clone must finish */
}
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;
int timeout_s = os_run_timeout_effective();
double deadline = timeout_s > 0 ? os_now_ms() + timeout_s * 1000.0 : 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) || (deadline > 0 && 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 */