//! process-test — a test fixture for process management (bundled in the //! initial-ramdisk, driven by the `supervision` test case). One binary, three //! roles picked by argv, so the whole user-side surface is exercised end to end: //! //! - `process-test run` — the supervisor: spawns the two children below with an //! exit-notification endpoint, sees them in `process_enumerate`, kills them, //! receives both exit notifications, and confirms they are gone. Prints //! "process-test: ok" for the kernel test to match, or a FAIL line naming the //! step that broke. //! - `process-test sleeper` — a child that blocks in `sleep` forever: its kill //! exercises the immediate reap of a blocked task. //! - `process-test spinner` — a child that spins in user mode making no system //! calls: its kill exercises the deferred path (kill_pending, finished by the //! timer tick). //! //! Spawned with no arguments (the initial-ramdisk sweep test starts every bundled //! binary bare), it exits silently so it cannot derange other tests' output. const std = @import("std"); const ipc = @import("ipc"); const process = @import("process"); const service = @import("service"); const time = @import("time"); const logging = @import("logging"); fn fail(step: []const u8) noreturn { _ = logging.write("process-test: FAIL "); _ = logging.write(step); _ = logging.write("\n"); process.exit(1); } /// Whether process `id` appears in a fresh `process_enumerate` snapshot, named /// `name` (an id present under the wrong name is a table mix-up, not a pass). fn listed(id: u32, name: []const u8) bool { var table: [32]process.ProcessDescriptor = undefined; const total = process.processes(&table); for (table[0..@min(total, table.len)]) |descriptor| { if (descriptor.id != id) continue; return std.mem.eql(u8, descriptor.name[0..descriptor.name_length], name); } return false; } /// Block on the exit endpoint until a child-exit notification arrives; returns /// the ended child's id. A wrong wake-up (there should be none — nothing else /// knows this endpoint) fails the test rather than looping forever. fn awaitChildExit(endpoint: ipc.Handle) u32 { var scratch: [8]u8 = undefined; const received = ipc.replyWait(endpoint, scratch[0..0], &scratch, null); if (!received.isChildExit()) fail("expected a child-exit notification"); return received.childProcessId(); } /// The harness-run child of the signals test: echoes requests, logs the two /// signals it handles. Terminate makes run() return, and returning from main is /// the clean exit the parent reads as ExitReason.exited. fn echo(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize { _ = sender; _ = capability; const n = @min(message.len, reply.len); @memcpy(reply[0..n], message[0..n]); return n; } fn onReload() void { _ = logging.write("process-test: reloaded\n"); } fn onTerminate() void { _ = logging.write("process-test: terminating\n"); } /// The parent of the signals test: drives ping, echo, reload, the one-shot /// timer, and both endings of the stop sequence (polite -> exited; deaf -> /// killed at the deadline). Prints "process-test: signals ok" as the marker. fn signalRun() void { const endpoint = ipc.createIpcEndpoint() orelse fail("create exit endpoint"); const child = process.spawnSupervised("process-test", &.{"service"}, endpoint) orelse fail("spawn service child"); // Reach the child's endpoint through the registry (retry: it may not be up). var service_handle: ?ipc.Handle = null; var tries: u32 = 0; while (service_handle == null and tries < 200) : (tries += 1) { service_handle = ipc.lookup(.input); if (service_handle == null) time.sleepMillis(20); } const h = service_handle orelse fail("service child never registered"); // The universal ping: a zero-length call answered zero-length by the harness. var reply: [16]u8 = undefined; const pong = ipc.call(h, &.{}, &reply) catch fail("ping call failed"); if (pong != 0) fail("ping reply not empty"); // An ordinary request still reaches on_message. const n = ipc.call(h, "echo!", &reply) catch fail("echo call failed"); if (n != 5 or !std.mem.eql(u8, reply[0..5], "echo!")) fail("echo mismatch"); // reload: a statement — the child logs it; the kernel test reads the serial. if (!process.sendSignal(child, .reload)) fail("send reload"); time.sleepMillis(200); // The one-shot timer: armed on our endpoint, lands as isTimer. if (!time.timerOnce(endpoint, 100)) fail("arm timer"); var scratch: [8]u8 = undefined; const landing = ipc.replyWait(endpoint, scratch[0..0], &scratch, null); if (!landing.isTimer()) fail("expected the timer landing"); // The stop sequence, polite path: terminate, clean exit inside the deadline. process.stop(child, 2000, endpoint); if ((process.exitReason(child) orelse .killed) != .exited) fail("service child reason not exited"); // The deaf child: binds nothing, hears nothing — the deadline kills it. const deaf = process.spawnSupervised("process-test", &.{"sleeper"}, endpoint) orelse fail("spawn deaf child"); time.sleepMillis(50); // let it reach its sleep process.stop(deaf, 300, endpoint); if ((process.exitReason(deaf) orelse .exited) != .killed) fail("deaf child reason not killed"); _ = logging.write("process-test: signals ok\n"); } pub fn main(init: process.Init) void { const role = init.arguments.get(1) orelse return; // spawned bare (ramdisk sweep): stay silent if (std.mem.eql(u8, role, "sleeper")) { while (true) time.sleepMillis(500); } if (std.mem.eql(u8, role, "service")) { // Borrowed well-known id: the input service is not part of this scenario. service.run(64, .{ .service = .input, .on_message = echo, .on_reload = onReload, .on_terminate = onTerminate, }); return; // terminate arrived; returning is the clean exit } if (std.mem.eql(u8, role, "signal-run")) { signalRun(); return; } if (std.mem.eql(u8, role, "spinner")) { var beat: u64 = 0; const touch: *volatile u64 = &beat; while (true) touch.* +%= 1; // user mode only — no system calls to die at } // The supervisor ("run"). const endpoint = ipc.createIpcEndpoint() orelse fail("create exit endpoint"); const sleeper = process.spawnSupervised("process-test", &.{"sleeper"}, endpoint) orelse fail("spawn sleeper"); const spinner = process.spawnSupervised("process-test", &.{"spinner"}, endpoint) orelse fail("spawn spinner"); time.sleepMillis(100); // let the sleeper block and the spinner get a core if (!listed(sleeper, "/test/system/services/process-test")) fail("sleeper not in process_enumerate"); if (!listed(spinner, "/test/system/services/process-test")) fail("spinner not in process_enumerate"); // Kills that must be refused: a kernel task (id 0), and an id that was never // issued — both -ESRCH. (-EPERM needs a second supervisor; the kernel-level // `process-kill` test covers it.) if (process.kill(0)) fail("killing a kernel task was allowed"); if (process.kill(0xFFFF_FFF0)) fail("killing an unknown id was allowed"); // The blocked child: usually reaped on the spot (it sits in `sleep`). The // notification is the fence — after it, the child is certainly gone, so the // second kill must miss (its id is never reused). if (!process.kill(sleeper)) fail("kill sleeper"); if (awaitChildExit(endpoint) != sleeper) fail("sleeper exit notification"); if (process.kill(sleeper)) fail("double kill was allowed"); // The running child: the deferred path — condemned now, dead by the next tick. if (!process.kill(spinner)) fail("kill spinner"); if (awaitChildExit(endpoint) != spinner) fail("spinner exit notification"); if (listed(sleeper, "/test/system/services/process-test")) fail("sleeper still listed after kill"); if (listed(spinner, "/test/system/services/process-test")) fail("spinner still listed after kill"); // M17.2: both children were killed by us, and the reason says so — the whole // restart-policy input, read through the runtime like a real supervisor would. if ((process.exitReason(sleeper) orelse .exited) != .killed) fail("sleeper reason not killed"); if ((process.exitReason(spinner) orelse .exited) != .killed) fail("spinner reason not killed"); if (process.exitReason(0xFFFF_FFF0) != null) fail("unknown id had a reason"); _ = logging.write("process-test: ok\n"); }