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.
This commit is contained in:
398
test/wwfixture/process/main.ww
Normal file
398
test/wwfixture/process/main.ww
Normal file
@@ -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));
|
||||
};
|
||||
Reference in New Issue
Block a user