#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #ifdef __APPLE__ #include #include #endif #include "os_compat.h" #include "os_internal.h" #include #include #include #include #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #include #include #include #include #else #include #include #include #include #include #include #include #include #include #include #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 */