Files
CST-agent/src/os_compat.c
T
WHCandCursor 1f1fcba7ad Initial CST-Agent: portable Linux/Termux/Windows coding agent.
Vendored sashidown, OpenAI-compatible env (OPENAI_*), and bootstrap scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 22:42:32 +08:00

585 lines
13 KiB
C

#define _DEFAULT_SOURCE
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#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 <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.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_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 *home = getenv("HOME");
if (!home || !home[0])
return -1;
int n = snprintf(out, out_sz, "%s/.config/cst-agent", home);
#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];
ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
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
}
#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;
si.hStdInput = 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 (!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;
}
for (;;) {
char tmp[1024];
DWORD n = 0;
if (!ReadFile(rd, tmp, sizeof(tmp), &n, NULL) || n == 0)
break;
if (len + n + 1 > cap) {
size_t nc = cap * 2;
if (nc < len + n + 1)
nc = len + n + 1;
char *nb = realloc(buf, nc);
if (!nb)
break;
buf = nb;
cap = nc;
}
memcpy(buf + len, tmp, n);
len += n;
if (len >= max_out)
break;
}
CloseHandle(rd);
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD code = 1;
GetExitCodeProcess(pi.hProcess, &code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
buf[len] = 0;
r->out = buf;
r->out_len = len;
r->exit_code = (int)code;
return 0;
}
#else
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));
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) {
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
dup2(pipefd[1], STDERR_FILENO);
close(pipefd[1]);
if (cwd && cwd[0] && chdir(cwd) != 0)
_exit(127);
execvp(argv[0], (char *const *)argv);
_exit(127);
}
close(pipefd[1]);
size_t cap = 4096, len = 0;
char *buf = malloc(cap);
if (!buf) {
close(pipefd[0]);
waitpid(pid, NULL, 0);
return -1;
}
for (;;) {
if (len + 1024 > cap) {
cap *= 2;
char *nb = realloc(buf, cap);
if (!nb)
break;
buf = nb;
}
ssize_t n = read(pipefd[0], buf + len, 1024);
if (n < 0) {
if (errno == EINTR)
continue;
break;
}
if (n == 0)
break;
len += (size_t)n;
if (len >= max_out)
break;
}
close(pipefd[0]);
int status = 0;
waitpid(pid, &status, 0);
buf[len] = 0;
r->out = buf;
r->out_len = len;
r->exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : 1;
return 0;
}
#endif
int os_run_shell_c(const char *cwd, const char *cmd, size_t max_out,
os_run_result_t *r) {
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(cwd, argv, max_out, r);
} else {
const char *argv[] = {shell, "-c", cmd, NULL};
return os_run_capture(cwd, argv, max_out, r);
}
}
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
}