From ff042df9376ad5e10943d0aec21423eb14147e11 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 7 Aug 2026 23:00:58 +0900 Subject: [PATCH] os/exec: subprocess supervision Captured-async start/poll/cancel plus inherited-stdio run/runstdio with process-group creation, close-on-exec errno marker for exec-setup failure, TERM-grace-KILL escalation, and a signalfd interrupt watch. test/wwfixture/process/main.ww is its self-exec harness, built by both driver stages. --- lib/os/exec/exec.ww | 687 +++++++++++++++++++++++++++++++++ test/wwfixture/process/main.ww | 398 +++++++++++++++++++ 2 files changed, 1085 insertions(+) create mode 100644 lib/os/exec/exec.ww create mode 100644 test/wwfixture/process/main.ww diff --git a/lib/os/exec/exec.ww b/lib/os/exec/exec.ww new file mode 100644 index 00000000..ecd5bd9a --- /dev/null +++ b/lib/os/exec/exec.ww @@ -0,0 +1,687 @@ +// exec runs subprocesses. It owns argv/environment conversion, launch error +// reporting, waiting, deadlines, and process-group cleanup; callers own build +// and test policy. + +package exec; + +import os; +import time; + +@symbol("rt_syscall") fn syscall3(num: i64, a: i64, b: i64, c: i64) i64; + +def SYS_FCNTL: i64 = 72i64; +def F_DUPFD_CLOEXEC: i32 = 1030; + +export type termination = enum i32 { + EXIT = 0, + SIGNAL = 1, + TIMEOUT = 2, + ERROR = 3, +}; + +export type result = struct { + termination: termination, + code: i32, + errno: i32, + cleanuperrno: i32, +}; + +// command is deliberately concrete. argv includes argv[0], env is the full +// environment, and an empty dir inherits the caller's working directory. +// stdoutpath and stderrpath are created exclusively with mode 0600. A zero +// deadline means no timeout. +export type command = struct { + path: str, + argv: []str, + env: []str, + dir: str, + stdoutpath: str, + stderrpath: str, + deadline: time.instant, + grace: time.duration, +}; + +type state = enum i32 { + EMPTY = 0, + RUNNING = 1, + TERM = 2, + KILL = 3, +}; + +export type process = struct { + pid: i32, + state: i32, + markerfd: i32, + marker: [4]u8, + markern: i32, + markerclosed: bool, + deadline: i64, + termdeadline: i64, + grace: time.duration, + hasdeadline: bool, + done: bool, + reaped: bool, + leaderdone: bool, + reaptermination: termination, + reapcode: i32, + result: result, +}; + +// interrupt is the small signalfd watch paired with child launch. start +// restores the pre-watch mask in children before exec, so SIGINT/SIGTERM do +// not remain blocked in executed programs. +export type interrupt = struct { + fd: i32, + oldmask: u64, + active: bool, + errno: i32, +}; + +let launchmaskactive: bool = false; +let launcholdmask: u64 = 0u64; + +fn instantns(i: *time.instant) i64 = { + return i.sec * (time.second: i64) + i.nsec; +}; + +fn nowns() i64 = { + let now: time.instant = time.now(time.clock.monotonic); + return instantns(&now); +}; + +fn reset(p: *process) void = { + p.pid = -1; + p.state = state.EMPTY as i32; + p.markerfd = -1; + p.markern = 0; + p.markerclosed = false; + p.deadline = 0i64; + p.termdeadline = 0i64; + p.grace = 0i64: time.duration; + p.hasdeadline = false; + p.done = false; + p.reaped = false; + p.leaderdone = false; + p.reaptermination = termination.ERROR; + p.reapcode = 0; + p.result.termination = termination.ERROR; + p.result.code = 0; + p.result.errno = 0; + p.result.cleanuperrno = 0; +}; + +fn hasnul(s: str) bool = { + let i: i32 = 0; + for (i < s.len) { + if (s[i] == 0u8) { return true; }; + i += 1; + }; + return false; +}; + +fn valid(c: *command) bool = { + if (c.path.len == 0 || c.argv.len == 0 || c.argv[0].len == 0) { + return false; + }; + if ((c.grace: i64) < 0i64) { return false; }; + if (hasnul(c.path) || hasnul(c.dir) || hasnul(c.stdoutpath) + || hasnul(c.stderrpath)) { + return false; + }; + let i: i32 = 0; + for (i < c.argv.len) { + if (hasnul(c.argv[i])) { return false; }; + i += 1; + }; + i = 0; + for (i < c.env.len) { + if (hasnul(c.env[i])) { return false; }; + i += 1; + }; + return true; +}; + +fn cstr(s: str) *u8 = { + let b: []u8 = alloc([], (s.len + 1): u64)!; + b.len = s.len + 1; + let i: i32 = 0; + for (i < s.len) { b[i] = s[i]; i += 1; }; + b[s.len] = 0u8; + return b.ptr; +}; + +fn ctable(v: []str) []*u8 = { + let r: []*u8 = alloc([], (v.len + 1): u64)!; + let i: i32 = 0; + for (i < v.len) { append(r, cstr(v[i])); i += 1; }; + append(r, nil: *u8); + return r; +}; + +fn setcleanup(r: *result, rc: i32) void = { + if (rc >= 0 || rc == -3 || r.cleanuperrno != 0) { return; }; + r.cleanuperrno = -rc; +}; + +fn closefd(r: *result, fd: i32) void = { + if (fd >= 0) { setcleanup(r, os.close(fd)); }; +}; + +// Capture and marker descriptors must not occupy stdin/stdout/stderr. The +// caller may legitimately have closed any of those descriptors before run. +fn safefd(r: *result, fd: i32) i32 = { + if (fd > os.STDERR_FILENO) { return fd; }; + let moved: i32 = syscall3(SYS_FCNTL, fd: i64, + F_DUPFD_CLOEXEC: i64, (os.STDERR_FILENO + 1): i64): i32; + closefd(r, fd); + return moved; +}; + +fn fail(p: *process, rc: i32) void = { + p.result.termination = termination.ERROR; + p.result.errno = rc; + if (p.result.errno < 0) { p.result.errno = -p.result.errno; }; + if (p.result.errno == 0) { p.result.errno = 5; }; + p.done = true; +}; + +fn childmark(fd: i32, rc: i32) void = { + let ei: i32 = rc; + if (ei < 0) { ei = -ei; }; + if (ei == 0) { ei = 5; }; + let e: u32 = ei: u32; + let b: [4]u8; + b[0] = e: u8; + b[1] = (e >> 8u32): u8; + b[2] = (e >> 16u32): u8; + b[3] = (e >> 24u32): u8; + for (true) { + let n: i64 = os.write(fd, &b[0], size([4]u8)); + if (n == size([4]u8): i64) { break; }; + if (n != -4i64) { os.exit(126); }; + }; + os.close(fd); + os.exit(127); +}; + +fn childclose(fd: i32, markerfd: i32) void = { + let rc: i32 = os.close(fd); + if (rc < 0) { childmark(markerfd, rc); }; +}; + +fn child(c: *command, av: []*u8, ep: []*u8, outfd: i32, + errfd: i32, markerread: i32, markerwrite: i32) void = { + let rc: i32 = os.setpgid(0, 0); + if (rc < 0) { childmark(markerwrite, rc); }; + if (launchmaskactive) { + rc = os.sigprocmask(os.SIG_SETMASK, &launcholdmask, nil: *u64); + if (rc < 0) { childmark(markerwrite, rc); }; + }; + childclose(markerread, markerwrite); + rc = os.dup2(outfd, os.STDOUT_FILENO); + if (rc < 0) { childmark(markerwrite, rc); }; + rc = os.dup2(errfd, os.STDERR_FILENO); + if (rc < 0) { childmark(markerwrite, rc); }; + childclose(outfd, markerwrite); + childclose(errfd, markerwrite); + if (c.dir.len != 0) { + rc = os.chdir(c.dir); + if (rc < 0) { childmark(markerwrite, rc); }; + }; + rc = os.execve(c.path, av.ptr, ep.ptr); + childmark(markerwrite, rc); +}; + +export fn start(p: *process, c: *command) void = { + reset(p); + p.deadline = instantns(&c.deadline); + p.hasdeadline = c.deadline.sec != 0i64 || c.deadline.nsec != 0i64; + p.grace = c.grace; + if (!valid(c)) { fail(p, 22); return; }; + let outfd: i32 = os.open(c.stdoutpath, + os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384); + if (outfd < 0) { fail(p, outfd); return; }; + outfd = safefd(&p.result, outfd); + if (outfd < 0) { fail(p, outfd); return; }; + let errfd: i32 = os.open(c.stderrpath, + os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384); + if (errfd < 0) { + fail(p, errfd); + closefd(&p.result, outfd); + return; + }; + errfd = safefd(&p.result, errfd); + if (errfd < 0) { + fail(p, errfd); + closefd(&p.result, outfd); + return; + }; + let marker: [2]i32; + let rc: i32 = os.pipe2(&marker, os.O_CLOEXEC | os.O_NONBLOCK); + if (rc < 0) { + fail(p, rc); + closefd(&p.result, outfd); + closefd(&p.result, errfd); + return; + }; + marker[0] = safefd(&p.result, marker[0]); + if (marker[0] < 0) { + fail(p, marker[0]); + closefd(&p.result, outfd); + closefd(&p.result, errfd); + closefd(&p.result, marker[1]); + return; + }; + marker[1] = safefd(&p.result, marker[1]); + if (marker[1] < 0) { + fail(p, marker[1]); + closefd(&p.result, outfd); + closefd(&p.result, errfd); + closefd(&p.result, marker[0]); + return; + }; + let av: []*u8 = ctable(c.argv); + let ep: []*u8 = ctable(c.env); + let pid: i32 = os.fork(); + if (pid < 0) { + fail(p, pid); + closefd(&p.result, outfd); + closefd(&p.result, errfd); + closefd(&p.result, marker[0]); + closefd(&p.result, marker[1]); + return; + }; + if (pid == 0) { child(c, av, ep, outfd, errfd, marker[0], marker[1]); }; + p.pid = pid; + p.state = state.RUNNING as i32; + p.markerfd = marker[0]; + closefd(&p.result, outfd); + closefd(&p.result, errfd); + closefd(&p.result, marker[1]); + // The child creates its group before user code. The parent repeats the + // operation to close the cancel-before-child-runs race. + let pgrc: i32 = os.setpgid(pid, pid); + if (pgrc < 0 && pgrc != -13 && pgrc != -3) { + p.result.termination = termination.ERROR; + p.result.errno = -pgrc; + setcleanup(&p.result, os.kill(-pid, os.SIGTERM)); + p.termdeadline = nowns() + (p.grace: i64); + p.state = state.TERM as i32; + }; +}; + +fn decodemarker(p: *process) void = { + if (p.markern != 4) { + if (p.result.termination != termination.TIMEOUT) { + p.result.termination = termination.ERROR; + }; + if (p.result.errno == 0) { p.result.errno = 71; }; + return; + }; + let e: u32 = p.marker[0]: u32 + | ((p.marker[1]: u32) << 8u32) + | ((p.marker[2]: u32) << 16u32) + | ((p.marker[3]: u32) << 24u32); + if (p.result.termination != termination.TIMEOUT) { + p.result.termination = termination.ERROR; + }; + if (p.result.errno == 0) { p.result.errno = e: i32; }; + if (p.result.errno == 0) { p.result.errno = 5; }; +}; + +fn closemarker(p: *process) void = { + if (p.markerfd < 0) { return; }; + closefd(&p.result, p.markerfd); + p.markerfd = -1; +}; + +fn readmarker(p: *process) void = { + if (p.markerfd < 0 || p.markerclosed || p.markern == 4) { return; }; + for (true) { + let n: i64 = os.read(p.markerfd, &p.marker[p.markern], + (4 - p.markern): u64); + if (n > 0) { + p.markern += n: i32; + if (p.markern == 4) { decodemarker(p); return; }; + continue; + }; + if (n == 0) { p.markerclosed = true; return; }; + if (n == -4i64) { continue; }; + if (n == -11i64) { return; }; + p.markerclosed = true; + if (p.result.termination != termination.TIMEOUT) { + p.result.termination = termination.ERROR; + }; + if (p.result.errno == 0) { p.result.errno = (-n): i32; }; + return; + }; +}; + +fn groupsignal(p: *process, sig: i32) i32 = { + let rc: i32 = os.kill(-p.pid, sig); + setcleanup(&p.result, rc); + return rc; +}; + +fn leadersignal(p: *process, sig: i32) i32 = { + let rc: i32 = os.kill(p.pid, sig); + setcleanup(&p.result, rc); + return rc; +}; + +fn leaderterminal(p: *process) bool = { + return p.reaped || p.leaderdone; +}; + +// 0 means no group member remains, 1 means the group is live, and -1 is an +// explicit cleanup failure. +fn groupstate(p: *process) i32 = { + let rc: i32 = os.kill(-p.pid, 0); + if (rc == -3) { return 0; }; + if (rc == 0) { return 1; }; + setcleanup(&p.result, rc); + return -1; +}; + +fn finish(p: *process) void = { + closemarker(p); + p.state = state.EMPTY as i32; + p.done = true; +}; + +fn startterm(p: *process, now: i64) void = { + groupsignal(p, os.SIGTERM); + p.termdeadline = now + (p.grace: i64); + p.state = state.TERM as i32; +}; + +fn startkill(p: *process) void = { + groupsignal(p, os.SIGKILL); + if (!leaderterminal(p)) { leadersignal(p, os.SIGKILL); }; + p.state = state.KILL as i32; +}; + +fn advancecleanup(p: *process, now: i64) bool = { + if (p.state == (state.RUNNING as i32)) { return false; }; + let gs: i32 = groupstate(p); + if (leaderterminal(p) && gs == 0) { finish(p); return true; }; + if (p.state == (state.TERM as i32) && now >= p.termdeadline) { + startkill(p); + gs = groupstate(p); + }; + if (p.state == (state.KILL as i32)) { + groupsignal(p, os.SIGKILL); + if (!leaderterminal(p)) { leadersignal(p, os.SIGKILL); }; + gs = groupstate(p); + if (leaderterminal(p) && (gs == 0 || gs < 0)) { + finish(p); + return true; + }; + }; + return false; +}; + +fn reap(p: *process, status: i32) void = { + p.reaped = true; + p.leaderdone = true; + if (os.wifexited(status)) { + p.reaptermination = termination.EXIT; + p.reapcode = os.wexitstatus(status); + } else if (os.wifsignaled(status)) { + p.reaptermination = termination.SIGNAL; + p.reapcode = os.wtermsig(status); + } else { + if (p.result.errno == 0) { p.result.errno = 71; }; + }; + readmarker(p); + if (p.markern == 0 && p.markerclosed) { + if (p.result.termination != termination.TIMEOUT + && p.result.errno == 0) { + p.result.termination = p.reaptermination; + p.result.code = p.reapcode; + }; + } else if (p.markern != 4 && p.result.errno == 0) { + decodemarker(p); + }; + if (p.state == (state.RUNNING as i32)) { + startterm(p, nowns()); + }; +}; + +fn waitleader(p: *process) bool = { + if (p.reaped) { return true; }; + let status: i32 = 0; + for (true) { + let pid: i32 = os.wait4(p.pid, &status, os.WNOHANG, nil: *void); + if (pid == p.pid) { reap(p, status); return true; }; + if (pid == 0) { return false; }; + if (pid == -4) { continue; }; + if (p.result.errno == 0) { p.result.errno = -pid; }; + if (pid == -10) { p.leaderdone = true; }; + if (p.result.termination != termination.TIMEOUT) { + p.result.termination = termination.ERROR; + }; + if (p.state == (state.RUNNING as i32)) { startterm(p, nowns()); }; + return false; + }; + return false; +}; + +export fn poll(p: *process) bool = { + if (p.done) { return true; }; + if (p.pid <= 0 || p.state == (state.EMPTY as i32)) { return true; }; + readmarker(p); + waitleader(p); + let now: i64 = nowns(); + if (p.state == (state.RUNNING as i32) && p.hasdeadline + && p.markerclosed && p.markern == 0 && p.result.errno == 0 + && now >= p.deadline) { + p.result.termination = termination.TIMEOUT; + p.result.code = 0; + startterm(p, now); + }; + advancecleanup(p, now); + return p.done; +}; + +export fn cancel(p: *process) void = { + if (p.done || p.pid <= 0 || p.state == (state.EMPTY as i32)) { return; }; + if (p.state == (state.RUNNING as i32)) { startterm(p, nowns()); }; +}; + +export fn run(c: *command, out: *result) void = { + let watch: interrupt; + if (!interruptopen(&watch)) { + out.termination = termination.ERROR; + out.code = 0; + out.errno = watch.errno; + out.cleanuperrno = 0; + return; + }; + let p: process; + let interrupted: i32 = 0; + start(&p, c); + for (!p.done) { + let signo: i32 = interruptpoll(&watch); + if (signo != 0) { + if (signo > 0 && interrupted == 0) { interrupted = signo; }; + cancel(&p); + }; + if (poll(&p)) { break; }; + time.sleep(time.millisecond, time.clock.monotonic); + }; + interruptclose(&watch); + if (watch.errno != 0 && p.result.cleanuperrno == 0) { + p.result.cleanuperrno = watch.errno; + }; + *out = p.result; + if (interrupted != 0) { os.kill(os.getpid(), interrupted); }; +}; + +// runstdio executes one child with the caller's stdin/stdout/stderr and waits +// only for that child. It deliberately does not create or clean a process +// group: commands such as a compiler driver or a user program own any +// descendants they create. env is the complete environment; an empty slice +// requests an empty environment. The close-on-exec marker keeps an execve +// setup failure distinct from a real program exit 127. +export fn runstdio(path: str, argv: []str, env: []str, out: *result) void = { + out.termination = termination.ERROR; + out.code = 0; + out.errno = 0; + out.cleanuperrno = 0; + if (path.len == 0 || argv.len == 0 || argv[0].len == 0 + || hasnul(path)) { + out.errno = 22; + return; + }; + let i: i32 = 0; + for (i < argv.len) { + if (hasnul(argv[i])) { out.errno = 22; return; }; + i += 1; + }; + i = 0; + for (i < env.len) { + if (hasnul(env[i])) { out.errno = 22; return; }; + i += 1; + }; + + let marker: [2]i32; + let rc: i32 = os.pipe2(&marker, os.O_CLOEXEC); + if (rc < 0) { out.errno = -rc; return; }; + marker[0] = safefd(out, marker[0]); + if (marker[0] < 0) { + out.errno = -marker[0]; + closefd(out, marker[1]); + return; + }; + marker[1] = safefd(out, marker[1]); + if (marker[1] < 0) { + out.errno = -marker[1]; + closefd(out, marker[0]); + return; + }; + let av: []*u8 = ctable(argv); + let ep: []*u8 = ctable(env); + let pid: i32 = os.fork(); + if (pid < 0) { + out.errno = -pid; + closefd(out, marker[0]); + closefd(out, marker[1]); + return; + }; + if (pid == 0) { + if (launchmaskactive) { + rc = os.sigprocmask(os.SIG_SETMASK, &launcholdmask, + nil: *u64); + if (rc < 0) { childmark(marker[1], rc); }; + }; + childclose(marker[0], marker[1]); + rc = os.execve(path, av.ptr, ep.ptr); + childmark(marker[1], rc); + }; + closefd(out, marker[1]); + + let status: i32 = 0; + let waited: i32 = 0; + for (true) { + waited = os.wait4(pid, &status, 0, nil: *void); + if (waited == pid) { break; }; + if (waited == -4) { continue; }; + out.errno = -waited; + if (out.errno <= 0) { out.errno = 5; }; + break; + }; + + let mark: [4]u8; + let markn: i32 = 0; + for (waited == pid && markn < 4) { + let n: i64 = os.read(marker[0], &mark[markn], (4 - markn): u64); + if (n > 0) { markn += n: i32; continue; }; + if (n == 0) { break; }; + if (n == -4i64) { continue; }; + out.errno = (-n): i32; + break; + }; + closefd(out, marker[0]); + if (waited != pid || out.errno != 0) { return; }; + if (markn != 0) { + if (markn != 4) { out.errno = 71; return; }; + let e: u32 = mark[0]: u32 + | ((mark[1]: u32) << 8u32) + | ((mark[2]: u32) << 16u32) + | ((mark[3]: u32) << 24u32); + out.errno = e: i32; + if (out.errno == 0) { out.errno = 5; }; + out.code = 127; + return; + }; + if (os.wifexited(status)) { + out.termination = termination.EXIT; + out.code = os.wexitstatus(status); + return; + }; + if (os.wifsignaled(status)) { + out.termination = termination.SIGNAL; + out.code = os.wtermsig(status); + return; + }; + out.errno = 71; +}; + +export fn interruptopen(w: *interrupt) bool = { + w.fd = -1; + w.oldmask = 0u64; + w.active = false; + w.errno = 0; + if (launchmaskactive) { w.errno = 16; return false; }; + let mask: u64 = (1u64 << ((os.SIGINT - 1): u64)) + | (1u64 << ((os.SIGTERM - 1): u64)); + let rc: i32 = os.sigprocmask(os.SIG_BLOCK, &mask, &w.oldmask); + if (rc < 0) { w.errno = -rc; return false; }; + w.fd = os.signalfd(-1, &mask, os.SFD_CLOEXEC | os.SFD_NONBLOCK); + if (w.fd < 0) { + w.errno = -w.fd; + os.sigprocmask(os.SIG_SETMASK, &w.oldmask, nil: *u64); + w.fd = -1; + return false; + }; + launcholdmask = w.oldmask; + launchmaskactive = true; + w.active = true; + return true; +}; + +export fn interruptpoll(w: *interrupt) i32 = { + if (!w.active || w.fd < 0) { w.errno = 22; return -22; }; + let info: [128]u8; + for (true) { + let n: i64 = os.read(w.fd, &info[0], size([128]u8)); + if (n == -4i64) { continue; }; + if (n == -11i64) { return 0; }; + if (n < 0) { w.errno = (-n): i32; return n: i32; }; + if (n != size([128]u8): i64) { w.errno = 71; return -71; }; + let signo: u32 = info[0]: u32 + | ((info[1]: u32) << 8u32) + | ((info[2]: u32) << 16u32) + | ((info[3]: u32) << 24u32); + return signo: i32; + }; + return 0; +}; + +export fn interruptclose(w: *interrupt) bool = { + if (!w.active) { return w.errno == 0; }; + let ok: bool = true; + let rc: i32 = os.close(w.fd); + if (rc < 0) { w.errno = -rc; ok = false; }; + rc = os.sigprocmask(os.SIG_SETMASK, &w.oldmask, nil: *u64); + if (rc < 0) { + if (w.errno == 0) { w.errno = -rc; }; + ok = false; + }; + launchmaskactive = false; + launcholdmask = 0u64; + w.fd = -1; + w.active = false; + return ok; +}; diff --git a/test/wwfixture/process/main.ww b/test/wwfixture/process/main.ww new file mode 100644 index 00000000..2b5d023f --- /dev/null +++ b/test/wwfixture/process/main.ww @@ -0,0 +1,398 @@ +package main; + +import os; +import os.exec; +import strings; +import temp; +import time; + +fn eq(a: str, b: str) bool = { + if (a.len != b.len) { return false; }; + let i: i32 = 0; + for (i < a.len) { + if (a[i] != b[i]) { return false; }; + i += 1; + }; + return true; +}; + +fn writeexact(fd: i32, s: str) bool = { + match (os.writeall(fd, s.ptr, s.len: u64)) { + case let n: i64 => return n == s.len: i64; + case let e: os.oserror => return false; + }; +}; + +fn readfile(path: str) str = { + let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + assert(!(fd < 0)); + let sr: (i64 | os.oserror) = os.filesize(fd); + let n: i64 = -1i64; + match (sr) { + case let v: i64 => n = v; + case let e: os.oserror => abort("filesize failed"); + }; + assert(!(n < 0i64)); + let b: []u8 = alloc([], (n + 1i64): u64)!; + b.len = (n + 1i64): i32; + let rr: (i64 | os.oserror) = os.readall(fd, b.ptr, n: u64); + assert(!(os.close(fd) != 0)); + let got: i64 = -1i64; + match (rr) { + case let v: i64 => got = v; + case let e: os.oserror => abort("read failed"); + }; + assert(got == n); + let ni: i32 = n: i32; + b[ni] = 0u8; + let out: str; + out.ptr = b.ptr; + out.len = n: i32; + return out; +}; + +fn waitchild(pid: i32) i32 = { + let status: i32 = 0; + for (true) { + let got: i32 = os.wait4(pid, &status, 0i32, nil: *void); + if (got == pid) { return status; }; + if (got != -4) { return -1; }; + }; + return -1; +}; + +fn childmode(args: []str) void = { + let mode: str = args[2]; + if (eq(mode, "exit0")) { os.exit(0); }; + if (eq(mode, "exit7")) { os.exit(7); }; + if (eq(mode, "exit127")) { os.exit(127); }; + if (eq(mode, "route")) { + if (args.len != 5 || !eq(args[3], "token")) { os.exit(129); }; + match (os.getenv("ROUTE")) { + case let value: str => if (!eq(value, "value")) { os.exit(130); }; + case void => os.exit(131); + }; + let cwdbytes: [4096]u8; + let n: i64 = os.getcwd(&cwdbytes[0], size([4096]u8)); + if (n <= 1i64 || n > size([4096]u8): i64) { os.exit(132); }; + let cwd: str; + cwd.ptr = &cwdbytes[0]; + cwd.len = (n - 1i64): i32; + if (!eq(cwd, args[4])) { os.exit(133); }; + if (!writeexact(os.STDOUT_FILENO, "route stdout\n")) { os.exit(134); }; + if (!writeexact(os.STDERR_FILENO, "route stderr\n")) { os.exit(135); }; + os.exit(0); + }; + if (eq(mode, "signal")) { + os.kill(os.getpid(), os.SIGKILL); + os.exit(120); + }; + if (eq(mode, "hold")) { + for (true) { time.sleep(time.second, time.clock.monotonic); }; + }; + if (eq(mode, "resistdesc")) { + let mask: u64 = 1u64 << ((os.SIGTERM - 1): u64); + if (os.sigprocmask(os.SIG_BLOCK, &mask, nil: *u64) != 0) { + os.exit(121); + }; + let pid: i32 = os.fork(); + if (pid < 0) { os.exit(122); }; + if (pid > 0 && args.len == 4) { + let readyfd: i32 = os.open(args[3], + os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384); + if (readyfd < 0) { os.exit(142); }; + let me: u32 = os.getpid(): u32; + let ready: [4]u8; + ready[0] = me: u8; + ready[1] = (me >> 8u32): u8; + ready[2] = (me >> 16u32): u8; + ready[3] = (me >> 24u32): u8; + if (os.write(readyfd, &ready[0], 4u64) != 4i64) { + os.exit(143); + }; + if (os.close(readyfd) != 0) { os.exit(144); }; + }; + for (true) { time.sleep(time.second, time.clock.monotonic); }; + }; + if (eq(mode, "exitdesc")) { + let mask: u64 = 1u64 << ((os.SIGTERM - 1): u64); + if (os.sigprocmask(os.SIG_BLOCK, &mask, nil: *u64) != 0) { + os.exit(124); + }; + let ready: [2]i32; + if (os.pipe(&ready) != 0) { os.exit(125); }; + let pid: i32 = os.fork(); + if (pid < 0) { os.exit(126); }; + let b: [1]u8 = [1u8]; + if (pid == 0) { + os.close(ready[0]); + if (os.write(ready[1], &b[0], 1u64) != 1i64) { os.exit(127); }; + os.close(ready[1]); + for (true) { time.sleep(time.second, time.clock.monotonic); }; + }; + os.close(ready[1]); + if (os.read(ready[0], &b[0], 1u64) != 1i64) { os.exit(128); }; + os.close(ready[0]); + os.exit(0); + }; + os.exit(123); +}; + +fn fillcommand(c: *exec.command, self: str, mode: str, root: str, + name: str, lifetime: time.duration, grace: time.duration) void = { + let av: []str = alloc([], 3u64)!; + append(av, self); + append(av, "child"); + append(av, mode); + let env: []str = alloc([], 2u64)!; + append(env, "PATH=/usr/bin:/bin"); + append(env, "LC_ALL=C"); + c.path = self; + c.argv = av; + c.env = env; + c.dir = root; + c.stdoutpath = strings.concat(root, "/", name, ".out"); + c.stderrpath = strings.concat(root, "/", name, ".err"); + c.deadline = time.add(time.now(time.clock.monotonic), lifetime); + c.grace = grace; +}; + +fn waitdone(p: *exec.process) void = { + for (!exec.poll(p)) { + time.sleep(time.millisecond, time.clock.monotonic); + }; +}; + +fn cancelall(ps: []exec.process) bool = { + let i: i32 = 0; + for (i < ps.len) { exec.cancel(&ps[i]); i += 1; }; + let pending: bool = true; + for (pending) { + pending = false; + i = 0; + for (i < ps.len) { + if (!exec.poll(&ps[i])) { pending = true; }; + i += 1; + }; + if (pending) { time.sleep(time.millisecond, time.clock.monotonic); }; + }; + let ok: bool = true; + i = 0; + for (i < ps.len) { + if (ps[i].pid > 0 && (ps[i].result.errno != 0 + || ps[i].result.cleanuperrno != 0)) { ok = false; }; + i += 1; + }; + return ok; +}; + +fn main() void = { + let args: []str = os.args(); + if (args.len >= 3 && eq(args[1], "child")) { + childmode(args); + }; + assert(!(args.len == 0)); + let self: str = strings.dup(args[0]); + if (self.len == 0 || self[0] != '/': u8) { + let cwdbuf: [4096]u8; + let cwdn: i64 = os.getcwd(&cwdbuf[0], size([4096]u8)); + assert(!(cwdn <= 1i64 || cwdn > size([4096]u8): i64)); + let cwd: str; + cwd.ptr = &cwdbuf[0]; + cwd.len = (cwdn - 1i64): i32; + self = strings.concat(cwd, "/", self); + }; + let root: str = strings.dup(temp.dir()); + let p: exec.process; + let c: exec.command; + let r: exec.result; + let grace: time.duration = + (20i64 * (time.millisecond: i64)): time.duration; + + fillcommand(&c, self, "exit0", root, "exit0", + time.second, grace); + exec.run(&c, &r); + assert(r.termination == exec.termination.EXIT && r.code == 0); + assert(r.errno == 0 && r.cleanuperrno == 0); + + fillcommand(&c, self, "exitdesc", root, "exitdesc", + time.second, grace); + let descbefore: time.instant = time.now(time.clock.monotonic); + exec.start(&p, &c); + waitdone(&p); + let descafter: time.instant = time.now(time.clock.monotonic); + assert(p.result.termination == exec.termination.EXIT + && p.result.code == 0); + assert(!((time.diff(descbefore, descafter): i64) < grace: i64)); + assert(!(os.kill(-p.pid, 0) != -3)); + + fillcommand(&c, self, "exit7", root, "exit7", + time.second, grace); + exec.run(&c, &r); + assert(r.termination == exec.termination.EXIT && r.code == 7); + + fillcommand(&c, self, "exit127", root, "exit127", + time.second, grace); + exec.run(&c, &r); + assert(r.termination == exec.termination.EXIT && r.code == 127); + assert(r.errno == 0); + + fillcommand(&c, self, "route", root, "route", + time.second, grace); + let routeargv: []str = c.argv; + append(routeargv, "token"); + append(routeargv, root); + c.argv = routeargv; + let routeenv: []str = c.env; + append(routeenv, "ROUTE=value"); + c.env = routeenv; + exec.run(&c, &r); + assert(r.termination == exec.termination.EXIT && r.code == 0); + assert(eq(readfile(c.stdoutpath), "route stdout\n")); + assert(eq(readfile(c.stderrpath), "route stderr\n")); + + fillcommand(&c, self, "signal", root, "signal", + time.second, grace); + exec.run(&c, &r); + assert(r.termination == exec.termination.SIGNAL); + assert(!(r.code != os.SIGKILL)); + + fillcommand(&c, "/no/such/exec-program", "exit0", root, "execfail", + time.second, grace); + exec.run(&c, &r); + assert(r.termination == exec.termination.ERROR && r.errno == 2); + + fillcommand(&c, "/no/such/exec-program", "exit0", root, "pastfail", + time.second, grace); + c.deadline = time.add(time.now(time.clock.monotonic), + (-1i64 * (time.second: i64)): time.duration); + exec.run(&c, &r); + assert(r.termination == exec.termination.ERROR && r.errno == 2); + + fillcommand(&c, self, "exit0", root, "chdirfail", + time.second, grace); + c.dir = strings.concat(root, "/missing-directory"); + exec.run(&c, &r); + assert(r.termination == exec.termination.ERROR && r.errno == 2); + + fillcommand(&c, self, "resistdesc", root, "timeout", + (300i64 * (time.millisecond: i64)): time.duration, grace); + let until: i64 = time.diff(time.now(time.clock.monotonic), + c.deadline): i64; + assert(!(until < 250i64 * (time.millisecond: i64))); + let timeoutbefore: time.instant = time.now(time.clock.monotonic); + exec.start(&p, &c); + waitdone(&p); + let timeoutafter: time.instant = time.now(time.clock.monotonic); + assert(p.result.termination == exec.termination.TIMEOUT); + assert(!((time.diff(timeoutbefore, timeoutafter): i64) + < 250i64 * (time.millisecond: i64))); + assert(!(os.kill(-p.pid, 0) != -3)); + + let exists: str = strings.concat(root, "/exists.out"); + let fd: i32 = os.open(exists, + os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384); + assert(!(fd < 0)); + assert(!(os.close(fd) != 0)); + fillcommand(&c, self, "exit0", root, "unused", + time.second, grace); + c.stdoutpath = exists; + exec.run(&c, &r); + assert(r.termination == exec.termination.ERROR && r.errno == 17); + + let readyfile: str = strings.concat(root, "/runinterrupt.ready"); + let coordinator: i32 = os.fork(); + assert(!(coordinator < 0)); + if (coordinator == 0) { + let ic: exec.command; + let ir: exec.result; + fillcommand(&ic, self, "resistdesc", root, "runinterrupt", + (10i64 * (time.second: i64)): time.duration, grace); + let iav: []str = ic.argv; + append(iav, readyfile); + ic.argv = iav; + exec.run(&ic, &ir); + os.exit(145); + }; + let readywait: i32 = 0; + for (os.access(readyfile, 0i32) != 0 && readywait < 2000) { + time.sleep(time.millisecond, time.clock.monotonic); + readywait += 1; + }; + assert(!(os.access(readyfile, 0i32) != 0)); + let readybytes: str = readfile(readyfile); + assert(readybytes.len == 4); + let runpid: i32 = (readybytes[0]: u32 + | ((readybytes[1]: u32) << 8u32) + | ((readybytes[2]: u32) << 16u32) + | ((readybytes[3]: u32) << 24u32)): i32; + assert(!(os.kill(coordinator, os.SIGINT) != 0)); + let coordinatorstatus: i32 = waitchild(coordinator); + assert(coordinatorstatus >= 0 && os.wifsignaled(coordinatorstatus) + && os.wtermsig(coordinatorstatus) == os.SIGINT); + assert(!(os.kill(-runpid, 0) != -3)); + + let closedprobe: i32 = os.fork(); + assert(!(closedprobe < 0)); + if (closedprobe == 0) { + if (os.close(os.STDOUT_FILENO) != 0) { os.exit(137); }; + if (os.close(os.STDERR_FILENO) != 0) { os.exit(138); }; + let cc: exec.command; + let cr: exec.result; + fillcommand(&cc, self, "route", root, "closed", + time.second, grace); + let cav: []str = cc.argv; + append(cav, "token"); + append(cav, root); + cc.argv = cav; + let cev: []str = cc.env; + append(cev, "ROUTE=value"); + cc.env = cev; + exec.run(&cc, &cr); + if (cr.termination != exec.termination.EXIT || cr.code != 0 + || cr.errno != 0 || cr.cleanuperrno != 0) { os.exit(139); }; + if (!eq(readfile(cc.stdoutpath), "route stdout\n")) { os.exit(140); }; + if (!eq(readfile(cc.stderrpath), "route stderr\n")) { os.exit(141); }; + os.exit(0); + }; + let closedstatus: i32 = waitchild(closedprobe); + assert(closedstatus >= 0 && os.wifexited(closedstatus) + && os.wexitstatus(closedstatus) == 0); + + let watch: exec.interrupt; + assert(exec.interruptopen(&watch)); + let handles: []exec.process = alloc([], 2u64)!; + let zero: exec.process; + append(handles, zero); + append(handles, zero); + fillcommand(&c, self, "hold", root, "interrupt", + (10i64 * (time.second: i64)): time.duration, grace); + exec.start(&handles[0], &c); + assert(!(os.kill(os.getpid(), os.SIGINT) != 0)); + let signo: i32 = 0; + for (signo == 0) { + signo = exec.interruptpoll(&watch); + }; + assert(!(signo != os.SIGINT)); + assert(cancelall(handles)); + assert(handles[0].result.termination == exec.termination.SIGNAL); + assert(exec.poll(&handles[1])); + assert(exec.interruptclose(&watch)); + + let captures: []str = [ + "exit0.out", "exit0.err", "exitdesc.out", "exitdesc.err", + "exit7.out", "exit7.err", "exit127.out", "exit127.err", + "route.out", "route.err", "signal.out", "signal.err", + "execfail.out", "execfail.err", "pastfail.out", "pastfail.err", + "chdirfail.out", "chdirfail.err", "timeout.out", "timeout.err", + "exists.out", "runinterrupt.out", "runinterrupt.err", + "runinterrupt.ready", + "closed.out", "closed.err", "interrupt.out", "interrupt.err", + ]; + let i: i32 = 0; + for (i < captures.len) { + assert(!(os.remove(strings.concat(root, "/", captures[i])) != 0)); + i += 1; + }; + assert(!(os.rmdir(root) != 0)); +};