mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-12 18:31:05 +00:00
On an Intel CPU with P-cores and E-cores a thread on an E-core runs at about 60% of a P-core's speed, and the frame waits for its slowest critical thread. The game's main thread, the thread that presents, and the vk replay, recorder and present threads now select the CPU sets of the P-core logical processors the process affinity allows. Nothing is selected on a CPU with one efficiency class or when the affinity leaves only one kind of core. TOTK_PCORE=0 turns it off; "power:" log lines say what was selected and which threads took it. LAS Recomp does the same for its game, render and vsync threads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1898 lines
69 KiB
C
1898 lines
69 KiB
C
// Linux implementation of platform.h: pthreads, CLOCK_MONOTONIC, futex locks and condition variables, POSIX files,
|
|
// /proc and sysfs for the load counters, posix_spawn for child processes.
|
|
#define _GNU_SOURCE
|
|
#include <dirent.h>
|
|
#include <dlfcn.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <limits.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/futex.h>
|
|
#include <linux/userfaultfd.h>
|
|
#include <poll.h>
|
|
#include <pthread.h>
|
|
#include <sched.h>
|
|
#include <signal.h>
|
|
#include <spawn.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/prctl.h>
|
|
#include <sys/random.h>
|
|
#include <sys/resource.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/syscall.h>
|
|
#include <sys/uio.h>
|
|
#include <sys/un.h>
|
|
#include <sys/utsname.h>
|
|
#include <sys/wait.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include <SDL3/SDL_clipboard.h>
|
|
|
|
#include "platform.h"
|
|
|
|
// The write-watch ABI of Linux 6.7 (uapi linux/fs.h and linux/userfaultfd.h), for headers older than it: the release
|
|
// builds against the Steam Runtime's 5.10 headers and runs on newer kernels. The kernel refuses what it lacks at run time.
|
|
#ifndef PAGEMAP_SCAN
|
|
struct page_region {
|
|
__u64 start;
|
|
__u64 end;
|
|
__u64 categories;
|
|
};
|
|
struct pm_scan_arg {
|
|
__u64 size;
|
|
__u64 flags;
|
|
__u64 start;
|
|
__u64 end;
|
|
__u64 walk_end;
|
|
__u64 vec;
|
|
__u64 vec_len;
|
|
__u64 max_pages;
|
|
__u64 category_inverted;
|
|
__u64 category_mask;
|
|
__u64 category_anyof_mask;
|
|
__u64 return_mask;
|
|
};
|
|
#define PAGEMAP_SCAN _IOWR('f', 16, struct pm_scan_arg)
|
|
#define PAGE_IS_WRITTEN (1 << 1)
|
|
#define PAGE_IS_PRESENT (1 << 3)
|
|
#define PAGE_IS_SWAPPED (1 << 4)
|
|
#define PAGE_IS_PFNZERO (1 << 5)
|
|
#define PM_SCAN_WP_MATCHING (1 << 0)
|
|
#define PM_SCAN_CHECK_WPASYNC (1 << 1)
|
|
#endif
|
|
#ifndef UFFD_FEATURE_WP_ASYNC
|
|
#define UFFD_FEATURE_WP_UNPOPULATED (1 << 13)
|
|
#define UFFD_FEATURE_WP_ASYNC (1 << 15)
|
|
#endif
|
|
#ifndef UFFD_USER_MODE_ONLY
|
|
#define UFFD_USER_MODE_ONLY 1
|
|
#endif
|
|
|
|
extern char **environ;
|
|
|
|
// ---- threads
|
|
|
|
struct PlatThread {
|
|
pthread_t t;
|
|
PlatThreadFn fn;
|
|
void *arg;
|
|
volatile int32_t tid; // the kernel's id once the thread runs, for plat_thread_set_priority
|
|
char name[16]; // the kernel keeps 15 characters
|
|
};
|
|
|
|
// A signal stack for the calling thread, so the fault and crash handlers still run when its own stack is exhausted.
|
|
// Never freed: threads that end are few, and a stack freed while a handler might run on it would be worse.
|
|
static void alt_stack(void) {
|
|
stack_t cur;
|
|
if (!sigaltstack(NULL, &cur) && !(cur.ss_flags & SS_DISABLE)) return;
|
|
size_t size = 64 << 10; // a signal frame with the AVX-512 state takes several KiB
|
|
void *p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
|
if (p == MAP_FAILED) return;
|
|
stack_t ss = {.ss_sp = p, .ss_size = size};
|
|
sigaltstack(&ss, NULL);
|
|
}
|
|
|
|
static void *thread_main(void *p) {
|
|
PlatThread *t = p;
|
|
alt_stack();
|
|
__atomic_store_n(&t->tid, (int32_t)syscall(SYS_gettid), __ATOMIC_RELEASE);
|
|
if (t->name[0]) pthread_setname_np(pthread_self(), t->name);
|
|
t->fn(t->arg);
|
|
return NULL;
|
|
}
|
|
|
|
PlatThread *plat_thread_new(PlatThreadFn fn, void *arg, const char *name) {
|
|
return plat_thread_new_ex(fn, arg, name, 0);
|
|
}
|
|
|
|
PlatThread *plat_thread_new_ex(PlatThreadFn fn, void *arg, const char *name, size_t stack) {
|
|
PlatThread *t = calloc(1, sizeof *t);
|
|
if (!t) return NULL;
|
|
t->fn = fn;
|
|
t->arg = arg;
|
|
if (name) snprintf(t->name, sizeof t->name, "%s", name);
|
|
pthread_attr_t a;
|
|
pthread_attr_init(&a);
|
|
// The stack is mapped lazily, as Windows reserves it: only the pages a thread touches cost memory.
|
|
pthread_attr_setstacksize(&a, stack ? stack : PLAT_STACK_DEFAULT);
|
|
int e = pthread_create(&t->t, &a, thread_main, t);
|
|
pthread_attr_destroy(&a);
|
|
if (e) { free(t); return NULL; }
|
|
return t;
|
|
}
|
|
|
|
static int read_u64_file(const char *path, uint64_t *v);
|
|
|
|
// The physical core of each logical processor is in /sys/devices/system/cpu/cpu<n>/topology/core_id together with its
|
|
// package; the lowest-numbered processor of each (package, core) pair is the core's first.
|
|
int plat_cpu_cores(uint64_t *all, uint64_t *first) {
|
|
cpu_set_t set;
|
|
*all = *first = 0;
|
|
if (sched_getaffinity(0, sizeof set, &set)) return 0;
|
|
int cores = 0;
|
|
uint64_t seen[64][2];
|
|
for (int i = 0; i < 64; i++) {
|
|
if (!CPU_ISSET(i, &set)) continue;
|
|
*all |= 1ull << i;
|
|
uint64_t core = 0, pkg = 0;
|
|
char path[96];
|
|
snprintf(path, sizeof path, "/sys/devices/system/cpu/cpu%d/topology/core_id", i);
|
|
if (!read_u64_file(path, &core)) core = (uint64_t)i;
|
|
snprintf(path, sizeof path, "/sys/devices/system/cpu/cpu%d/topology/physical_package_id", i);
|
|
read_u64_file(path, &pkg);
|
|
int k = 0;
|
|
while (k < cores && (seen[k][0] != core || seen[k][1] != pkg)) k++;
|
|
if (k == cores) seen[cores][0] = core, seen[cores][1] = pkg, cores++, *first |= 1ull << i;
|
|
}
|
|
return cores;
|
|
}
|
|
|
|
// Linux has no process-wide affinity: each thread of /proc/self/task gets the mask, and threads started later inherit
|
|
// it from the thread that starts them.
|
|
int plat_process_affinity(uint64_t mask) {
|
|
cpu_set_t set;
|
|
CPU_ZERO(&set);
|
|
for (int i = 0; i < 64; i++)
|
|
if (mask >> i & 1) CPU_SET(i, &set);
|
|
DIR *d = opendir("/proc/self/task");
|
|
if (!d) return 0;
|
|
int ok = 1;
|
|
for (struct dirent *e; (e = readdir(d));)
|
|
if (e->d_name[0] != '.' && sched_setaffinity((pid_t)atoi(e->d_name), sizeof set, &set)) ok = 0;
|
|
closedir(d);
|
|
return ok;
|
|
}
|
|
|
|
// The first field of /proc/self/task/<tid>/schedstat is the time the thread has run, in ns.
|
|
uint64_t plat_thread_cpu_ns(uint32_t tid) {
|
|
char path[64];
|
|
snprintf(path, sizeof path, "/proc/self/task/%u/schedstat", tid);
|
|
FILE *f = fopen(path, "r");
|
|
unsigned long long ns = 0;
|
|
if (f) {
|
|
if (fscanf(f, "%llu", &ns) != 1) ns = 0;
|
|
fclose(f);
|
|
}
|
|
return ns;
|
|
}
|
|
|
|
// ---- thread interruption: a signal handshake in place of Windows' SuspendThread / Get/SetThreadContext.
|
|
// The caller moves the handle from IDLE to REQ and signals the thread. The thread's handler stores where it was and
|
|
// moves REQ to STOPPED, then spins until the caller moves it on to GO (with the redirect in to), changes its own
|
|
// return context, and goes back to IDLE. A caller that sees no STOPPED within INTR_WAIT_NS (the thread is blocked in
|
|
// the kernel and runs no handler) takes REQ back to IDLE; a handler that then finds IDLE returns at once.
|
|
enum { INTR_IDLE, INTR_REQ, INTR_STOPPED, INTR_GO };
|
|
#define INTR_WAIT_NS 200000u
|
|
|
|
struct PlatIntr {
|
|
pthread_t pt;
|
|
volatile int32_t state;
|
|
uint64_t rip, rsp, to;
|
|
};
|
|
|
|
static __thread PlatIntr *intr_me;
|
|
static int intr_sig;
|
|
|
|
static void intr_handler(int sig, siginfo_t *si, void *uc) {
|
|
(void)sig, (void)si;
|
|
PlatIntr *t = intr_me;
|
|
ucontext_t *u = uc;
|
|
if (!t) return;
|
|
t->rip = (uint64_t)u->uc_mcontext.gregs[REG_RIP];
|
|
t->rsp = (uint64_t)u->uc_mcontext.gregs[REG_RSP];
|
|
int32_t req = INTR_REQ;
|
|
if (!__atomic_compare_exchange_n(&t->state, &req, INTR_STOPPED, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) return;
|
|
while (__atomic_load_n(&t->state, __ATOMIC_ACQUIRE) != INTR_GO) plat_pause();
|
|
if (t->to) u->uc_mcontext.gregs[REG_RIP] = (greg_t)t->to;
|
|
__atomic_store_n(&t->state, INTR_IDLE, __ATOMIC_RELEASE);
|
|
}
|
|
|
|
PlatIntr *plat_intr_self(void) {
|
|
static PlatMutex install = PLAT_MUTEX_INIT;
|
|
if (!__atomic_load_n(&intr_sig, __ATOMIC_ACQUIRE)) {
|
|
plat_mutex_lock(&install);
|
|
if (!intr_sig) {
|
|
int sig = SIGRTMIN + 4; // glibc keeps the first real-time signals for itself; SIGRTMIN skips them
|
|
struct sigaction sa = {.sa_sigaction = intr_handler, .sa_flags = SA_SIGINFO | SA_RESTART | SA_ONSTACK};
|
|
sigemptyset(&sa.sa_mask);
|
|
if (!sigaction(sig, &sa, NULL)) __atomic_store_n(&intr_sig, sig, __ATOMIC_RELEASE);
|
|
}
|
|
plat_mutex_unlock(&install);
|
|
}
|
|
if (!intr_sig) return NULL;
|
|
PlatIntr *t = calloc(1, sizeof *t);
|
|
if (!t) return NULL;
|
|
t->pt = pthread_self();
|
|
intr_me = t;
|
|
return t;
|
|
}
|
|
|
|
// Called by the thread itself as it ends, or by another once the thread no longer runs.
|
|
void plat_intr_free(PlatIntr *t) {
|
|
if (!t) return;
|
|
if (intr_me == t) intr_me = NULL;
|
|
free(t);
|
|
}
|
|
|
|
int plat_intr_stop(PlatIntr *t, uint64_t *rip, uint64_t *rsp) {
|
|
int32_t idle = INTR_IDLE;
|
|
if (!__atomic_compare_exchange_n(&t->state, &idle, INTR_REQ, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) return 0;
|
|
if (pthread_kill(t->pt, intr_sig)) {
|
|
__atomic_store_n(&t->state, INTR_IDLE, __ATOMIC_RELEASE);
|
|
return 0;
|
|
}
|
|
uint64_t end = plat_now_ns() + INTR_WAIT_NS;
|
|
while (__atomic_load_n(&t->state, __ATOMIC_ACQUIRE) != INTR_STOPPED) {
|
|
if (plat_now_ns() < end) {
|
|
plat_pause();
|
|
continue;
|
|
}
|
|
int32_t req = INTR_REQ;
|
|
if (__atomic_compare_exchange_n(&t->state, &req, INTR_IDLE, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) return 0;
|
|
}
|
|
*rip = t->rip;
|
|
*rsp = t->rsp;
|
|
return 1;
|
|
}
|
|
|
|
int plat_intr_resume(PlatIntr *t, uint64_t to) {
|
|
t->to = to;
|
|
__atomic_store_n(&t->state, INTR_GO, __ATOMIC_RELEASE);
|
|
return 1;
|
|
}
|
|
|
|
// ---- fibers: a stack switch of our own. glibc's swapcontext saves and restores the signal mask with a system call
|
|
// on every switch, and the game switches fibers often. fiber_swap saves the callee-saved registers, the MXCSR and the
|
|
// x87 control word on the current stack, stores the stack pointer in *save and loads the other one. A new fiber's
|
|
// stack is laid out as fiber_swap leaves one, returning into fiber_entry, which realigns the stack and calls fn(arg)
|
|
// (r12 and r13).
|
|
struct PlatFiber {
|
|
void *sp;
|
|
void *stack; // mapped stack (NULL for a thread's own context)
|
|
size_t size;
|
|
};
|
|
|
|
void plat_fiber_swap(void **save, void *load);
|
|
void plat_fiber_entry(void);
|
|
__asm__(".text\n"
|
|
".globl plat_fiber_swap\n"
|
|
".type plat_fiber_swap, @function\n"
|
|
".p2align 4\n"
|
|
"plat_fiber_swap:\n"
|
|
" .cfi_startproc\n"
|
|
" pushq %rbp\n"
|
|
" pushq %rbx\n"
|
|
" pushq %r12\n"
|
|
" pushq %r13\n"
|
|
" pushq %r14\n"
|
|
" pushq %r15\n"
|
|
" subq $16, %rsp\n"
|
|
" stmxcsr 8(%rsp)\n"
|
|
" fnstcw (%rsp)\n"
|
|
" movq %rsp, (%rdi)\n"
|
|
" movq %rsi, %rsp\n"
|
|
" fldcw (%rsp)\n"
|
|
" ldmxcsr 8(%rsp)\n"
|
|
" addq $16, %rsp\n"
|
|
" popq %r15\n"
|
|
" popq %r14\n"
|
|
" popq %r13\n"
|
|
" popq %r12\n"
|
|
" popq %rbx\n"
|
|
" popq %rbp\n"
|
|
" ret\n"
|
|
" .cfi_endproc\n"
|
|
".size plat_fiber_swap, .-plat_fiber_swap\n"
|
|
".globl plat_fiber_entry\n"
|
|
".type plat_fiber_entry, @function\n"
|
|
".p2align 4\n"
|
|
"plat_fiber_entry:\n"
|
|
// fiber_swap's ret lands here with rsp = top - 8, as at the entry of a called function. The call below must
|
|
// be made with rsp 16-byte aligned so that fn starts with rsp = 8 mod 16, as the System V ABI demands; without
|
|
// the realignment every 16-byte local of fn is off by 8, which the VEX instructions of a v3 build tolerate
|
|
// but the SSE ones of a v2 or k10 build (movdqa, pcmpeqb with a memory operand) fault on.
|
|
" andq $-16, %rsp\n"
|
|
" movq %r13, %rdi\n"
|
|
" callq *%r12\n"
|
|
" ud2\n"
|
|
".size plat_fiber_entry, .-plat_fiber_entry\n");
|
|
|
|
static __thread PlatFiber *fiber_cur;
|
|
static __thread PlatFiber fiber_own;
|
|
|
|
PlatFiber *plat_fiber_self(void) {
|
|
fiber_own = (PlatFiber){0};
|
|
fiber_cur = &fiber_own;
|
|
return &fiber_own;
|
|
}
|
|
|
|
void plat_fiber_unself(void) { fiber_cur = NULL; }
|
|
|
|
PlatFiber *plat_fiber_new(size_t stack, void (*fn)(void *arg), void *arg) {
|
|
PlatFiber *f = calloc(1, sizeof *f);
|
|
if (!f) return NULL;
|
|
// Reserved, not committed; the lowest page stays inaccessible, so an overflow faults instead of corrupting.
|
|
void *m = mmap(NULL, stack, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_STACK, -1, 0);
|
|
if (m == MAP_FAILED) {
|
|
free(f);
|
|
return NULL;
|
|
}
|
|
mprotect(m, 4096, PROT_NONE);
|
|
f->stack = m, f->size = stack;
|
|
uint64_t *top = (uint64_t *)(((uintptr_t)m + stack) & ~(uintptr_t)15);
|
|
uint64_t *sp = top - 10; // fpu state (2 words), r15, r14, r13, r12, rbx, rbp, return address, padding
|
|
sp[0] = 0x037f; // x87 control word: the default
|
|
sp[1] = 0x1f80; // MXCSR: the default
|
|
sp[2] = sp[3] = 0; // r15, r14
|
|
sp[4] = (uint64_t)(uintptr_t)arg; // r13
|
|
sp[5] = (uint64_t)(uintptr_t)fn; // r12
|
|
sp[6] = sp[7] = 0; // rbx, rbp
|
|
sp[8] = (uint64_t)(uintptr_t)plat_fiber_entry;
|
|
sp[9] = 0;
|
|
f->sp = sp;
|
|
return f;
|
|
}
|
|
|
|
void plat_fiber_switch(PlatFiber *to) {
|
|
PlatFiber *from = fiber_cur;
|
|
fiber_cur = to; // on this thread; the switch back into from sets it on whichever thread that is
|
|
plat_fiber_swap(&from->sp, to->sp);
|
|
}
|
|
|
|
void plat_fiber_free(PlatFiber *f) {
|
|
if (!f) return;
|
|
if (f->stack) munmap(f->stack, f->size);
|
|
free(f);
|
|
}
|
|
|
|
int plat_thread_spawn(PlatThreadFn fn, void *arg, const char *name, size_t stack) {
|
|
PlatThread *t = plat_thread_new_ex(fn, arg, name, stack);
|
|
if (t) plat_thread_detach(t);
|
|
return t != NULL;
|
|
}
|
|
|
|
void plat_thread_join(PlatThread *t) {
|
|
pthread_join(t->t, NULL);
|
|
free(t);
|
|
}
|
|
|
|
int plat_thread_join_ns(PlatThread *t, uint64_t ns) {
|
|
struct timespec at;
|
|
clock_gettime(CLOCK_REALTIME, &at); // pthread_timedjoin_np takes a CLOCK_REALTIME deadline
|
|
uint64_t end = (uint64_t)at.tv_sec * 1000000000u + (uint64_t)at.tv_nsec + ns;
|
|
at.tv_sec = (time_t)(end / 1000000000u), at.tv_nsec = (long)(end % 1000000000u);
|
|
if (pthread_timedjoin_np(t->t, NULL, &at)) return 0;
|
|
free(t);
|
|
return 1;
|
|
}
|
|
|
|
// The thread keeps using t->fn and t->arg, so the struct stays allocated (a few bytes per detached thread).
|
|
void plat_thread_detach(PlatThread *t) { pthread_detach(t->t); }
|
|
|
|
uint64_t plat_thread_affinity(PlatThread *t, uint64_t mask) {
|
|
pthread_t th = t ? t->t : pthread_self();
|
|
cpu_set_t old, set;
|
|
if (pthread_getaffinity_np(th, sizeof old, &old)) return 0;
|
|
CPU_ZERO(&set);
|
|
for (int i = 0; i < 64; i++)
|
|
if (mask >> i & 1) CPU_SET(i, &set);
|
|
if (pthread_setaffinity_np(th, sizeof set, &set)) return 0;
|
|
uint64_t prev = 0;
|
|
for (int i = 0; i < 64; i++)
|
|
if (CPU_ISSET(i, &old)) prev |= 1ull << i;
|
|
return prev;
|
|
}
|
|
|
|
uint32_t plat_cpu_count(void) {
|
|
long n = sysconf(_SC_NPROCESSORS_ONLN);
|
|
return n > 0 ? (uint32_t)n : 1;
|
|
}
|
|
|
|
// The processors of the process's affinity mask (taskset, a launcher's CPU set), every online one when it is unknown.
|
|
uint32_t plat_cpu_usable(void) {
|
|
cpu_set_t set;
|
|
if (!sched_getaffinity(0, sizeof set, &set) && CPU_COUNT(&set) > 0) return (uint32_t)CPU_COUNT(&set);
|
|
return plat_cpu_count();
|
|
}
|
|
|
|
// Windows' levels map to nice values: IDLE 19, LOWEST 4, BELOW_NORMAL 2, NORMAL 0, ABOVE_NORMAL -2, HIGHEST -5,
|
|
// TIME_CRITICAL -10. Applied only when the process may lower a nice value again (platform.h says why).
|
|
static int nice_of(int prio) {
|
|
return prio <= PLAT_PRIO_IDLE ? 19 : prio == PLAT_PRIO_LOWEST ? 4 : prio == PLAT_PRIO_BELOW_NORMAL ? 2
|
|
: prio == PLAT_PRIO_NORMAL ? 0 : prio == PLAT_PRIO_ABOVE_NORMAL ? -2 : prio == PLAT_PRIO_HIGHEST ? -5 : -10;
|
|
}
|
|
|
|
static int may_renice(int nice) {
|
|
static int lowest = 100; // the lowest nice value this process may set; 100 until probed
|
|
if (lowest == 100) {
|
|
struct rlimit r;
|
|
int l = !getrlimit(RLIMIT_NICE, &r) && r.rlim_cur != RLIM_INFINITY ? 20 - (int)r.rlim_cur : -20;
|
|
// CAP_SYS_NICE shows as being allowed to set a nice value below the limit's
|
|
pid_t tid = (pid_t)syscall(SYS_gettid);
|
|
errno = 0;
|
|
int cur = getpriority(PRIO_PROCESS, (id_t)tid);
|
|
if (!errno && l > cur - 1 && !setpriority(PRIO_PROCESS, (id_t)tid, cur - 1)) {
|
|
setpriority(PRIO_PROCESS, (id_t)tid, cur);
|
|
l = -20;
|
|
}
|
|
__atomic_store_n(&lowest, l, __ATOMIC_RELAXED);
|
|
}
|
|
return lowest <= 0 && nice >= lowest;
|
|
}
|
|
|
|
static void renice(pid_t tid, int prio) {
|
|
int nice = nice_of(prio);
|
|
if (tid > 0 && may_renice(nice)) setpriority(PRIO_PROCESS, (id_t)tid, nice);
|
|
}
|
|
|
|
void plat_thread_priority(int prio) { renice((pid_t)syscall(SYS_gettid), prio); }
|
|
|
|
void plat_thread_set_priority(PlatThread *t, int prio) { renice(__atomic_load_n(&t->tid, __ATOMIC_ACQUIRE), prio); }
|
|
|
|
int plat_thread_priority_get(void) {
|
|
errno = 0;
|
|
int nice = getpriority(PRIO_PROCESS, (id_t)syscall(SYS_gettid));
|
|
if (errno) return PLAT_PRIO_NORMAL;
|
|
return nice >= 19 ? PLAT_PRIO_IDLE : nice >= 4 ? PLAT_PRIO_LOWEST : nice >= 2 ? PLAT_PRIO_BELOW_NORMAL
|
|
: nice >= 0 ? PLAT_PRIO_NORMAL : nice >= -2 ? PLAT_PRIO_ABOVE_NORMAL : nice >= -5 ? PLAT_PRIO_HIGHEST
|
|
: PLAT_PRIO_TIME_CRITICAL;
|
|
}
|
|
|
|
void plat_yield(void) { sched_yield(); }
|
|
|
|
// ---- time
|
|
|
|
uint64_t plat_now_ns(void) {
|
|
struct timespec t;
|
|
clock_gettime(CLOCK_MONOTONIC, &t);
|
|
return (uint64_t)t.tv_sec * 1000000000u + (uint64_t)t.tv_nsec;
|
|
}
|
|
|
|
uint64_t plat_counter(void) { return plat_now_ns(); }
|
|
uint64_t plat_counter_hz(void) { return 1000000000u; }
|
|
|
|
// The timer slack (50 us by default) is how late the kernel may wake a sleeping thread to batch wakeups. New threads
|
|
// inherit it from the thread that creates them.
|
|
void plat_timer_1ms(void) { prctl(PR_SET_TIMERSLACK, 1000ul, 0, 0, 0); }
|
|
|
|
int plat_power_full(void) { return -1; }
|
|
|
|
int plat_perf_core_setup(char *why, size_t n) {
|
|
snprintf(why, n, "not done on Linux");
|
|
return 0;
|
|
}
|
|
int plat_thread_perf_core(void) { return -1; }
|
|
|
|
// clock_nanosleep wakes within the timer slack (50 us by default) of its deadline; the last PLAT_SPIN_NS are spent
|
|
// yielding so the deadline is met closely, as on Windows.
|
|
#define PLAT_SPIN_NS 100000u
|
|
|
|
void plat_sleep_ns(uint64_t ns) {
|
|
uint64_t end = plat_now_ns() + ns;
|
|
if (ns > PLAT_SPIN_NS) {
|
|
uint64_t at = end - PLAT_SPIN_NS;
|
|
struct timespec ts = {(time_t)(at / 1000000000u), (long)(at % 1000000000u)};
|
|
while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL) == EINTR) {}
|
|
}
|
|
while (plat_now_ns() < end) sched_yield();
|
|
}
|
|
|
|
void plat_wait_ns(uint64_t ns) {
|
|
struct timespec ts = {(time_t)(ns / 1000000000u), (long)(ns % 1000000000u)};
|
|
while (nanosleep(&ts, &ts) == -1 && errno == EINTR) {}
|
|
}
|
|
|
|
void plat_sleep_ms(uint32_t ms) {
|
|
struct timespec ts = {(time_t)(ms / 1000), (long)(ms % 1000) * 1000000};
|
|
while (nanosleep(&ts, &ts) == -1 && errno == EINTR) {}
|
|
}
|
|
|
|
// ---- locks: a reader-writer lock in one 32-bit word, zero when free.
|
|
// bit 31: a writer holds it; bit 30: some thread sleeps on the word; bits 0-29: readers holding it.
|
|
// A thread that cannot take the lock sets the sleeper bit and waits on the futex; every release that finds the bit
|
|
// clears it and wakes all sleepers, which retry (and set it again if they still cannot get in). Readers do not enter
|
|
// while the sleeper bit is set, so a waiting writer is not starved by a stream of readers.
|
|
|
|
#define LK_W 0x80000000u
|
|
#define LK_S 0x40000000u
|
|
#define LK_R 0x3fffffffu
|
|
#define LK_SPINS 64
|
|
|
|
static long futex(volatile uint32_t *w, int op, uint32_t val, const struct timespec *ts) {
|
|
return syscall(SYS_futex, w, op | FUTEX_PRIVATE_FLAG, val, ts, NULL, 0);
|
|
}
|
|
|
|
static inline volatile uint32_t *lk(PlatMutex *m) { return (volatile uint32_t *)&m->p; }
|
|
|
|
// Sets the sleeper bit on s and sleeps while the word still equals the result.
|
|
static void lk_sleep(volatile uint32_t *w, uint32_t s) {
|
|
if (!(s & LK_S) && !__atomic_compare_exchange_n(w, &s, s | LK_S, 0, __ATOMIC_RELAXED, __ATOMIC_RELAXED)) return;
|
|
futex(w, FUTEX_WAIT, s | LK_S, NULL);
|
|
}
|
|
|
|
static void lk_wake(volatile uint32_t *w) {
|
|
__atomic_fetch_and(w, ~LK_S, __ATOMIC_RELAXED);
|
|
futex(w, FUTEX_WAKE, INT_MAX, NULL);
|
|
}
|
|
|
|
void plat_mutex_init(PlatMutex *m) { m->p = 0; }
|
|
|
|
int plat_mutex_trylock(PlatMutex *m) {
|
|
volatile uint32_t *w = lk(m);
|
|
uint32_t s = __atomic_load_n(w, __ATOMIC_RELAXED);
|
|
while (!(s & (LK_W | LK_R)))
|
|
if (__atomic_compare_exchange_n(w, &s, s | LK_W, 1, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) return 1;
|
|
return 0;
|
|
}
|
|
|
|
void plat_mutex_lock(PlatMutex *m) {
|
|
volatile uint32_t *w = lk(m);
|
|
for (int spin = 0;; spin++) {
|
|
uint32_t s = __atomic_load_n(w, __ATOMIC_RELAXED);
|
|
if (!(s & (LK_W | LK_R))) {
|
|
if (__atomic_compare_exchange_n(w, &s, s | LK_W, 1, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) return;
|
|
continue;
|
|
}
|
|
if (spin < LK_SPINS) plat_pause();
|
|
else lk_sleep(w, s);
|
|
}
|
|
}
|
|
|
|
void plat_mutex_unlock(PlatMutex *m) {
|
|
volatile uint32_t *w = lk(m);
|
|
uint32_t s = __atomic_fetch_and(w, ~LK_W, __ATOMIC_RELEASE);
|
|
if (s & LK_S) lk_wake(w);
|
|
}
|
|
|
|
int plat_mutex_trylock_shared(PlatMutex *m) {
|
|
volatile uint32_t *w = lk(m);
|
|
uint32_t s = __atomic_load_n(w, __ATOMIC_RELAXED);
|
|
while (!(s & (LK_W | LK_S)))
|
|
if (__atomic_compare_exchange_n(w, &s, s + 1, 1, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) return 1;
|
|
return 0;
|
|
}
|
|
|
|
void plat_mutex_lock_shared(PlatMutex *m) {
|
|
volatile uint32_t *w = lk(m);
|
|
for (int spin = 0;; spin++) {
|
|
uint32_t s = __atomic_load_n(w, __ATOMIC_RELAXED);
|
|
if (!(s & (LK_W | LK_S))) {
|
|
if (__atomic_compare_exchange_n(w, &s, s + 1, 1, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) return;
|
|
continue;
|
|
}
|
|
if (spin < LK_SPINS) plat_pause();
|
|
else lk_sleep(w, s);
|
|
}
|
|
}
|
|
|
|
void plat_mutex_unlock_shared(PlatMutex *m) {
|
|
volatile uint32_t *w = lk(m);
|
|
uint32_t s = __atomic_sub_fetch(w, 1, __ATOMIC_RELEASE);
|
|
if (!(s & LK_R) && (s & LK_S)) lk_wake(w);
|
|
}
|
|
|
|
// ---- condition variables: a sequence number in the first 32 bits. A waiter reads it before releasing the lock and
|
|
// sleeps only while it is unchanged, so a signal between the release and the sleep is not lost.
|
|
|
|
static inline volatile uint32_t *cv(PlatCond *c) { return (volatile uint32_t *)&c->p; }
|
|
|
|
void plat_cond_init(PlatCond *c) { c->p = 0; }
|
|
|
|
void plat_cond_wait(PlatCond *c, PlatMutex *m) {
|
|
uint32_t seq = __atomic_load_n(cv(c), __ATOMIC_RELAXED);
|
|
plat_mutex_unlock(m);
|
|
futex(cv(c), FUTEX_WAIT, seq, NULL);
|
|
plat_mutex_lock(m);
|
|
}
|
|
|
|
int plat_cond_wait_ns(PlatCond *c, PlatMutex *m, uint64_t ns) {
|
|
uint32_t seq = __atomic_load_n(cv(c), __ATOMIC_RELAXED);
|
|
struct timespec ts = {(time_t)(ns / 1000000000u), (long)(ns % 1000000000u)};
|
|
plat_mutex_unlock(m);
|
|
long r = futex(cv(c), FUTEX_WAIT, seq, &ts);
|
|
int timed_out = r == -1 && errno == ETIMEDOUT;
|
|
plat_mutex_lock(m);
|
|
return !timed_out;
|
|
}
|
|
|
|
void plat_cond_signal(PlatCond *c) {
|
|
__atomic_add_fetch(cv(c), 1, __ATOMIC_RELEASE);
|
|
futex(cv(c), FUTEX_WAKE, 1, NULL);
|
|
}
|
|
|
|
void plat_cond_broadcast(PlatCond *c) {
|
|
__atomic_add_fetch(cv(c), 1, __ATOMIC_RELEASE);
|
|
futex(cv(c), FUTEX_WAKE, INT_MAX, NULL);
|
|
}
|
|
|
|
// ---- files
|
|
|
|
int plat_file_exists(const char *path) {
|
|
struct stat st;
|
|
return !stat(path, &st) && !S_ISDIR(st.st_mode);
|
|
}
|
|
|
|
int plat_dir_exists(const char *path) {
|
|
struct stat st;
|
|
return !stat(path, &st) && S_ISDIR(st.st_mode);
|
|
}
|
|
|
|
int plat_full_path(const char *path, char *out, size_t cap) {
|
|
if (path[0] == '/') return snprintf(out, cap, "%s", path) < (int)cap;
|
|
char cwd[PATH_MAX];
|
|
if (!getcwd(cwd, sizeof cwd)) return 0;
|
|
return snprintf(out, cap, "%s/%s", cwd, path) < (int)cap;
|
|
}
|
|
|
|
void plat_file_mkdir_p(const char *path) {
|
|
char tmp[PATH_MAX];
|
|
if (snprintf(tmp, sizeof tmp, "%s", path) >= (int)sizeof tmp) return;
|
|
for (char *p = tmp + 1; *p; p++) {
|
|
if (*p == '/' || *p == '\\') {
|
|
char c = *p;
|
|
*p = 0;
|
|
mkdir(tmp, 0755);
|
|
*p = c;
|
|
}
|
|
}
|
|
mkdir(tmp, 0755);
|
|
}
|
|
|
|
static int write_all(int fd, const void *data, size_t n) {
|
|
const uint8_t *p = data;
|
|
while (n) {
|
|
ssize_t k = write(fd, p, n);
|
|
if (k < 0 && errno == EINTR) continue;
|
|
if (k <= 0) return 0;
|
|
p += k, n -= (size_t)k;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
uint8_t *plat_file_read(const char *path, uint64_t *size) {
|
|
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
|
if (fd < 0) return NULL;
|
|
struct stat st;
|
|
uint8_t *buf = NULL;
|
|
if (!fstat(fd, &st) && S_ISREG(st.st_mode) && st.st_size > 0 && st.st_size < (1ll << 30)) {
|
|
buf = malloc((size_t)st.st_size + 1);
|
|
size_t got = 0;
|
|
while (buf && got < (size_t)st.st_size) {
|
|
ssize_t k = read(fd, buf + got, (size_t)st.st_size - got);
|
|
if (k < 0 && errno == EINTR) continue;
|
|
if (k <= 0) break;
|
|
got += (size_t)k;
|
|
}
|
|
if (buf && got != (size_t)st.st_size) {
|
|
free(buf);
|
|
buf = NULL;
|
|
}
|
|
if (buf) {
|
|
buf[st.st_size] = 0;
|
|
*size = (uint64_t)st.st_size;
|
|
}
|
|
}
|
|
close(fd);
|
|
return buf;
|
|
}
|
|
|
|
int plat_file_write(const char *path, const void *data, size_t n) {
|
|
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
|
|
if (fd < 0) return 0;
|
|
int ok = write_all(fd, data, n);
|
|
return close(fd) == 0 && ok;
|
|
}
|
|
|
|
// O_APPEND makes each write land whole at the end of the file, from any thread.
|
|
PlatAppend *plat_append_open(const char *path) {
|
|
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_APPEND | O_CLOEXEC, 0644);
|
|
return fd < 0 ? NULL : (PlatAppend *)(intptr_t)(fd + 1);
|
|
}
|
|
|
|
int plat_append_write(PlatAppend *f, const void *data, size_t n) {
|
|
int fd = (int)(intptr_t)f - 1;
|
|
ssize_t k;
|
|
do k = write(fd, data, n);
|
|
while (k < 0 && errno == EINTR);
|
|
return k == (ssize_t)n;
|
|
}
|
|
|
|
// Flushes the directory holding path, so a rename into it survives a crash.
|
|
static void sync_dir_of(const char *path) {
|
|
char dir[PATH_MAX];
|
|
if (snprintf(dir, sizeof dir, "%s", path) >= (int)sizeof dir) return;
|
|
char *slash = strrchr(dir, '/');
|
|
if (slash == dir) slash[1] = 0;
|
|
else if (slash) *slash = 0;
|
|
else strcpy(dir, ".");
|
|
int fd = open(dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
|
if (fd >= 0) {
|
|
fsync(fd);
|
|
close(fd);
|
|
}
|
|
}
|
|
|
|
int plat_file_replace(const char *from, const char *to) {
|
|
if (rename(from, to)) return 0;
|
|
sync_dir_of(to);
|
|
return 1;
|
|
}
|
|
|
|
int plat_file_write_atomic(const char *path, const void *data, size_t n) {
|
|
// A rename replaces a read-only file here (only the folder's permission counts) but not on Windows: a file the
|
|
// player made read-only stays as it is on both.
|
|
if (access(path, W_OK) && errno != ENOENT) return 0;
|
|
char tmp[PATH_MAX + 48];
|
|
int len = snprintf(tmp, sizeof tmp, "%s.%u.%u.tmp", path, plat_process_id(), plat_thread_id());
|
|
if (len < 0 || len >= (int)sizeof tmp) return 0;
|
|
int fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
|
|
if (fd < 0) return 0;
|
|
int ok = write_all(fd, data, n) && fsync(fd) == 0;
|
|
ok = close(fd) == 0 && ok;
|
|
if (ok && !rename(tmp, path)) {
|
|
sync_dir_of(path);
|
|
return 1;
|
|
}
|
|
unlink(tmp);
|
|
return 0;
|
|
}
|
|
|
|
int plat_file_flush(const char *path) {
|
|
int fd = open(path, O_WRONLY | O_CLOEXEC);
|
|
if (fd < 0) return 0;
|
|
int ok = fsync(fd) == 0;
|
|
close(fd);
|
|
return ok;
|
|
}
|
|
|
|
int plat_file_sync(FILE *f) { return !fflush(f) && !fsync(fileno(f)); }
|
|
|
|
void plat_file_delete(const char *path) { unlink(path); }
|
|
|
|
int plat_file_truncate(const char *path, uint64_t size) { return truncate(path, (off_t)size) == 0; }
|
|
|
|
int64_t plat_file_size(const char *path) {
|
|
struct stat st;
|
|
return stat(path, &st) ? -1 : (int64_t)st.st_size;
|
|
}
|
|
|
|
// The owner's write permission stands for Windows' read-only attribute.
|
|
int plat_file_set_readonly(const char *path, int on) {
|
|
struct stat st;
|
|
if (stat(path, &st)) return 0;
|
|
mode_t m = on ? st.st_mode & ~(mode_t)0222 : st.st_mode | S_IWUSR;
|
|
return chmod(path, m & 07777) == 0;
|
|
}
|
|
|
|
int plat_file_list(const char *dir, const char *ext, int (*cb)(const char *name, void *ctx), void *ctx) {
|
|
DIR *d = opendir(dir);
|
|
if (!d) return 0;
|
|
size_t el = strlen(ext);
|
|
for (struct dirent *e; (e = readdir(d));) {
|
|
size_t n = strlen(e->d_name);
|
|
if (n < el || strcmp(e->d_name + n - el, ext) || e->d_name[0] == '.') continue;
|
|
if (!cb(e->d_name, ctx)) break;
|
|
}
|
|
closedir(d);
|
|
return 1;
|
|
}
|
|
|
|
// ---- virtual memory. Reservations are PROT_NONE MAP_NORESERVE mappings; commit is mprotect read-write, decommit
|
|
// MADV_DONTNEED (private anonymous pages read zero afterwards) and PROT_NONE. Linux keeps no committed/reserved
|
|
// distinction, so a byte per guest page records it (pstate, mapped lazily: only the pages touched cost memory).
|
|
//
|
|
// Write watch: a userfaultfd in asynchronous write-protect mode (Linux 6.7) over the watched reservations. The kernel
|
|
// clears a page's protection on the first write without a round trip to us, and PAGEMAP_SCAN lists the pages whose
|
|
// protection was cleared and protects them again. Without it (an older kernel), plat_mem_written reports the ranges as
|
|
// untracked and the back end uploads them in full.
|
|
|
|
#define PG 4096ull
|
|
#define PS_LIMIT (1ull << 39)
|
|
enum { PS_FREE, PS_RESERVED, PS_NONE, PS_READ, PS_RW }; // PS_NONE.. are committed: PLAT_PROT_* + PS_NONE
|
|
static uint8_t *pstate;
|
|
static int ww_fd = -1, pm_fd = -1; // userfaultfd and /proc/self/pagemap; -1 when write watch is unavailable
|
|
static PlatMutex vm_lock = PLAT_MUTEX_INIT;
|
|
|
|
static void ps_set(uint64_t addr, uint64_t n, uint8_t v) {
|
|
if (addr < PS_LIMIT) memset(pstate + addr / PG, v, (size_t)((n + PG - 1) / PG));
|
|
}
|
|
|
|
static void ww_open(void) {
|
|
int fd = (int)syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK | UFFD_USER_MODE_ONLY);
|
|
struct uffdio_api api = {.api = UFFD_API,
|
|
.features = UFFD_FEATURE_WP_ASYNC | UFFD_FEATURE_WP_UNPOPULATED};
|
|
if (fd < 0 || ioctl(fd, UFFDIO_API, &api)) {
|
|
fprintf(stderr, "platform: no asynchronous userfaultfd write protection (%s): written memory is not tracked\n",
|
|
strerror(errno));
|
|
if (fd >= 0) close(fd);
|
|
return;
|
|
}
|
|
pm_fd = open("/proc/self/pagemap", O_RDONLY | O_CLOEXEC);
|
|
if (pm_fd < 0) {
|
|
close(fd);
|
|
return;
|
|
}
|
|
ww_fd = fd;
|
|
}
|
|
|
|
int plat_mem_reserve(uint64_t addr, uint64_t n, int watch) {
|
|
plat_mutex_lock(&vm_lock);
|
|
if (!pstate) {
|
|
void *p = mmap(NULL, PS_LIMIT / PG, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
|
|
pstate = p == MAP_FAILED ? NULL : p;
|
|
}
|
|
if (watch && ww_fd < 0 && pm_fd < 0) ww_open();
|
|
void *p = pstate && addr + n <= PS_LIMIT
|
|
? mmap((void *)(uintptr_t)addr, n, PROT_NONE,
|
|
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, -1, 0)
|
|
: MAP_FAILED;
|
|
int ok = p == (void *)(uintptr_t)addr;
|
|
if (p != MAP_FAILED && !ok) munmap(p, n); // a kernel without MAP_FIXED_NOREPLACE put it elsewhere
|
|
if (ok) {
|
|
ps_set(addr, n, PS_RESERVED);
|
|
// Transparent huge pages would populate (and count as written) 2 MiB at the first write of one page; writes are
|
|
// tracked per 4 KiB page, as on Windows.
|
|
if (watch) madvise(p, n, MADV_NOHUGEPAGE);
|
|
struct uffdio_register reg = {.range = {addr, n}, .mode = UFFDIO_REGISTER_MODE_WP};
|
|
if (watch && ww_fd >= 0 && ioctl(ww_fd, UFFDIO_REGISTER, ®))
|
|
fprintf(stderr, "platform: write watch over %#llx+%#llx failed (%s)\n", (unsigned long long)addr,
|
|
(unsigned long long)n, strerror(errno));
|
|
}
|
|
plat_mutex_unlock(&vm_lock);
|
|
return ok;
|
|
}
|
|
|
|
void *plat_mem_alloc(size_t n) {
|
|
void *p = mmap(NULL, n, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
|
|
return p == MAP_FAILED ? NULL : p;
|
|
}
|
|
|
|
int plat_mem_commit(uint64_t addr, size_t n) {
|
|
if (mprotect((void *)(uintptr_t)addr, n, PROT_READ | PROT_WRITE)) return 0;
|
|
ps_set(addr, n, PS_RW);
|
|
return 1;
|
|
}
|
|
|
|
int plat_mem_decommit(uint64_t addr, uint64_t n) {
|
|
if (madvise((void *)(uintptr_t)addr, n, MADV_DONTNEED) || mprotect((void *)(uintptr_t)addr, n, PROT_NONE)) return 0;
|
|
ps_set(addr, n, PS_RESERVED);
|
|
return 1;
|
|
}
|
|
|
|
int plat_mem_protect(uint64_t addr, uint64_t n, int prot) {
|
|
int p = prot == PLAT_PROT_RW ? PROT_READ | PROT_WRITE : prot == PLAT_PROT_READ ? PROT_READ : PROT_NONE;
|
|
if (mprotect((void *)(uintptr_t)addr, n, p)) return 0;
|
|
ps_set(addr, n, (uint8_t)(PS_NONE + prot));
|
|
return 1;
|
|
}
|
|
|
|
int plat_mem_state(uint64_t addr, uint64_t limit, uint64_t *end) {
|
|
if (!pstate || addr >= PS_LIMIT) {
|
|
*end = limit;
|
|
return PLAT_MEM_FREE;
|
|
}
|
|
uint64_t lim = limit < PS_LIMIT ? limit : PS_LIMIT, i = addr / PG;
|
|
uint8_t v = pstate[i];
|
|
while ((i + 1) * PG < lim && pstate[i + 1] == v) i++;
|
|
*end = (i + 1) * PG < lim ? (i + 1) * PG : lim;
|
|
return v == PS_FREE ? PLAT_MEM_FREE : v == PS_RESERVED ? PLAT_MEM_RESERVED : v - PS_NONE;
|
|
}
|
|
|
|
uint64_t plat_mem_written(uint64_t addr, uint64_t n, uint64_t *pages, uint64_t max, int reset) {
|
|
if (!max || !n) return 0;
|
|
if (ww_fd < 0) return UINT64_MAX;
|
|
struct page_region r[256];
|
|
uint64_t got = 0;
|
|
for (uint64_t at = addr, end = addr + n; at < end && got < max;) {
|
|
// Written means: write-protection cleared, on a populated page (present or swapped out) that is not the shared
|
|
// zero page. A page never touched since its commit has no protection marker either and counts as written to
|
|
// the kernel; a read maps the zero page there. Neither is a write, as on Windows.
|
|
struct pm_scan_arg arg = {.size = sizeof arg,
|
|
.flags = reset ? PM_SCAN_WP_MATCHING | PM_SCAN_CHECK_WPASYNC : 0,
|
|
.start = at, .end = end, .vec = (uint64_t)(uintptr_t)r, .vec_len = 256,
|
|
.max_pages = max - got,
|
|
.category_inverted = PAGE_IS_PFNZERO,
|
|
.category_mask = PAGE_IS_WRITTEN | PAGE_IS_PFNZERO,
|
|
.category_anyof_mask = PAGE_IS_PRESENT | PAGE_IS_SWAPPED,
|
|
.return_mask = PAGE_IS_WRITTEN};
|
|
long k = ioctl(pm_fd, PAGEMAP_SCAN, &arg);
|
|
if (k < 0) return UINT64_MAX;
|
|
for (long i = 0; i < k; i++)
|
|
for (uint64_t p = r[i].start; p < r[i].end && got < max; p += PG) pages[got++] = p;
|
|
if (arg.walk_end <= at) break;
|
|
at = arg.walk_end;
|
|
}
|
|
return got;
|
|
}
|
|
|
|
// One handler for the synchronous faults. A SIGSEGV whose page-fault error code has the write bit set goes to the
|
|
// write-fault hook first (memory.c thaws a frozen region); any other fault, in this executable or in a library (Mesa,
|
|
// SDL), then goes to the crash hook (main.c), which reports it and ends the process. Without a crash hook the fault
|
|
// gets the default action. Runs on the thread's signal stack.
|
|
static int (*fault_hook)(uint64_t addr);
|
|
static void (*crash_fn)(const PlatCrash *c);
|
|
|
|
static void fault_handler(int sig, siginfo_t *si, void *uc) {
|
|
ucontext_t *u = uc;
|
|
long err = u->uc_mcontext.gregs[REG_ERR];
|
|
if (sig == SIGSEGV && fault_hook && (err & 2) && fault_hook((uint64_t)(uintptr_t)si->si_addr)) return;
|
|
uintptr_t rip = (uintptr_t)u->uc_mcontext.gregs[REG_RIP];
|
|
Dl_info at, self;
|
|
if (crash_fn) {
|
|
int found = dladdr((void *)rip, &at) && at.dli_fname;
|
|
PlatCrash c = {sig == SIGSEGV ? "access violation" : sig == SIGBUS ? "bus error"
|
|
: sig == SIGILL ? "illegal instruction" : "arithmetic exception",
|
|
(uint32_t)sig, rip, found ? rip - (uintptr_t)at.dli_fbase : rip, -1, 0};
|
|
if (!found) {
|
|
c.module = "";
|
|
} else if (!dladdr((void *)plat_crash_hook, &self) || at.dli_fbase != self.dli_fbase) {
|
|
const char *n = strrchr(at.dli_fname, '/');
|
|
c.module = n ? n + 1 : at.dli_fname;
|
|
}
|
|
if (sig == SIGSEGV || sig == SIGBUS)
|
|
c.access = (err & 16) ? 8 : (err & 2) ? 1 : 0, c.addr = (uint64_t)(uintptr_t)si->si_addr;
|
|
crash_fn(&c);
|
|
}
|
|
signal(sig, SIG_DFL); // returns into the faulting instruction, which faults again with the default action
|
|
}
|
|
|
|
static void fault_install(void) {
|
|
static int done;
|
|
if (done) return;
|
|
done = 1;
|
|
alt_stack();
|
|
struct sigaction sa = {.sa_sigaction = fault_handler, .sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER};
|
|
sigemptyset(&sa.sa_mask);
|
|
sigaction(SIGSEGV, &sa, NULL);
|
|
sigaction(SIGBUS, &sa, NULL);
|
|
sigaction(SIGILL, &sa, NULL);
|
|
sigaction(SIGFPE, &sa, NULL);
|
|
}
|
|
|
|
void plat_mem_fault_hook(int (*fn)(uint64_t addr)) {
|
|
fault_hook = fn;
|
|
fault_install();
|
|
}
|
|
|
|
void plat_crash_hook(void (*fn)(const PlatCrash *c)) {
|
|
crash_fn = fn;
|
|
fault_install();
|
|
}
|
|
|
|
void plat_reexec(void) {
|
|
static char args[16384];
|
|
char *argv[256];
|
|
int argc = 0;
|
|
FILE *f = fopen("/proc/self/cmdline", "rb");
|
|
size_t n = f ? fread(args, 1, sizeof args - 1, f) : 0;
|
|
if (f) fclose(f);
|
|
args[n] = 0;
|
|
for (size_t i = 0; i < n && argc < 255; i += strlen(args + i) + 1) argv[argc++] = args + i;
|
|
argv[argc] = NULL;
|
|
fflush(stdout);
|
|
fflush(stderr);
|
|
execv("/proc/self/exe", argv);
|
|
fprintf(stderr, "FATAL: re-running the process failed (%s)\n", strerror(errno));
|
|
_exit(70);
|
|
}
|
|
|
|
int plat_mem_read(uint64_t addr, void *out, size_t n) {
|
|
struct iovec local = {out, n}, remote = {(void *)(uintptr_t)addr, n};
|
|
return process_vm_readv(getpid(), &local, 1, &remote, 1, 0) == (ssize_t)n;
|
|
}
|
|
|
|
// Not known before the SDL window exists; the Steam Deck has its own profile (cfg.c)
|
|
int plat_screen_size(int *w, int *h) {
|
|
(void)w, (void)h;
|
|
return 0;
|
|
}
|
|
|
|
int plat_mem_readable(uint64_t addr, uint64_t n) {
|
|
uint8_t b;
|
|
uint64_t end = addr + (n ? n : 1);
|
|
for (uint64_t a = addr; a < end; a = (a | 4095) + 1)
|
|
if (!plat_mem_read(a, &b, 1)) return 0;
|
|
return 1;
|
|
}
|
|
|
|
void *plat_aligned_alloc(size_t size, size_t align) {
|
|
void *p = NULL;
|
|
return posix_memalign(&p, align < sizeof(void *) ? sizeof(void *) : align, size) ? NULL : p;
|
|
}
|
|
|
|
void plat_aligned_free(void *p) { free(p); }
|
|
|
|
// A PlatFile is the descriptor + 1, so descriptor 0 is not NULL.
|
|
PlatFile *plat_file_open(const char *path) {
|
|
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
|
return fd < 0 ? NULL : (PlatFile *)(intptr_t)(fd + 1);
|
|
}
|
|
|
|
PlatFile *plat_file_open_unbuffered(const char *path) {
|
|
int fd = open(path, O_RDONLY | O_CLOEXEC | O_DIRECT);
|
|
return fd < 0 ? NULL : (PlatFile *)(intptr_t)(fd + 1);
|
|
}
|
|
|
|
static int fd_of(PlatFile *f) { return (int)(intptr_t)f - 1; }
|
|
|
|
void plat_file_close(PlatFile *f) {
|
|
if (f) close(fd_of(f));
|
|
}
|
|
|
|
int64_t plat_file_pread(PlatFile *f, void *buf, uint32_t n, uint64_t off) {
|
|
ssize_t k;
|
|
do k = pread(fd_of(f), buf, n, (off_t)off);
|
|
while (k < 0 && errno == EINTR);
|
|
return k >= 0 ? k : errno == EFAULT ? -2 : -1;
|
|
}
|
|
|
|
int plat_file_id(PlatFile *f, PlatFileId *out) {
|
|
struct stat st;
|
|
if (fstat(fd_of(f), &st)) return 0;
|
|
*out = (PlatFileId){(uint64_t)st.st_dev, (uint64_t)st.st_ino, (uint64_t)st.st_size,
|
|
(uint64_t)st.st_mtim.tv_sec * 1000000000u + (uint64_t)st.st_mtim.tv_nsec,
|
|
(uint64_t)st.st_ctim.tv_sec * 1000000000u + (uint64_t)st.st_ctim.tv_nsec};
|
|
return 1;
|
|
}
|
|
|
|
const void *plat_file_map(PlatFile *f) {
|
|
struct stat st;
|
|
if (fstat(fd_of(f), &st) || !st.st_size) return NULL;
|
|
void *v = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_SHARED, fd_of(f), 0);
|
|
return v == MAP_FAILED ? NULL : v;
|
|
}
|
|
|
|
int plat_fs_os_error(void) { return errno; }
|
|
|
|
static int fs_err(int e) {
|
|
switch (e) {
|
|
case ENOENT: case ENOTDIR: return PLAT_FS_NOT_FOUND;
|
|
case EEXIST: return PLAT_FS_EXISTS;
|
|
case ENOTEMPTY: return PLAT_FS_NOT_EMPTY;
|
|
case EBUSY: case ETXTBSY: return PLAT_FS_BUSY;
|
|
default: return PLAT_FS_ERROR;
|
|
}
|
|
}
|
|
|
|
int plat_fs_kind(const char *path) {
|
|
struct stat st;
|
|
if (lstat(path, &st)) return PLAT_FS_MISSING;
|
|
return S_ISLNK(st.st_mode) ? PLAT_FS_LINK : S_ISDIR(st.st_mode) ? PLAT_FS_DIR : PLAT_FS_FILE;
|
|
}
|
|
|
|
int plat_fs_mkdir(const char *path) { return mkdir(path, 0755) ? fs_err(errno) : PLAT_FS_OK; }
|
|
|
|
int plat_fs_rmdir(const char *path) {
|
|
struct stat st;
|
|
if (!lstat(path, &st) && S_ISLNK(st.st_mode)) return unlink(path) ? fs_err(errno) : PLAT_FS_OK;
|
|
return rmdir(path) ? fs_err(errno) : PLAT_FS_OK;
|
|
}
|
|
|
|
// Deleting needs write access to the directory, not to the file: a read-only file goes as any other.
|
|
int plat_fs_unlink(const char *path) { return unlink(path) ? fs_err(errno) : PLAT_FS_OK; }
|
|
|
|
// RENAME_NOREPLACE keeps Windows' rule that a rename never replaces; a file system without it (EINVAL) gets a check
|
|
// first, which another process could race but the save directories are ours.
|
|
int plat_fs_rename(const char *from, const char *to, int durable) {
|
|
if (syscall(SYS_renameat2, AT_FDCWD, from, AT_FDCWD, to, RENAME_NOREPLACE)) {
|
|
if (errno != EINVAL && errno != ENOSYS) return fs_err(errno);
|
|
if (plat_fs_kind(to) != PLAT_FS_MISSING) return PLAT_FS_EXISTS;
|
|
if (rename(from, to)) return fs_err(errno);
|
|
}
|
|
if (durable) {
|
|
sync_dir_of(from);
|
|
sync_dir_of(to);
|
|
}
|
|
return PLAT_FS_OK;
|
|
}
|
|
|
|
int plat_fs_link(const char *existing, const char *path) { return link(existing, path) ? fs_err(errno) : PLAT_FS_OK; }
|
|
|
|
int plat_fs_links(const char *path) {
|
|
struct stat st;
|
|
return stat(path, &st) ? 0 : (int)st.st_nlink;
|
|
}
|
|
|
|
void plat_fs_sync_dir(const char *path) {
|
|
int fd = open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
|
if (fd >= 0) {
|
|
fsync(fd);
|
|
close(fd);
|
|
}
|
|
}
|
|
|
|
// Open files never stop a rename here.
|
|
void plat_fs_busy_reason(const char *dir, char *out, size_t cap) {
|
|
(void)dir;
|
|
if (cap) *out = 0;
|
|
}
|
|
|
|
int plat_fs_list(const char *dir, int (*cb)(const char *name, int kind, uint64_t size, void *ctx), void *ctx) {
|
|
DIR *d = opendir(dir);
|
|
if (!d) return fs_err(errno);
|
|
for (struct dirent *e; (e = readdir(d));) {
|
|
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
|
struct stat st;
|
|
if (fstatat(dirfd(d), e->d_name, &st, AT_SYMLINK_NOFOLLOW)) continue;
|
|
int kind = S_ISLNK(st.st_mode) ? PLAT_FS_LINK : S_ISDIR(st.st_mode) ? PLAT_FS_DIR : PLAT_FS_FILE;
|
|
if (!cb(e->d_name, kind, kind == PLAT_FS_FILE ? (uint64_t)st.st_size : 0, ctx)) break;
|
|
}
|
|
closedir(d);
|
|
return PLAT_FS_OK;
|
|
}
|
|
|
|
PlatFile *plat_fs_open(const char *path, int write, int *err) {
|
|
struct stat st;
|
|
int fd = open(path, (write ? O_RDWR : O_RDONLY) | O_CLOEXEC);
|
|
if (fd >= 0 && (fstat(fd, &st) || !S_ISREG(st.st_mode))) {
|
|
close(fd);
|
|
fd = -1, errno = ENOENT;
|
|
}
|
|
if (fd < 0 && err) *err = fs_err(errno);
|
|
return fd < 0 ? NULL : (PlatFile *)(intptr_t)(fd + 1);
|
|
}
|
|
|
|
PlatFile *plat_fs_create(const char *path, int *err) {
|
|
int fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
|
|
if (fd < 0 && err) *err = fs_err(errno);
|
|
return fd < 0 ? NULL : (PlatFile *)(intptr_t)(fd + 1);
|
|
}
|
|
|
|
int64_t plat_file_pwrite(PlatFile *f, const void *buf, uint32_t n, uint64_t off) {
|
|
ssize_t k;
|
|
do k = pwrite(fd_of(f), buf, n, (off_t)off);
|
|
while (k < 0 && errno == EINTR);
|
|
return k;
|
|
}
|
|
|
|
int plat_file_set_size(PlatFile *f, uint64_t size) { return ftruncate(fd_of(f), (off_t)size) ? fs_err(errno) : 0; }
|
|
|
|
int plat_file_fsync(PlatFile *f) { return fsync(fd_of(f)) == 0; }
|
|
|
|
int plat_temp_dir(char *out, size_t cap) {
|
|
const char *t = getenv("TMPDIR");
|
|
if (!t || !*t) t = "/tmp";
|
|
size_t n = strlen(t);
|
|
while (n > 1 && t[n - 1] == '/') n--;
|
|
return snprintf(out, cap, "%.*s", (int)n, t) < (int)cap;
|
|
}
|
|
|
|
uintptr_t plat_exe_base(void) {
|
|
Dl_info d;
|
|
return dladdr((void *)plat_exe_base, &d) ? (uintptr_t)d.dli_fbase : 0;
|
|
}
|
|
|
|
int plat_module_of(uintptr_t addr, char *name, size_t cap, uintptr_t *base) {
|
|
Dl_info d;
|
|
if (!dladdr((void *)addr, &d) || !d.dli_fname) return 0;
|
|
const char *n = strrchr(d.dli_fname, '/');
|
|
*base = (uintptr_t)d.dli_fbase;
|
|
return snprintf(name, cap, "%s", n ? n + 1 : d.dli_fname) < (int)cap;
|
|
}
|
|
|
|
int plat_exe_path(char *out, size_t cap) {
|
|
ssize_t n = readlink("/proc/self/exe", out, cap - 1);
|
|
if (n <= 0 || (size_t)n >= cap - 1) return 0;
|
|
out[n] = 0;
|
|
return 1;
|
|
}
|
|
|
|
size_t plat_getenv(const char *name, char *out, size_t cap) {
|
|
const char *v = getenv(name);
|
|
size_t n = v ? strlen(v) : 0;
|
|
if (!n || n >= cap) return 0;
|
|
memcpy(out, v, n + 1);
|
|
return n;
|
|
}
|
|
|
|
// Cached per thread: plat_rmutex_lock asks on every acquire, hundreds of thousands of times a second in play, and
|
|
// gettid is a full syscall on Linux (Windows reads GetCurrentThreadId from the TEB). A thread's id never changes, and
|
|
// the runtime starts child processes with posix_spawn only, so no forked copy inherits a stale id. noinline: a fiber
|
|
// may resume on another thread (platform.h), and a caller that inlined this could keep the address of tid it computed
|
|
// before a plat_fiber_switch; as a call it reads the slot of the thread it runs on.
|
|
__attribute__((noinline)) uint32_t plat_thread_id(void) {
|
|
static __thread uint32_t tid;
|
|
if (!tid) tid = (uint32_t)syscall(SYS_gettid);
|
|
return tid;
|
|
}
|
|
|
|
// The first set variable of names ("ru_RU.UTF-8") as a BCP-47 tag ("ru-RU") into tag; "" for C and POSIX.
|
|
static void locale_tag(const char *const *names, int n, char *tag, size_t cap) {
|
|
const char *v = NULL;
|
|
for (int i = 0; i < n && !(v && *v); i++) v = getenv(names[i]);
|
|
size_t k = 0;
|
|
if (v && strcmp(v, "C") && strncmp(v, "C.", 2) && strcmp(v, "POSIX"))
|
|
for (; v[k] && v[k] != '.' && v[k] != '@' && k < cap - 1; k++) tag[k] = v[k] == '_' ? '-' : v[k];
|
|
tag[k] = 0;
|
|
}
|
|
|
|
const char *plat_ui_language(void) {
|
|
static char tag[32];
|
|
static const char *const names[] = {"LC_ALL", "LC_MESSAGES", "LANG"};
|
|
if (!tag[0]) locale_tag(names, 3, tag, sizeof tag);
|
|
return tag;
|
|
}
|
|
|
|
const char *plat_user_locale(void) {
|
|
static char tag[32];
|
|
static const char *const names[] = {"LC_ALL", "LC_TIME", "LANG"};
|
|
if (!tag[0]) locale_tag(names, 3, tag, sizeof tag);
|
|
return tag;
|
|
}
|
|
|
|
// Noto CJK as the common distributions and SteamOS install it; one collection covers every CJK language.
|
|
int plat_font_path(const char *lang, char *out, size_t cap) {
|
|
static const char *files[] = {
|
|
"/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
|
|
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
|
"/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
|
|
"/usr/share/fonts/noto/NotoSansCJK-Regular.ttc",
|
|
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
|
|
};
|
|
if (strcmp(lang, "ja") && strcmp(lang, "ko") && strcmp(lang, "zh-Hans") && strcmp(lang, "zh-Hant")) return 0;
|
|
for (size_t i = 0; i < sizeof files / sizeof *files; i++)
|
|
if (plat_file_exists(files[i])) return snprintf(out, cap, "%s", files[i]) < (int)cap;
|
|
return 0;
|
|
}
|
|
|
|
// PRETTY_NAME of /etc/os-release and the kernel release: "SteamOS (Linux 6.16.12-valve)".
|
|
int plat_os_version(char *out, size_t cap) {
|
|
char name[128] = "Linux";
|
|
FILE *f = fopen("/etc/os-release", "r");
|
|
if (f) {
|
|
char line[256];
|
|
while (fgets(line, sizeof line, f))
|
|
if (!strncmp(line, "PRETTY_NAME=", 12)) {
|
|
char *v = line + 12, *e;
|
|
if (*v == '"') v++;
|
|
if ((e = strpbrk(v, "\"\n"))) *e = 0;
|
|
snprintf(name, sizeof name, "%s", v);
|
|
break;
|
|
}
|
|
fclose(f);
|
|
}
|
|
struct utsname u;
|
|
if (uname(&u)) return snprintf(out, cap, "%s", name) < (int)cap;
|
|
return snprintf(out, cap, "%s (Linux %s)", name, u.release) < (int)cap;
|
|
}
|
|
|
|
int plat_is_wine(void) { return 0; }
|
|
|
|
int plat_stderr_path(char *out, size_t cap) {
|
|
struct stat st;
|
|
if (fstat(2, &st) || !S_ISREG(st.st_mode)) return 0;
|
|
ssize_t n = readlink("/proc/self/fd/2", out, cap - 1);
|
|
if (n <= 0 || (size_t)n >= cap - 1) return 0;
|
|
out[n] = 0;
|
|
return 1;
|
|
}
|
|
|
|
// ---- process
|
|
|
|
#ifdef TOTK_PGO_GEN
|
|
// compiler-rt's profile runtime (an instrumented build, tools/build_game.py --pgo gen). It writes its counters from an
|
|
// atexit handler, which _exit skips; __llvm_profile_dump writes them once and makes later writes no-ops.
|
|
int __llvm_profile_dump(void);
|
|
#endif
|
|
|
|
void plat_exit(int code) {
|
|
#ifdef TOTK_PGO_GEN
|
|
__llvm_profile_dump();
|
|
#endif
|
|
fflush(stdout);
|
|
fflush(stderr);
|
|
_exit(code);
|
|
}
|
|
|
|
int plat_setenv(const char *name, const char *value) {
|
|
return (value ? setenv(name, value, 1) : unsetenv(name)) == 0;
|
|
}
|
|
|
|
uint32_t plat_process_id(void) { return (uint32_t)getpid(); }
|
|
|
|
uint64_t plat_process_cpu_ns(void) {
|
|
struct timespec t;
|
|
if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &t)) return 0;
|
|
return (uint64_t)t.tv_sec * 1000000000u + (uint64_t)t.tv_nsec;
|
|
}
|
|
|
|
// Field 22 of /proc/self/stat is the start time in clock ticks after boot; CLOCK_BOOTTIME is now after boot.
|
|
uint64_t plat_process_uptime_ns(void) {
|
|
FILE *f = fopen("/proc/self/stat", "r");
|
|
if (!f) return 0;
|
|
char buf[1024];
|
|
size_t n = fread(buf, 1, sizeof buf - 1, f);
|
|
fclose(f);
|
|
buf[n] = 0;
|
|
char *p = strrchr(buf, ')'); // the command name may hold spaces
|
|
unsigned long long start = 0;
|
|
if (!p || sscanf(p + 2, "%*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %*u %*u %*d %*d %*d %*d %*d %*d %llu",
|
|
&start) != 1)
|
|
return 0;
|
|
struct timespec t;
|
|
clock_gettime(CLOCK_BOOTTIME, &t);
|
|
uint64_t now = (uint64_t)t.tv_sec * 1000000000u + (uint64_t)t.tv_nsec;
|
|
uint64_t at = start * (1000000000u / (uint64_t)sysconf(_SC_CLK_TCK));
|
|
return now > at ? now - at : 0;
|
|
}
|
|
|
|
uint64_t plat_unix_time_ns(void) {
|
|
struct timespec t;
|
|
clock_gettime(CLOCK_REALTIME, &t);
|
|
return (uint64_t)t.tv_sec * 1000000000u + (uint64_t)t.tv_nsec;
|
|
}
|
|
|
|
void plat_local_time(struct tm *out) {
|
|
time_t t = time(NULL);
|
|
localtime_r(&t, out);
|
|
}
|
|
|
|
// The range of the Windows CRT's localtime_s and gmtime_s (1970 to 3000), which glibc exceeds: the game's time
|
|
// service reports times outside it as invalid on both hosts.
|
|
#define PLAT_TIME_MAX 32535215999ll
|
|
|
|
int plat_localtime(int64_t unix_s, struct tm *out) {
|
|
time_t t = (time_t)unix_s;
|
|
return unix_s >= 0 && unix_s <= PLAT_TIME_MAX && localtime_r(&t, out) != NULL;
|
|
}
|
|
|
|
int plat_gmtime(int64_t unix_s, struct tm *out) {
|
|
time_t t = (time_t)unix_s;
|
|
return unix_s >= 0 && unix_s <= PLAT_TIME_MAX && gmtime_r(&t, out) != NULL;
|
|
}
|
|
|
|
long plat_tz_west(void) {
|
|
tzset();
|
|
return timezone;
|
|
}
|
|
|
|
int plat_run_self(const char *env_name, const char *env_value, char *out, size_t cap, uint32_t timeout_ms) {
|
|
// the arguments, NUL-separated, from /proc/self/cmdline
|
|
static char args[16384];
|
|
char *argv[256];
|
|
int argc = 0;
|
|
FILE *f = fopen("/proc/self/cmdline", "rb");
|
|
size_t n = f ? fread(args, 1, sizeof args - 1, f) : 0;
|
|
if (f) fclose(f);
|
|
args[n] = 0;
|
|
for (size_t i = 0; i < n && argc < 255; i += strlen(args + i) + 1) argv[argc++] = args + i;
|
|
argv[argc] = NULL;
|
|
char exe[PATH_MAX];
|
|
if (!argc || !plat_exe_path(exe, sizeof exe)) return -1;
|
|
int p[2];
|
|
if (pipe2(p, O_CLOEXEC)) return -1;
|
|
posix_spawn_file_actions_t fa;
|
|
posix_spawn_file_actions_init(&fa);
|
|
posix_spawn_file_actions_adddup2(&fa, p[1], 1);
|
|
posix_spawn_file_actions_adddup2(&fa, p[1], 2);
|
|
setenv(env_name, env_value, 1);
|
|
pid_t pid;
|
|
int e = posix_spawn(&pid, exe, &fa, NULL, argv, environ);
|
|
unsetenv(env_name);
|
|
posix_spawn_file_actions_destroy(&fa);
|
|
close(p[1]);
|
|
if (e) {
|
|
close(p[0]);
|
|
return -1;
|
|
}
|
|
size_t len = 0;
|
|
char sink[512];
|
|
ssize_t k;
|
|
while (len + 1 < cap && (k = read(p[0], out + len, cap - 1 - len)) != 0)
|
|
if (k > 0) len += (size_t)k;
|
|
else if (errno != EINTR) break;
|
|
if (cap) out[len] = 0;
|
|
while ((k = read(p[0], sink, sizeof sink)) > 0 || (k < 0 && errno == EINTR)) {}
|
|
close(p[0]);
|
|
int status = 0;
|
|
uint64_t end = plat_now_ns() + (uint64_t)timeout_ms * 1000000u;
|
|
while (waitpid(pid, &status, timeout_ms ? WNOHANG : 0) == 0) {
|
|
if (plat_now_ns() >= end) {
|
|
kill(pid, SIGKILL);
|
|
waitpid(pid, &status, 0);
|
|
break;
|
|
}
|
|
plat_sleep_ms(10);
|
|
}
|
|
return WIFEXITED(status) ? WEXITSTATUS(status) : 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0);
|
|
}
|
|
|
|
int plat_random(void *out, size_t n) {
|
|
uint8_t *p = out;
|
|
while (n) {
|
|
ssize_t k = getrandom(p, n, 0);
|
|
if (k < 0 && errno == EINTR) continue;
|
|
if (k <= 0) return 0;
|
|
p += k, n -= (size_t)k;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// ---- desktop
|
|
|
|
int plat_clipboard_set(const char *utf8) { return SDL_SetClipboardText(utf8); }
|
|
|
|
static void reap_main(void *arg) {
|
|
waitpid((pid_t)(intptr_t)arg, NULL, 0);
|
|
}
|
|
|
|
// xdg-open picks the desktop's file manager; it runs detached, so a slow answer does not hold up the caller, and a
|
|
// thread of its own reaps it so it does not stay a zombie.
|
|
int plat_open_folder(const char *path) {
|
|
struct stat st;
|
|
if (stat(path, &st) || !S_ISDIR(st.st_mode)) return 0;
|
|
posix_spawnattr_t a;
|
|
posix_spawnattr_init(&a);
|
|
posix_spawnattr_setflags(&a, POSIX_SPAWN_SETSID);
|
|
char *argv[] = {"xdg-open", (char *)path, NULL};
|
|
pid_t pid;
|
|
int e = posix_spawnp(&pid, "xdg-open", NULL, &a, argv, environ);
|
|
posix_spawnattr_destroy(&a);
|
|
if (e) return 0;
|
|
plat_thread_spawn(reap_main, (void *)(intptr_t)pid, "reap", 64 << 10);
|
|
return 1;
|
|
}
|
|
|
|
// ---- dynamic libraries
|
|
|
|
void *plat_lib_open(const char *name) { return dlopen(name, RTLD_NOW | RTLD_LOCAL); }
|
|
void *plat_lib_sym(void *lib, const char *name) { return lib ? dlsym(lib, name) : NULL; }
|
|
|
|
void plat_lib_error(char *out, size_t cap) {
|
|
const char *e = dlerror();
|
|
snprintf(out, cap, "%s", e ? e : "unknown error");
|
|
}
|
|
|
|
int plat_ngx_core_path(char *out, size_t cap) { return snprintf(out, cap, "libnvidia-ngx.so.1") < (int)cap; }
|
|
|
|
// UTF-8 to UTF-32 wchar_t, without the C library's locale (the runtime never sets one, so mbstowcs would stop at the
|
|
// first non-ASCII byte). Returns 0 on malformed input or when out has no room.
|
|
static int utf8_to_wide(const char *in, wchar_t *out, size_t cap) {
|
|
const unsigned char *u = (const unsigned char *)in;
|
|
size_t n = 0;
|
|
while (*u) {
|
|
unsigned c = *u++, more = c < 0x80 ? 0 : c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 4;
|
|
if (more == 4) return 0;
|
|
if (more) c &= 0x3F >> more;
|
|
for (unsigned k = 0; k < more; k++, u++) {
|
|
if ((*u & 0xC0) != 0x80) return 0;
|
|
c = c << 6 | (*u & 0x3F);
|
|
}
|
|
if (n + 1 >= cap) return 0;
|
|
out[n++] = (wchar_t)c;
|
|
}
|
|
out[n] = 0;
|
|
return 1;
|
|
}
|
|
|
|
int plat_exe_dir_wide(wchar_t *out, size_t cap) {
|
|
char dir[PLAT_PATH_MAX];
|
|
char *cut = plat_exe_path(dir, sizeof dir) ? plat_last_sep(dir) : NULL;
|
|
if (!cut) return 0;
|
|
cut[1] = 0;
|
|
return utf8_to_wide(dir, out, cap);
|
|
}
|
|
|
|
// ---- machine load: /proc/stat and /proc/self/stat for the CPU, hwmon for the temperature, the amdgpu sysfs files of
|
|
// the render device for the GPU.
|
|
|
|
struct PlatLoad {
|
|
uint64_t busy, total, proc; // the previous call's jiffies; total 0 before the first
|
|
uint64_t proc_ns;
|
|
char gpu[256]; // /sys/class/drm/cardN/device of the GPU in use; empty when unknown
|
|
char temp[256]; // the CPU's hwmon temp1_input; empty when unknown
|
|
};
|
|
|
|
static int read_u64_file(const char *path, uint64_t *v) {
|
|
FILE *f = fopen(path, "r");
|
|
if (!f) return 0;
|
|
unsigned long long x;
|
|
int ok = fscanf(f, "%llu", &x) == 1;
|
|
fclose(f);
|
|
if (ok) *v = x;
|
|
return ok;
|
|
}
|
|
|
|
// The hwmon device of the CPU package: k10temp (AMD), coretemp (Intel), or the first that has a temp1_input.
|
|
static void find_cpu_temp(char *out, size_t cap) {
|
|
out[0] = 0;
|
|
DIR *d = opendir("/sys/class/hwmon");
|
|
if (!d) return;
|
|
for (struct dirent *e; (e = readdir(d));) {
|
|
if (e->d_name[0] == '.') continue;
|
|
char path[256], name[64] = "";
|
|
snprintf(path, sizeof path, "/sys/class/hwmon/%s/name", e->d_name);
|
|
FILE *f = fopen(path, "r");
|
|
if (!f) continue;
|
|
if (!fgets(name, sizeof name, f)) name[0] = 0;
|
|
fclose(f);
|
|
name[strcspn(name, "\n")] = 0;
|
|
int cpu = !strcmp(name, "k10temp") || !strcmp(name, "coretemp") || !strcmp(name, "zenpower");
|
|
snprintf(path, sizeof path, "/sys/class/hwmon/%s/temp1_input", e->d_name);
|
|
if ((cpu || !out[0]) && plat_file_exists(path)) snprintf(out, cap, "%s", path);
|
|
if (cpu && out[0]) break;
|
|
}
|
|
closedir(d);
|
|
}
|
|
|
|
// The DRM card with gpu_busy_percent (amdgpu) on PCI bus bus, the game's device. With bus -1 (no VK_EXT_pci_bus_info):
|
|
// of several, the one with the most VRAM in use. None when the game's GPU has no such file (NVIDIA): the overlay then
|
|
// takes its load and VRAM from NVML, not from an idle integrated GPU.
|
|
static void find_gpu(char *out, size_t cap, int bus) {
|
|
out[0] = 0;
|
|
uint64_t best = 0;
|
|
DIR *d = opendir("/sys/class/drm");
|
|
if (!d) return;
|
|
for (struct dirent *e; (e = readdir(d));) {
|
|
if (strncmp(e->d_name, "card", 4) || strchr(e->d_name, '-')) continue;
|
|
char dev[256], path[300];
|
|
snprintf(dev, sizeof dev, "/sys/class/drm/%s/device", e->d_name);
|
|
snprintf(path, sizeof path, "%s/gpu_busy_percent", dev);
|
|
uint64_t v, used = 0;
|
|
if (!read_u64_file(path, &v)) continue;
|
|
if (bus >= 0) { // device links to the PCI function, "0000:03:00.0": domain, bus, device, function
|
|
char real[PATH_MAX];
|
|
unsigned dom, b;
|
|
const char *base = realpath(dev, real) ? strrchr(real, '/') : NULL;
|
|
if (!base || sscanf(base + 1, "%x:%x:", &dom, &b) != 2 || (int)b != bus) continue;
|
|
}
|
|
snprintf(path, sizeof path, "%s/mem_info_vram_used", dev);
|
|
read_u64_file(path, &used);
|
|
if (!out[0] || used > best) snprintf(out, cap, "%s", dev), best = used;
|
|
}
|
|
closedir(d);
|
|
}
|
|
|
|
PlatLoad *plat_load_open(const uint8_t *gpu_luid, int pci_bus) {
|
|
(void)gpu_luid; // Linux has no adapter LUIDs; the PCI bus names the card
|
|
PlatLoad *l = calloc(1, sizeof *l);
|
|
if (!l) return NULL;
|
|
find_cpu_temp(l->temp, sizeof l->temp);
|
|
find_gpu(l->gpu, sizeof l->gpu, pci_bus);
|
|
return l;
|
|
}
|
|
|
|
void plat_load_close(PlatLoad *l) { free(l); }
|
|
|
|
void plat_cpu_load(PlatLoad *l, float *total_pct, float *proc_pct, float *temp_c) {
|
|
*total_pct = *proc_pct = *temp_c = -1;
|
|
if (!l) return;
|
|
FILE *f = fopen("/proc/stat", "r");
|
|
unsigned long long v[10] = {0};
|
|
int n = f ? fscanf(f, "cpu %llu %llu %llu %llu %llu %llu %llu %llu %llu %llu", &v[0], &v[1], &v[2], &v[3], &v[4],
|
|
&v[5], &v[6], &v[7], &v[8], &v[9])
|
|
: 0;
|
|
if (f) fclose(f);
|
|
uint64_t pns = plat_process_cpu_ns();
|
|
if (n >= 8) {
|
|
uint64_t idle = v[3] + v[4], total = 0;
|
|
for (int i = 0; i < 8; i++) total += v[i]; // guest time is already inside user time
|
|
uint64_t busy = total - idle;
|
|
if (l->total && total > l->total) {
|
|
uint64_t dt = total - l->total;
|
|
*total_pct = 100.0f * (float)(busy - l->busy) / (float)dt;
|
|
// the process time is in ns, the machine's in jiffies summed over every CPU
|
|
double dt_ns = (double)dt * 1e9 / (double)sysconf(_SC_CLK_TCK);
|
|
*proc_pct = (float)(100.0 * (double)(pns - l->proc_ns) / dt_ns);
|
|
}
|
|
l->busy = busy, l->total = total, l->proc_ns = pns;
|
|
}
|
|
uint64_t mc;
|
|
if (l->temp[0] && read_u64_file(l->temp, &mc)) *temp_c = (float)mc / 1000.0f;
|
|
}
|
|
|
|
void plat_gpu_load(PlatLoad *l, float *load_pct, float *vram_used_mb) {
|
|
*load_pct = *vram_used_mb = -1;
|
|
if (!l || !l->gpu[0]) return;
|
|
char path[300];
|
|
uint64_t v;
|
|
snprintf(path, sizeof path, "%s/gpu_busy_percent", l->gpu);
|
|
if (read_u64_file(path, &v)) *load_pct = (float)v;
|
|
snprintf(path, sizeof path, "%s/mem_info_vram_used", l->gpu);
|
|
if (read_u64_file(path, &v)) *vram_used_mb = (float)(v >> 20);
|
|
}
|
|
|
|
void plat_mem_info(float *used_mb, float *total_mb, float *proc_mb) {
|
|
*used_mb = *total_mb = *proc_mb = -1;
|
|
FILE *f = fopen("/proc/meminfo", "r");
|
|
if (f) {
|
|
char line[128];
|
|
unsigned long long total = 0, avail = 0, k;
|
|
while (fgets(line, sizeof line, f)) {
|
|
if (sscanf(line, "MemTotal: %llu kB", &k) == 1) total = k;
|
|
else if (sscanf(line, "MemAvailable: %llu kB", &k) == 1) avail = k;
|
|
}
|
|
fclose(f);
|
|
if (total) *total_mb = (float)(total >> 10), *used_mb = (float)((total - avail) >> 10);
|
|
}
|
|
f = fopen("/proc/self/statm", "r");
|
|
unsigned long long size, resident;
|
|
if (f && fscanf(f, "%llu %llu", &size, &resident) == 2) *proc_mb = (float)(resident * (uint64_t)sysconf(_SC_PAGESIZE) >> 20);
|
|
if (f) fclose(f);
|
|
}
|
|
|
|
void plat_process_memory(uint64_t *resident, uint64_t *private_bytes) {
|
|
*resident = *private_bytes = 0;
|
|
FILE *f = fopen("/proc/self/status", "r");
|
|
if (!f) return;
|
|
char line[128];
|
|
unsigned long long k;
|
|
while (fgets(line, sizeof line, f)) {
|
|
if (sscanf(line, "VmRSS: %llu kB", &k) == 1) *resident = (uint64_t)k << 10;
|
|
else if (sscanf(line, "RssAnon: %llu kB", &k) == 1) *private_bytes = (uint64_t)k << 10;
|
|
}
|
|
fclose(f);
|
|
}
|
|
|
|
// Linux overcommits by default, so CommitLimit binds only with vm.overcommit_memory = 2; Committed_AS is what the
|
|
// system has promised. The process's share is its resident anonymous memory, the nearest to Windows' private bytes.
|
|
void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb) {
|
|
*proc_mb = *used_mb = *limit_mb = -1;
|
|
char line[128];
|
|
unsigned long long k;
|
|
FILE *f = fopen("/proc/meminfo", "r");
|
|
if (f) {
|
|
while (fgets(line, sizeof line, f)) {
|
|
if (sscanf(line, "CommitLimit: %llu kB", &k) == 1) *limit_mb = (double)(k >> 10);
|
|
else if (sscanf(line, "Committed_AS: %llu kB", &k) == 1) *used_mb = (double)(k >> 10);
|
|
}
|
|
fclose(f);
|
|
}
|
|
uint64_t resident, private_bytes;
|
|
plat_process_memory(&resident, &private_bytes);
|
|
if (private_bytes) *proc_mb = (double)(private_bytes >> 20);
|
|
}
|
|
|
|
// ---- child processes
|
|
|
|
struct PlatChild {
|
|
pid_t pid;
|
|
int in, out; // in: our write end, out: our read end
|
|
};
|
|
|
|
PlatChild *plat_child_start(const char *exe, const char *arg, uint32_t *err) {
|
|
if (err) *err = 0;
|
|
int in[2], out[2];
|
|
if (pipe2(in, O_CLOEXEC)) return NULL;
|
|
if (pipe2(out, O_CLOEXEC)) {
|
|
close(in[0]), close(in[1]);
|
|
return NULL;
|
|
}
|
|
posix_spawn_file_actions_t fa;
|
|
posix_spawn_file_actions_init(&fa);
|
|
posix_spawn_file_actions_adddup2(&fa, in[0], 0);
|
|
posix_spawn_file_actions_adddup2(&fa, out[1], 1);
|
|
posix_spawn_file_actions_addopen(&fa, 2, "/dev/null", O_WRONLY, 0);
|
|
char *argv[] = {(char *)exe, (char *)arg, NULL};
|
|
pid_t pid;
|
|
int e = posix_spawn(&pid, exe, &fa, NULL, argv, environ);
|
|
posix_spawn_file_actions_destroy(&fa);
|
|
close(in[0]);
|
|
close(out[1]);
|
|
if (e) {
|
|
close(in[1]), close(out[0]);
|
|
if (err) *err = (uint32_t)e;
|
|
return NULL;
|
|
}
|
|
PlatChild *c = malloc(sizeof *c);
|
|
if (!c) {
|
|
kill(pid, SIGKILL);
|
|
waitpid(pid, NULL, 0);
|
|
close(in[1]), close(out[0]);
|
|
return NULL;
|
|
}
|
|
c->pid = pid, c->in = in[1], c->out = out[0];
|
|
return c;
|
|
}
|
|
|
|
void plat_child_stop(PlatChild *c) {
|
|
if (!c) return;
|
|
close(c->in);
|
|
close(c->out);
|
|
kill(c->pid, SIGKILL);
|
|
waitpid(c->pid, NULL, 0);
|
|
free(c);
|
|
}
|
|
|
|
// A child that died makes the write fail with EPIPE; SIGPIPE is ignored for the process (main.c), as Windows has none.
|
|
int plat_child_write(PlatChild *c, const void *data, size_t n) { return write_all(c->in, data, n); }
|
|
|
|
long plat_child_read(PlatChild *c, void *buf, size_t cap) {
|
|
ssize_t k;
|
|
do k = read(c->out, buf, cap);
|
|
while (k < 0 && errno == EINTR);
|
|
return (long)k;
|
|
}
|
|
|
|
// ---- local IPC (platform.h): Discord's Rich Presence Unix domain socket
|
|
struct PlatIpc { int fd; };
|
|
|
|
PlatIpc *plat_ipc_connect(const char *name) {
|
|
const char *dirs[] = {getenv("DISCORD_IPC_PATH"), getenv("XDG_RUNTIME_DIR"), getenv("TMPDIR"), "/tmp"};
|
|
for (size_t d = 0; d < sizeof dirs / sizeof *dirs; d++) {
|
|
if (!dirs[d] || !*dirs[d]) continue;
|
|
char path[sizeof ((struct sockaddr_un *)0)->sun_path];
|
|
if ((size_t)snprintf(path, sizeof path, "%s/%s", dirs[d], name) >= sizeof path) continue;
|
|
int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
|
if (fd < 0) return NULL;
|
|
struct sockaddr_un sa = {0};
|
|
sa.sun_family = AF_UNIX;
|
|
strcpy(sa.sun_path, path);
|
|
if (connect(fd, (struct sockaddr *)&sa, sizeof sa) == 0) {
|
|
PlatIpc *p = malloc(sizeof *p);
|
|
if (!p) {
|
|
close(fd);
|
|
return NULL;
|
|
}
|
|
p->fd = fd;
|
|
return p;
|
|
}
|
|
close(fd); // nothing listens at this path: the next directory may have it
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
int plat_ipc_read(PlatIpc *p, void *buf, size_t n, uint32_t ms) {
|
|
struct pollfd pf = {.fd = p->fd, .events = POLLIN};
|
|
int r = poll(&pf, 1, (int)ms);
|
|
if (r <= 0) return r < 0 && errno != EINTR ? -1 : 0;
|
|
ssize_t got = recv(p->fd, buf, n, 0);
|
|
return got > 0 ? (int)got : -1; // 0 bytes: the peer closed the socket
|
|
}
|
|
|
|
int plat_ipc_write(PlatIpc *p, const void *buf, size_t n) {
|
|
const uint8_t *at = buf;
|
|
while (n) {
|
|
ssize_t put = send(p->fd, at, n, MSG_NOSIGNAL); // a closed peer is an error, not a SIG_PIPE
|
|
if (put <= 0) {
|
|
if (put < 0 && errno == EINTR) continue;
|
|
return 0;
|
|
}
|
|
at += put, n -= put;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
void plat_ipc_close(PlatIpc *p) {
|
|
if (!p) return;
|
|
close(p->fd);
|
|
free(p);
|
|
}
|
|
|
|
|
|
// ---- restart
|
|
// TOTK_RESTART_WAIT names the process that called plat_restart, as its id and start time (field 22 of /proc/<pid>/stat,
|
|
// clock ticks after boot): a process that has only reused the id is not waited for. Waiting keeps the new process off
|
|
// the old one's files (the log it moves at start, the saves) until the old one is gone. The new process gets no
|
|
// descriptor past stderr (glibc 2.34's closefrom file action), and the default signal mask and dispositions, whatever
|
|
// the calling thread had.
|
|
#define RESTART_WAIT "TOTK_RESTART_WAIT"
|
|
|
|
// The start time of process pid (field 22 of its /proc stat line), 0 when it is gone.
|
|
static unsigned long long process_start(int pid) {
|
|
char path[64], buf[1024];
|
|
snprintf(path, sizeof path, "/proc/%d/stat", pid);
|
|
FILE *f = fopen(path, "r");
|
|
if (!f) return 0;
|
|
size_t n = fread(buf, 1, sizeof buf - 1, f);
|
|
fclose(f);
|
|
buf[n] = 0;
|
|
char *p = strrchr(buf, ')'); // the command name may hold spaces
|
|
unsigned long long start = 0;
|
|
if (!p || sscanf(p + 2, "%*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %*u %*u %*d %*d %*d %*d %*d %*d %llu",
|
|
&start) != 1)
|
|
return 0;
|
|
return start;
|
|
}
|
|
|
|
// glibc 2.34 added posix_spawn_file_actions_addclosefrom_np, and the release links against the Steam Runtime's 2.31: a
|
|
// weak reference, used when the running glibc has it. Without it the child inherits the open descriptors.
|
|
extern int posix_spawn_file_actions_addclosefrom_np(posix_spawn_file_actions_t *fa, int from) __attribute__((weak));
|
|
|
|
static void spawn_close_from(posix_spawn_file_actions_t *fa, int from) {
|
|
if (posix_spawn_file_actions_addclosefrom_np) posix_spawn_file_actions_addclosefrom_np(fa, from);
|
|
}
|
|
|
|
int plat_restart(void) {
|
|
// the arguments, NUL-separated, from /proc/self/cmdline
|
|
static char args[16384];
|
|
char *argv[256];
|
|
int argc = 0;
|
|
FILE *f = fopen("/proc/self/cmdline", "rb");
|
|
size_t n = f ? fread(args, 1, sizeof args - 1, f) : 0;
|
|
if (f) fclose(f);
|
|
args[n] = 0;
|
|
for (size_t i = 0; i < n && argc < 255; i += strlen(args + i) + 1) argv[argc++] = args + i;
|
|
argv[argc] = NULL;
|
|
char exe[PATH_MAX], wait[64];
|
|
if (!argc || !plat_exe_path(exe, sizeof exe)) return 0;
|
|
snprintf(wait, sizeof wait, "%d.%llu", (int)getpid(), process_start((int)getpid()));
|
|
posix_spawn_file_actions_t fa;
|
|
posix_spawn_file_actions_init(&fa);
|
|
spawn_close_from(&fa, 3);
|
|
posix_spawnattr_t at;
|
|
posix_spawnattr_init(&at);
|
|
sigset_t none, all;
|
|
sigemptyset(&none);
|
|
sigfillset(&all);
|
|
posix_spawnattr_setsigmask(&at, &none);
|
|
posix_spawnattr_setsigdefault(&at, &all);
|
|
posix_spawnattr_setflags(&at, POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF);
|
|
setenv(RESTART_WAIT, wait, 1);
|
|
pid_t pid;
|
|
int e = posix_spawn(&pid, exe, &fa, &at, argv, environ);
|
|
unsetenv(RESTART_WAIT);
|
|
posix_spawnattr_destroy(&at);
|
|
posix_spawn_file_actions_destroy(&fa);
|
|
return e == 0;
|
|
}
|
|
|
|
int plat_spawn(const char *const *argv, const char *cwd) {
|
|
posix_spawn_file_actions_t fa;
|
|
posix_spawn_file_actions_init(&fa);
|
|
spawn_close_from(&fa, 3);
|
|
posix_spawn_file_actions_addchdir_np(&fa, cwd);
|
|
posix_spawnattr_t at;
|
|
posix_spawnattr_init(&at);
|
|
sigset_t none, all;
|
|
sigemptyset(&none);
|
|
sigfillset(&all);
|
|
posix_spawnattr_setsigmask(&at, &none);
|
|
posix_spawnattr_setsigdefault(&at, &all);
|
|
posix_spawnattr_setflags(&at, POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSID);
|
|
pid_t pid;
|
|
int e = posix_spawn(&pid, argv[0], &fa, &at, (char *const *)argv, environ);
|
|
posix_spawnattr_destroy(&at);
|
|
posix_spawn_file_actions_destroy(&fa);
|
|
return e == 0;
|
|
}
|
|
|
|
int plat_restart_wait(uint32_t timeout_ms) {
|
|
const char *v = getenv(RESTART_WAIT);
|
|
if (!v) return 0;
|
|
int pid = 0;
|
|
unsigned long long start = 0;
|
|
int ok = sscanf(v, "%d.%llu", &pid, &start) == 2 && pid > 0 && start;
|
|
unsetenv(RESTART_WAIT); // the processes this one starts do not wait again
|
|
if (!ok) return 0;
|
|
// The descriptor is taken first: a start time that still matches then proves it is that process, not a new one with
|
|
// the id. It reads as ready once the process has exited, reaped or not.
|
|
int fd = (int)syscall(SYS_pidfd_open, pid, 0);
|
|
if (fd < 0) return 0; // already gone
|
|
int r = 0;
|
|
if (process_start(pid) == start) {
|
|
struct pollfd p = {fd, POLLIN, 0};
|
|
do r = poll(&p, 1, (int)timeout_ms);
|
|
while (r < 0 && errno == EINTR);
|
|
r = r > 0 ? 1 : -1;
|
|
}
|
|
close(fd);
|
|
return r;
|
|
}
|