Files
Hermit/src/os_win.c
T

939 lines
28 KiB
C

/* Windows side of os_compat: every string crossing the OS boundary is UTF-8
* inside cst-agent and UTF-16 at the Win32 W APIs. The embedded manifest also
* sets the process code page to UTF-8, so CRT calls (fopen, getenv, argv)
* see UTF-8 too. */
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include "os_compat.h"
#include "cst_util.h"
#include "os_internal.h"
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <aclapi.h>
#include <sddl.h>
wchar_t *os_utf8_to_wide(const char *s) {
if (!s)
return NULL;
int n = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0);
if (n <= 0)
return NULL;
wchar_t *w = malloc((size_t)n * sizeof(wchar_t));
if (w && MultiByteToWideChar(CP_UTF8, 0, s, -1, w, n) <= 0) {
free(w);
return NULL;
}
return w;
}
char *os_wide_to_utf8(const wchar_t *w) {
if (!w)
return NULL;
int n = WideCharToMultiByte(CP_UTF8, 0, w, -1, NULL, 0, NULL, NULL);
if (n <= 0)
return NULL;
char *s = malloc((size_t)n);
if (s && WideCharToMultiByte(CP_UTF8, 0, w, -1, s, n, NULL, NULL) <= 0) {
free(s);
return NULL;
}
return s;
}
static UINT g_old_cp, g_old_out_cp;
static void console_restore(void) {
if (g_old_cp)
SetConsoleCP(g_old_cp);
if (g_old_out_cp)
SetConsoleOutputCP(g_old_out_cp);
}
int os_console_init(void) {
static int done;
if (done)
return GetACP() == CP_UTF8 ? 0 : -1;
done = 1;
g_old_cp = GetConsoleCP();
g_old_out_cp = GetConsoleOutputCP();
if (g_old_cp || g_old_out_cp) {
SetConsoleCP(CP_UTF8);
SetConsoleOutputCP(CP_UTF8);
atexit(console_restore);
}
setlocale(LC_ALL, ".UTF-8");
/* children inherit these: Python otherwise writes the ANSI code page to pipes */
if (!getenv("PYTHONUTF8"))
_putenv_s("PYTHONUTF8", "1");
if (!getenv("PYTHONIOENCODING"))
_putenv_s("PYTHONIOENCODING", "utf-8");
return GetACP() == CP_UTF8 ? 0 : -1;
}
/* The system locale's code page. GetOEMCP()/GetACP() say 65001 once the
* manifest makes this process UTF-8, but children without one (cmd, most
* console tools) still write the system's OEM code page, e.g. 936. */
static UINT system_cp(LCTYPE which) {
wchar_t b[8];
if (!GetLocaleInfoW(LOCALE_SYSTEM_DEFAULT, which, b, 8))
return 0;
return (UINT)wcstoul(b, NULL, 10);
}
char *os_output_to_utf8(const char *s, size_t n, size_t *out_len) {
if (!s || cst_utf8_valid(s, n))
return NULL;
UINT cps[] = {GetConsoleOutputCP(), system_cp(LOCALE_IDEFAULTCODEPAGE),
system_cp(LOCALE_IDEFAULTANSICODEPAGE), GetOEMCP(), GetACP()};
for (size_t i = 0; i < sizeof(cps) / sizeof(cps[0]); i++) {
UINT cp = cps[i];
if (!cp || cp == CP_UTF8)
continue;
int wn = MultiByteToWideChar(cp, MB_ERR_INVALID_CHARS, s, (int)n, NULL, 0);
if (wn <= 0)
continue;
wchar_t *w = malloc((size_t)wn * sizeof(wchar_t));
if (!w)
return NULL;
MultiByteToWideChar(cp, 0, s, (int)n, w, wn);
int un = WideCharToMultiByte(CP_UTF8, 0, w, wn, NULL, 0, NULL, NULL);
char *u = un > 0 ? malloc((size_t)un + 1) : NULL;
if (u) {
WideCharToMultiByte(CP_UTF8, 0, w, wn, u, un, NULL, NULL);
u[un] = 0;
if (out_len)
*out_len = (size_t)un;
}
free(w);
return u;
}
return NULL;
}
/* --- paths --------------------------------------------------------------- */
/* UTF-8 path -> wide, with the \\?\ prefix once it gets near MAX_PATH so
* deep trees work without the LongPathsEnabled policy. */
static wchar_t *wpath(const char *p) {
wchar_t *w = os_utf8_to_wide(p);
if (!w || wcslen(w) < MAX_PATH - 12)
return w;
DWORD n = GetFullPathNameW(w, 0, NULL, NULL);
wchar_t *f = n ? malloc(n * sizeof(wchar_t)) : NULL;
wchar_t *out = n ? malloc((n + 8) * sizeof(wchar_t)) : NULL;
if (!f || !out || GetFullPathNameW(w, n, f, NULL) == 0) {
free(f);
free(out);
return w;
}
free(w);
if (wcsncmp(f, L"\\\\?\\", 4) == 0) {
wcscpy(out, f);
} else if (f[0] == L'\\' && f[1] == L'\\') { /* \\srv\share -> \\?\UNC\srv\share */
wcscpy(out, L"\\\\?\\UNC");
wcscat(out, f + 1);
} else {
wcscpy(out, L"\\\\?\\");
wcscat(out, f);
}
free(f);
return out;
}
/* Wide path from the OS -> UTF-8 with '/' separators, no \\?\ prefix and a
* lowercase drive letter (stable for prefix compares). */
static int path_out(const wchar_t *w, char *out, size_t out_sz) {
const char *lead = "";
if (wcsncmp(w, L"\\\\?\\UNC\\", 8) == 0) {
lead = "\\\\"; /* \\?\UNC\srv\share -> \\srv\share */
w += 8;
} else if (wcsncmp(w, L"\\\\?\\", 4) == 0) {
w += 4;
}
char *u = os_wide_to_utf8(w);
if (!u)
return -1;
int n = snprintf(out, out_sz, "%s%s", lead, u);
free(u);
if (n < 0 || (size_t)n >= out_sz)
return -1;
os_path_norm_slashes(out);
if (out[0] >= 'A' && out[0] <= 'Z' && out[1] == ':')
out[0] = (char)(out[0] - 'A' + 'a');
return 0;
}
int os_realpath(const char *path, char *out, size_t out_sz) {
if (!path || !path[0] || !out || out_sz < 2)
return -1;
wchar_t *w = wpath(path);
if (!w)
return -1;
HANDLE h = CreateFileW(w, 0,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
free(w);
if (h == INVALID_HANDLE_VALUE)
return -1;
wchar_t stackbuf[MAX_PATH + 16], *buf = stackbuf;
DWORD n = GetFinalPathNameByHandleW(h, buf, MAX_PATH + 16,
FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
if (n >= MAX_PATH + 16) {
buf = malloc((n + 1) * sizeof(wchar_t));
n = buf ? GetFinalPathNameByHandleW(h, buf, n + 1,
FILE_NAME_NORMALIZED | VOLUME_NAME_DOS)
: 0;
}
CloseHandle(h);
int rc = n ? path_out(buf, out, out_sz) : -1;
if (buf != stackbuf)
free(buf);
return rc;
}
/* NTFS names are case-insensitive: compare the prefix the way the OS does. */
int os_path_under(const char *root, const char *path) {
if (!root || !path)
return 0;
wchar_t *wr = os_utf8_to_wide(root), *wp = os_utf8_to_wide(path);
int ok = 0;
if (wr && wp) {
size_t rl = wcslen(wr);
if (wcslen(wp) >= rl &&
(rl == 0 || CompareStringOrdinal(wr, (int)rl, wp, (int)rl, TRUE) ==
CSTR_EQUAL)) {
wchar_t c = wp[rl];
ok = c == 0 || c == L'/' || c == L'\\' ||
(rl && (wr[rl - 1] == L'/' || wr[rl - 1] == L'\\'));
}
}
free(wr);
free(wp);
return ok;
}
static int reparse_is_link(DWORD attrs, DWORD tag) {
return (attrs & FILE_ATTRIBUTE_REPARSE_POINT) &&
(tag == IO_REPARSE_TAG_SYMLINK || tag == IO_REPARSE_TAG_MOUNT_POINT);
}
/* --- directories ------------------------------------------------------- */
struct os_dir {
HANDLE h;
int pending; /* fd holds an entry not yet returned */
WIN32_FIND_DATAW fd;
char name[MAX_PATH * 4];
os_dirent_t ent;
};
os_dir_t *os_dir_open(const char *path) {
if (!path || !path[0])
return NULL;
size_t L = strlen(path);
char *pat = malloc(L + 3);
if (!pat)
return NULL;
memcpy(pat, path, L);
if (path[L - 1] != '/' && path[L - 1] != '\\')
pat[L++] = '\\';
pat[L++] = '*';
pat[L] = 0;
wchar_t *w = wpath(pat);
free(pat);
os_dir_t *d = w ? calloc(1, sizeof(*d)) : NULL;
if (!d) {
free(w);
return NULL;
}
d->h = FindFirstFileExW(w, FindExInfoBasic, &d->fd, FindExSearchNameMatch,
NULL, FIND_FIRST_EX_LARGE_FETCH);
free(w);
if (d->h == INVALID_HANDLE_VALUE) {
DWORD e = GetLastError();
if (e != ERROR_FILE_NOT_FOUND) { /* an empty drive root has no entries */
free(d);
return NULL;
}
} else {
d->pending = 1;
}
return d;
}
const os_dirent_t *os_dir_next(os_dir_t *d) {
if (!d || d->h == INVALID_HANDLE_VALUE)
return NULL;
for (;;) {
if (!d->pending && !FindNextFileW(d->h, &d->fd))
return NULL;
d->pending = 0;
const wchar_t *n = d->fd.cFileName;
if (wcscmp(n, L".") == 0 || wcscmp(n, L"..") == 0)
continue;
if (WideCharToMultiByte(CP_UTF8, 0, n, -1, d->name, (int)sizeof(d->name),
NULL, NULL) <= 0)
continue;
DWORD a = d->fd.dwFileAttributes;
d->ent.name = d->name;
d->ent.is_link = reparse_is_link(a, d->fd.dwReserved0);
d->ent.is_dir = !d->ent.is_link && (a & FILE_ATTRIBUTE_DIRECTORY);
return &d->ent;
}
}
void os_dir_close(os_dir_t *d) {
if (!d)
return;
if (d->h != INVALID_HANDLE_VALUE)
FindClose(d->h);
free(d);
}
static double filetime_secs(FILETIME ft) {
ULONGLONG t = ((ULONGLONG)ft.dwHighDateTime << 32) | ft.dwLowDateTime;
return (double)t / 1e7 - 11644473600.0;
}
int os_lstat(const char *path, os_stat_t *st) {
memset(st, 0, sizeof(*st));
wchar_t *w = path ? wpath(path) : NULL;
if (!w)
return -1;
WIN32_FILE_ATTRIBUTE_DATA fa;
if (!GetFileAttributesExW(w, GetFileExInfoStandard, &fa)) {
free(w);
return -1;
}
DWORD a = fa.dwFileAttributes;
if (a & FILE_ATTRIBUTE_REPARSE_POINT) {
WIN32_FIND_DATAW fd;
HANDLE h = FindFirstFileW(w, &fd);
if (h != INVALID_HANDLE_VALUE) {
st->is_link = reparse_is_link(a, fd.dwReserved0);
FindClose(h);
}
}
free(w);
st->is_dir = !st->is_link && (a & FILE_ATTRIBUTE_DIRECTORY);
st->is_reg = !st->is_link && !(a & FILE_ATTRIBUTE_DIRECTORY);
st->size = (long long)(((ULONGLONG)fa.nFileSizeHigh << 32) | fa.nFileSizeLow);
st->mtime = filetime_secs(fa.ftLastWriteTime);
return 0;
}
int os_rename_replace(const char *from, const char *to) {
wchar_t *wf = wpath(from), *wt = wpath(to);
int ok = wf && wt &&
MoveFileExW(wf, wt, MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED);
free(wf);
free(wt);
return ok ? 0 : -1;
}
/* --- processes ----------------------------------------------------------- */
/* One argument the way CommandLineToArgvW and the CRT split it again. */
static void cmdline_arg(cst_buf_t *b, const char *a) {
if (a[0] && !strpbrk(a, " \t\n\v\"")) {
cst_buf_puts(b, a);
return;
}
cst_buf_puts(b, "\"");
for (const char *p = a;; p++) {
size_t bs = 0;
while (*p == '\\') {
bs++;
p++;
}
if (!*p) {
for (size_t i = 0; i < bs * 2; i++)
cst_buf_puts(b, "\\");
break;
}
for (size_t i = 0; i < (*p == '"' ? bs * 2 + 1 : bs); i++)
cst_buf_puts(b, "\\");
cst_buf_append(b, p, 1);
}
cst_buf_puts(b, "\"");
}
static wchar_t *build_cmdline(const char *const argv[]) {
cst_buf_t b = {0};
for (int i = 0; argv[i]; i++) {
if (i)
cst_buf_puts(&b, " ");
cmdline_arg(&b, argv[i]);
}
wchar_t *w = b.buf ? os_utf8_to_wide(b.buf) : NULL;
cst_buf_free(&b);
return w;
}
static size_t env_name_len(const wchar_t *e) {
const wchar_t *eq = wcschr(e + 1, L'='); /* "=C:=C:\x" hides a leading '=' */
return eq ? (size_t)(eq - e) : wcslen(e);
}
static int env_cmp(const void *a, const void *b) {
const wchar_t *x = *(const wchar_t *const *)a, *y = *(const wchar_t *const *)b;
return CompareStringOrdinal(x, (int)env_name_len(x), y, (int)env_name_len(y),
TRUE) -
CSTR_EQUAL;
}
/* The inherited environment with env_add ("KEY=VALUE") layered on top, as a
* sorted CREATE_UNICODE_ENVIRONMENT block; NULL = inherit unchanged. */
static wchar_t *env_block(const char *const env_add[]) {
if (!env_add || !env_add[0])
return NULL;
size_t nadd = 0, ncur = 0;
while (env_add[nadd])
nadd++;
wchar_t *cur = GetEnvironmentStringsW();
for (const wchar_t *e = cur; e && *e; e += wcslen(e) + 1)
ncur++;
wchar_t **add = calloc(nadd, sizeof(*add));
wchar_t **v = calloc(ncur + nadd, sizeof(*v));
wchar_t *blk = NULL;
size_t n = 0, total = 1;
if (!add || !v)
goto out;
for (size_t i = 0; i < nadd; i++)
if (!(add[i] = os_utf8_to_wide(env_add[i])))
goto out;
for (wchar_t *e = cur; e && *e; e += wcslen(e) + 1) {
int shadowed = 0;
for (size_t i = 0; i < nadd && !shadowed; i++)
shadowed = env_cmp(&e, &add[i]) == 0;
if (!shadowed)
v[n++] = e;
}
for (size_t i = 0; i < nadd; i++) {
int dup = 0; /* a later entry for the same name wins */
for (size_t k = i + 1; k < nadd && !dup; k++)
dup = env_cmp(&add[i], &add[k]) == 0;
if (!dup)
v[n++] = add[i];
}
qsort(v, n, sizeof(*v), env_cmp);
for (size_t i = 0; i < n; i++)
total += wcslen(v[i]) + 1;
blk = malloc(total * sizeof(wchar_t));
if (blk) {
wchar_t *o = blk;
for (size_t i = 0; i < n; i++) {
size_t l = wcslen(v[i]) + 1;
memcpy(o, v[i], l * sizeof(wchar_t));
o += l;
}
*o = 0;
}
out:
for (size_t i = 0; add && i < nadd; i++)
free(add[i]);
free(add);
free(v);
if (cur)
FreeEnvironmentStringsW(cur);
return blk;
}
static HANDLE new_job(void) {
HANDLE job = CreateJobObjectW(NULL, NULL);
if (!job)
return NULL;
JOBOBJECT_EXTENDED_LIMIT_INFORMATION li;
memset(&li, 0, sizeof(li));
li.BasicLimitInformation.LimitFlags =
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE |
JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION;
SetInformationJobObject(job, JobObjectExtendedLimitInformation, &li,
sizeof(li));
return job;
}
static DWORD job_active(HANDLE job) {
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION ai;
if (!job || !QueryInformationJobObject(job,
JobObjectBasicAccountingInformation,
&ai, sizeof(ai), NULL))
return 0;
return ai.ActiveProcesses;
}
/* Children still running after the shell exited. A child started without
* our console (the sandbox) has its own conhost.exe, which leaves the job a
* moment after its client, so give the job a short while to drain. */
static int job_lingers(HANDLE job) {
for (int i = 0; i < 50 && job_active(job) > 0; i++)
Sleep(10);
return job_active(job) > 0;
}
/* Start argv suspended, put it in a fresh kill-on-close job, then resume.
* Only in/out/err are inherited (PROC_THREAD_ATTRIBUTE_HANDLE_LIST), so a
* concurrent spawn on another thread never leaks into this child. */
static int spawn(const char *cwd, const char *const argv[],
const char *const env_add[], HANDLE token, HANDLE in,
HANDLE out, HANDLE err, HANDLE *proc, HANDLE *job,
DWORD *error) {
*proc = *job = NULL;
*error = ERROR_OUTOFMEMORY;
wchar_t *cmd = build_cmdline(argv);
wchar_t *wcwd = cwd && cwd[0] ? os_utf8_to_wide(cwd) : NULL;
wchar_t *env = env_block(env_add);
HANDLE inherit[3];
int ninh = 0;
HANDLE std3[3] = {in, out, err};
for (int i = 0; i < 3; i++) {
int seen = !std3[i] || std3[i] == INVALID_HANDLE_VALUE;
for (int k = 0; k < ninh && !seen; k++)
seen = inherit[k] == std3[i];
if (!seen)
inherit[ninh++] = std3[i];
}
SIZE_T asz = 0;
InitializeProcThreadAttributeList(NULL, 1, 0, &asz);
LPPROC_THREAD_ATTRIBUTE_LIST attrs = malloc(asz);
int ok = 0;
if (cmd && (!cwd || !cwd[0] || wcwd) && (!env_add || !env_add[0] || env) &&
attrs && InitializeProcThreadAttributeList(attrs, 1, 0, &asz)) {
if (UpdateProcThreadAttribute(attrs, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
inherit, (SIZE_T)ninh * sizeof(HANDLE), NULL,
NULL)) {
STARTUPINFOEXW si;
memset(&si, 0, sizeof(si));
si.StartupInfo.cb = sizeof(si);
si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
si.StartupInfo.hStdInput = in;
si.StartupInfo.hStdOutput = out;
si.StartupInfo.hStdError = err;
si.lpAttributeList = attrs;
/* Share our console when there is one (children then see its UTF-8
* code page); the sandbox token cannot attach to it. */
DWORD flags = CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT |
CREATE_NEW_PROCESS_GROUP | EXTENDED_STARTUPINFO_PRESENT;
if (token || !GetConsoleWindow())
flags |= CREATE_NO_WINDOW;
PROCESS_INFORMATION pi;
memset(&pi, 0, sizeof(pi));
ok = token ? CreateProcessAsUserW(token, NULL, cmd, NULL, NULL, TRUE,
flags, env, wcwd, &si.StartupInfo, &pi)
: CreateProcessW(NULL, cmd, NULL, NULL, TRUE, flags, env, wcwd,
&si.StartupInfo, &pi);
if (ok) {
*job = new_job();
if (*job && !AssignProcessToJobObject(*job, pi.hProcess)) {
CloseHandle(*job);
*job = NULL;
}
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
*proc = pi.hProcess;
} else {
*error = GetLastError();
}
}
DeleteProcThreadAttributeList(attrs);
}
free(attrs);
free(cmd);
free(wcwd);
free(env);
return ok ? 0 : -1;
}
int os_run_capture_token(const char *cwd, const char *const argv[],
size_t max_out, os_run_result_t *r, void *token,
const char *const env_add[]) {
if (!argv || !argv[0] || !r)
return -1;
if (max_out == 0)
max_out = 512 * 1024;
memset(r, 0, sizeof(*r));
SECURITY_ATTRIBUTES sa = {sizeof(sa), NULL, TRUE};
HANDLE rd = NULL, wr = NULL;
if (!CreatePipe(&rd, &wr, &sa, 64 * 1024))
return -1;
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
HANDLE nul = CreateFileW(L"NUL", GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE, &sa,
OPEN_EXISTING, 0, NULL);
HANDLE proc, job;
DWORD err;
int rc = spawn(cwd, argv, env_add, (HANDLE)token, nul, wr, wr, &proc, &job,
&err);
CloseHandle(wr);
if (nul != INVALID_HANDLE_VALUE)
CloseHandle(nul);
if (rc != 0) {
CloseHandle(rd);
if (err != ERROR_FILE_NOT_FOUND && err != ERROR_PATH_NOT_FOUND)
return -1;
cst_buf_t b = {0};
cst_buf_printf(&b, "%s: command not found\n", argv[0]);
r->out_len = b.len;
r->out = cst_buf_steal(&b);
r->exit_code = 127;
return 0;
}
size_t cap = 4096, len = 0;
char *buf = malloc(cap);
if (!buf) {
TerminateJobObject(job, 1);
TerminateProcess(proc, 1);
CloseHandle(rd);
CloseHandle(proc);
if (job)
CloseHandle(job);
return -1;
}
buf[0] = 0;
int truncated = 0, killed = 0, reaped = 0, bg_killed = 0;
double reaped_at = 0, killed_at = 0;
/* no timeout unless one was asked for: a deadline far in the future keeps
* every `now > deadline` check working without a second code path */
int timeout_s = os_run_timeout_effective();
double deadline = timeout_s > 0 ? os_now_ms() + timeout_s * 1000.0 : 1e18;
int idle = 0; /* empty polls in a row: wait longer once output stalls */
for (;;) {
DWORD avail = 0;
if (!PeekNamedPipe(rd, NULL, 0, NULL, &avail, NULL))
break; /* every writer closed */
if (avail) {
char tmp[65536];
DWORD n = 0;
if (!ReadFile(rd, tmp, avail < sizeof(tmp) ? avail : sizeof(tmp), &n,
NULL) ||
n == 0)
break;
os_cap_append(&buf, &len, &cap, max_out, tmp, n, &truncated);
os_observe_output(tmp, n);
idle = 0;
} else if (reaped) {
Sleep(idle++ < 50 ? 1 : 20);
}
DWORD wait = avail ? 0 : idle++ < 50 ? 1 : 20;
if (!reaped && WaitForSingleObject(proc, wait) == WAIT_OBJECT_0) {
reaped = 1;
reaped_at = os_now_ms();
}
double now = os_now_ms();
/* The shell exited but a background child still holds the pipe
* (`server &`): give it a moment, then end the tree. */
if (reaped && !killed && !bg_killed && now - reaped_at > 1000) {
TerminateJobObject(job, 1);
bg_killed = 1;
killed_at = now;
}
if (!reaped && !killed && (os_cancel_requested() || now > deadline)) {
killed = os_cancel_requested() ? 2 : 1;
killed_at = now;
TerminateJobObject(job, killed == 1 ? 124 : 130);
if (!job)
TerminateProcess(proc, killed == 1 ? 124 : 130);
}
/* a writer outside the job can keep the pipe open */
if ((killed || bg_killed) && now - killed_at > 3000)
break;
}
CloseHandle(rd);
WaitForSingleObject(proc, killed ? 5000 : INFINITE);
DWORD code = 1;
GetExitCodeProcess(proc, &code);
if (!bg_killed && !killed && job_lingers(job))
bg_killed = 1; /* closing the job below ends them */
CloseHandle(proc);
if (job)
CloseHandle(job);
size_t ulen = 0;
char *u = os_output_to_utf8(buf, len, &ulen);
if (u) {
free(buf);
buf = u;
len = ulen;
cap = ulen + 1;
}
if (truncated)
os_cap_note(&buf, &len, &cap, "\n...(output truncated)\n");
if (bg_killed && !killed)
os_cap_note(&buf, &len, &cap,
"\n[cst: background processes left running were stopped]\n");
if (killed == 1) {
char note[96];
snprintf(note, sizeof(note), "\n[cst: killed after %ds timeout]\n",
os_run_timeout_effective());
os_cap_note(&buf, &len, &cap, note);
code = 124;
} else if (killed == 2) {
os_cap_note(&buf, &len, &cap, "\n[cst: cancelled]\n");
code = 130;
}
r->out = buf;
r->out_len = len;
r->exit_code = (int)code;
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_token(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) {
(void)child_hook;
(void)ud;
return os_run_capture_token(cwd, argv, max_out, r, NULL, NULL);
}
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));
if (!argv || !argv[0])
return -1;
SECURITY_ATTRIBUTES sa = {sizeof(sa), NULL, TRUE};
HANDLE in_r = NULL, in_w = NULL, out_r = NULL, out_w = NULL;
if (!CreatePipe(&in_r, &in_w, &sa, 64 * 1024))
return -1;
if (!CreatePipe(&out_r, &out_w, &sa, 64 * 1024)) {
CloseHandle(in_r);
CloseHandle(in_w);
return -1;
}
SetHandleInformation(in_w, HANDLE_FLAG_INHERIT, 0);
SetHandleInformation(out_r, HANDLE_FLAG_INHERIT, 0);
wchar_t *werr = stderr_path ? wpath(stderr_path) : NULL;
HANDLE err = CreateFileW(werr ? werr : L"NUL", GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, &sa,
werr ? CREATE_ALWAYS : OPEN_EXISTING, 0, NULL);
free(werr);
HANDLE proc, job;
DWORD e;
int rc = spawn(cwd, argv, env_add, NULL, in_r, out_w,
err != INVALID_HANDLE_VALUE ? err : out_w, &proc, &job, &e);
CloseHandle(in_r);
CloseHandle(out_w);
if (err != INVALID_HANDLE_VALUE)
CloseHandle(err);
if (rc != 0) {
CloseHandle(in_w);
CloseHandle(out_r);
return -1;
}
p->proc = proc;
p->job = job;
p->in_w = in_w;
p->out_r = out_r;
return 0;
}
int os_proc_write(os_proc_t *p, const char *data, size_t n) {
while (n > 0) {
DWORD w = 0;
if (!p->in_w || !WriteFile((HANDLE)p->in_w, data, (DWORD)n, &w, NULL) ||
w == 0)
return -1;
data += w;
n -= w;
}
return 0;
}
long os_proc_read(os_proc_t *p, char *buf, size_t n, int timeout_ms) {
if (!p->out_r)
return -1;
double deadline = os_now_ms() + timeout_ms;
DWORD avail = 0;
for (;;) {
if (!PeekNamedPipe((HANDLE)p->out_r, NULL, 0, NULL, &avail, NULL))
return GetLastError() == ERROR_BROKEN_PIPE ? 0 : -1;
if (avail > 0)
break;
if (os_now_ms() >= deadline)
return -2;
Sleep(5);
}
DWORD r = 0;
if (!ReadFile((HANDLE)p->out_r, buf, avail < n ? avail : (DWORD)n, &r, NULL))
return GetLastError() == ERROR_BROKEN_PIPE ? 0 : -1;
return (long)r;
}
void os_proc_close_stdin(os_proc_t *p) {
if (p->in_w) {
CloseHandle((HANDLE)p->in_w);
p->in_w = NULL;
}
}
void os_proc_kill(os_proc_t *p) {
os_proc_close_stdin(p);
if (p->proc) {
/* polite first: most servers exit when stdin closes */
if (WaitForSingleObject((HANDLE)p->proc, 500) != WAIT_OBJECT_0) {
if (p->job)
TerminateJobObject((HANDLE)p->job, 1);
else
TerminateProcess((HANDLE)p->proc, 1);
}
WaitForSingleObject((HANDLE)p->proc, 2000);
CloseHandle((HANDLE)p->proc);
p->proc = NULL;
}
if (p->job) {
CloseHandle((HANDLE)p->job); /* ends anything the server left behind */
p->job = NULL;
}
if (p->out_r) {
CloseHandle((HANDLE)p->out_r);
p->out_r = NULL;
}
}
void os_windows_version(char *out, size_t n) {
typedef LONG(WINAPI * rtl_ver_fn)(OSVERSIONINFOW *);
OSVERSIONINFOW vi;
memset(&vi, 0, sizeof(vi));
vi.dwOSVersionInfoSize = sizeof(vi);
HMODULE nt = GetModuleHandleW(L"ntdll.dll");
rtl_ver_fn fn = nt ? (rtl_ver_fn)(void (*)(void))GetProcAddress(nt, "RtlGetVersion")
: NULL;
SYSTEM_INFO si;
GetNativeSystemInfo(&si);
const char *arch = si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_ARM64
? "arm64"
: "x86_64";
if (fn && fn(&vi) == 0)
snprintf(out, n, "Windows %lu.%lu.%lu %s", vi.dwMajorVersion,
vi.dwMinorVersion, vi.dwBuildNumber, arch);
else
snprintf(out, n, "Windows %s", arch);
}
/* Mandatory label from an SDDL SACL such as "S:(ML;;NWNR;;;ME)". */
int os_win_set_label(const char *path, const char *sddl) {
wchar_t *wp = wpath(path), *ws = os_utf8_to_wide(sddl);
PSECURITY_DESCRIPTOR sd = NULL;
int rc = -1;
if (wp && ws &&
ConvertStringSecurityDescriptorToSecurityDescriptorW(ws, SDDL_REVISION_1,
&sd, NULL)) {
PACL sacl = NULL;
BOOL present = FALSE, defaulted = FALSE;
if (GetSecurityDescriptorSacl(sd, &present, &sacl, &defaulted) && present &&
SetNamedSecurityInfoW(wp, SE_FILE_OBJECT, LABEL_SECURITY_INFORMATION,
NULL, NULL, NULL, sacl) == ERROR_SUCCESS)
rc = 0;
LocalFree(sd);
}
free(wp);
free(ws);
return rc;
}
int os_win_get_label(const char *path, char *out, size_t n) {
wchar_t *wp = wpath(path);
PSECURITY_DESCRIPTOR sd = NULL;
wchar_t *ws = NULL;
int rc = -1;
if (wp && n > 2 &&
GetNamedSecurityInfoW(wp, SE_FILE_OBJECT, LABEL_SECURITY_INFORMATION,
NULL, NULL, NULL, NULL, &sd) == ERROR_SUCCESS &&
ConvertSecurityDescriptorToStringSecurityDescriptorW(
sd, SDDL_REVISION_1, LABEL_SECURITY_INFORMATION, &ws, NULL)) {
char *s = os_wide_to_utf8(ws);
if (s) {
/* keep "S:" and the explicit ACEs; inherited ones carry the ID flag */
size_t o = 0;
out[o++] = 'S';
out[o++] = ':';
rc = 0;
for (const char *p = strchr(s, '('); p; p = strchr(p + 1, '(')) {
const char *e = strchr(p, ')');
if (!e)
break;
/* (type;flags;rights;...): flags are two-letter codes */
const char *f = strchr(p, ';');
int inherited = 0;
for (f = f && f < e ? f + 1 : e; f + 1 < e && *f != ';'; f += 2)
inherited |= f[0] == 'I' && f[1] == 'D';
size_t len = (size_t)(e - p) + 1;
if (inherited)
continue;
if (o + len >= n) {
rc = -1;
break;
}
memcpy(out + o, p, len);
o += len;
}
out[o] = 0;
free(s);
}
}
if (ws)
LocalFree(ws);
if (sd)
LocalFree(sd);
free(wp);
return rc;
}
int os_chmod_private(const char *path) {
if (!path)
return -1;
return os_win_set_label(path, "S:(ML;;NWNRNX;;;ME)");
}
/* --- clipboard ----------------------------------------------------------- */
int os_clipboard_set(const char *utf8, size_t n) {
cst_buf_t b = {0}; /* CRLF is what Windows text consumers expect */
for (size_t i = 0; i < n; i++) {
if (utf8[i] == '\n' && (i == 0 || utf8[i - 1] != '\r'))
cst_buf_puts(&b, "\r");
cst_buf_append(&b, utf8 + i, 1);
}
if (!b.buf)
cst_buf_puts(&b, "");
int wn = MultiByteToWideChar(CP_UTF8, 0, b.buf, (int)b.len, NULL, 0);
HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, ((size_t)wn + 1) * sizeof(wchar_t));
wchar_t *w = h ? GlobalLock(h) : NULL;
if (!w) {
if (h)
GlobalFree(h);
cst_buf_free(&b);
return -1;
}
MultiByteToWideChar(CP_UTF8, 0, b.buf, (int)b.len, w, wn);
w[wn] = 0;
GlobalUnlock(h);
cst_buf_free(&b);
if (!OpenClipboard(NULL)) {
GlobalFree(h);
return -1;
}
EmptyClipboard();
int ok = SetClipboardData(CF_UNICODETEXT, h) != NULL;
if (!ok)
GlobalFree(h);
CloseClipboard();
return ok ? 0 : -1;
}
#endif /* _WIN32 */