Files
ww/lib/os/exec/exec.ww

716 lines
18 KiB
Plaintext

// 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.
// An empty stdinpath selects the null device; 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,
stdinpath: 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.stdinpath)
|| 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, infd: i32,
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(infd, os.STDIN_FILENO);
if (rc < 0) { childmark(markerwrite, rc); };
childclose(infd, 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 stdinpath: str = c.stdinpath;
if (stdinpath.len == 0) { stdinpath = "/dev/null"; };
let infd: i32 = os.open(stdinpath, os.flag.RDONLY, 0i32);
if (infd < 0) { fail(p, infd); return; };
infd = safefd(&p.result, infd);
if (infd < 0) { fail(p, infd); return; };
let outfd: i32 = os.open(c.stdoutpath,
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384);
if (outfd < 0) {
fail(p, outfd);
closefd(&p.result, infd);
return;
};
outfd = safefd(&p.result, outfd);
if (outfd < 0) {
fail(p, outfd);
closefd(&p.result, infd);
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, infd);
closefd(&p.result, outfd);
return;
};
errfd = safefd(&p.result, errfd);
if (errfd < 0) {
fail(p, errfd);
closefd(&p.result, infd);
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, infd);
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, infd);
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, infd);
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, infd);
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, infd, outfd, errfd, marker[0], marker[1]);
};
p.pid = pid;
p.state = state.RUNNING as i32;
p.markerfd = marker[0];
closefd(&p.result, infd);
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;
};