Files
Hermit/src/os_compat.c
T
WHC ef63f6d612
ci / linux (clang) (push) Canceled after 0s
ci / linux (gcc) (push) Canceled after 0s
ci / sanitizers (push) Canceled after 0s
ci / musl (push) Canceled after 0s
ci / macos (push) Canceled after 0s
OpenCode-style TUI, model probing, sandboxed auto approval
- TUI: centered home screen with logo, boxed input with mode line, sidebar
  (>=120 cols), Ctrl+P palette, Tab Build/Plan, Shift+Tab approval cycle,
  mouse (cards, cursor, pickers, wheel), /sidebar /mouse /approval
- /models probing: key check on entry (401 re-asks), API models in /model
- Shell sandbox in C: Linux Landlock (writes limited to workspace/tmp,
  ABI>=4 blocks TCP), macOS sandbox_init, rules fallback elsewhere
- Approval modes ask/auto/yolo (default auto); read-only command whitelist;
  ask to rerun outside the sandbox when a command looks blocked
- Upgradable bootstrap scripts (git pull --ff-only, rebuild, reinstall) and
  cst-agent-update; cst-agent --version
- tests: test_sandbox, test_models
2026-10-05 12:34:46 +08:00

796 lines
20 KiB
C

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