939 lines
28 KiB
C
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 */
|