Signals over IPC, one-shot timers, and the service harness (M17.4)
Signals are statements delivered as coalescing notifications to the endpoint a process nominates with signal_bind — never a hijacked stack, never a question (liveness is the zero-length ping the harness answers). process_signal is supervisor-or-self gated, like kill; unbound targets accumulate a pending mask delivered on bind. timer_bind is the missing timed wait: a one-shot deadline landing in the same replyWait as everything else — what stop(), hello deadlines, and restart backoff are built from. runtime.service.run folds requests, signals, and notifications into callbacks; the VFS conversion deletes its hand-rolled loop and gains the whole lifecycle contract. The signals scenario drives ping, reload, terminate->exited, the timer, and the deaf-child deadline->killed path from ring 3. docs/process-lifecycle.md increments 1-4 are now as-built.
This commit is contained in:
@@ -48,11 +48,91 @@ fn awaitChildExit(endpoint: runtime.ipc.Handle) u32 {
|
||||
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) usize {
|
||||
_ = sender;
|
||||
const n = @min(message.len, reply.len);
|
||||
@memcpy(reply[0..n], message[0..n]);
|
||||
return n;
|
||||
}
|
||||
|
||||
fn onReload() void {
|
||||
_ = runtime.system.write("process-test: reloaded\n");
|
||||
}
|
||||
|
||||
fn onTerminate() void {
|
||||
_ = runtime.system.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 = runtime.ipc.createIpcEndpoint() orelse fail("create exit endpoint");
|
||||
const child = runtime.system.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: ?runtime.ipc.Handle = null;
|
||||
var tries: u32 = 0;
|
||||
while (service_handle == null and tries < 200) : (tries += 1) {
|
||||
service_handle = runtime.ipc.lookup(.input);
|
||||
if (service_handle == null) runtime.system.sleep(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 = runtime.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 = runtime.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 (!runtime.process.sendSignal(child, .reload)) fail("send reload");
|
||||
runtime.system.sleep(200);
|
||||
|
||||
// The one-shot timer: armed on our endpoint, lands as isTimer.
|
||||
if (!runtime.system.timerOnce(endpoint, 100)) fail("arm timer");
|
||||
var scratch: [8]u8 = undefined;
|
||||
const landing = runtime.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.
|
||||
runtime.process.stop(child, 2000, endpoint);
|
||||
if ((runtime.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 = runtime.system.spawnSupervised("process-test", &.{"sleeper"}, endpoint) orelse fail("spawn deaf child");
|
||||
runtime.system.sleep(50); // let it reach its sleep
|
||||
runtime.process.stop(deaf, 300, endpoint);
|
||||
if ((runtime.process.exitReason(deaf) orelse .exited) != .killed) fail("deaf child reason not killed");
|
||||
|
||||
_ = runtime.system.write("process-test: signals ok\n");
|
||||
}
|
||||
|
||||
pub fn main(init: runtime.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) runtime.system.sleep(500);
|
||||
}
|
||||
if (std.mem.eql(u8, role, "service")) {
|
||||
// Borrowed well-known id: the input service is not part of this scenario.
|
||||
runtime.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;
|
||||
|
||||
+22
-28
@@ -138,42 +138,36 @@ fn handle(message: []const u8, out: []u8, sender: u32) usize {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
const endpoint = runtime.ipc.createIpcEndpoint() orelse {
|
||||
_ = runtime.system.write("vfs: no endpoint\n");
|
||||
return;
|
||||
};
|
||||
if (!runtime.ipc.register(.vfs, endpoint)) {
|
||||
_ = runtime.system.write("vfs: register failed\n");
|
||||
return;
|
||||
}
|
||||
// First subscriber of the published exit events (docs/process-lifecycle.md):
|
||||
// when a client dies holding open handles, the notification below is how the
|
||||
// VFS learns to release them.
|
||||
/// Startup, under the harness: subscribe to the published exit events — when a
|
||||
/// client dies holding open handles, the exit notification is how the VFS learns
|
||||
/// to release them (docs/process-lifecycle.md).
|
||||
fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||
if (!runtime.process.subscribeExits(endpoint)) {
|
||||
_ = runtime.system.write("vfs: exit subscription failed\n");
|
||||
}
|
||||
_ = runtime.system.write("vfs: ready\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
var reply_buffer: [protocol.message_maximum]u8 = undefined;
|
||||
var reply_len: usize = 0;
|
||||
var receive: [protocol.message_maximum]u8 = undefined;
|
||||
while (true) {
|
||||
const got = runtime.ipc.replyWait(endpoint, reply_buffer[0..reply_len], &receive, null);
|
||||
if (got.isChildExit()) {
|
||||
// A published exit event: a process died; drop whatever it held.
|
||||
releaseClientHandles(got.childProcessId());
|
||||
reply_len = 0;
|
||||
continue;
|
||||
}
|
||||
if (got.isNotification()) {
|
||||
reply_len = 0; // not a request; nothing owed
|
||||
continue;
|
||||
}
|
||||
reply_len = handle(receive[0..got.len], &reply_buffer, got.senderTaskId());
|
||||
/// A non-signal notification: the only kind the VFS subscribes to is exit events.
|
||||
fn onNotification(badge: u64) void {
|
||||
if (badge & runtime.ipc.notify_exit_bit != 0) {
|
||||
releaseClientHandles(@intCast(badge & ~(runtime.ipc.notify_badge_bit | runtime.ipc.notify_exit_bit)));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
// The harness owns the loop: requests dispatch to handle(), exit events to
|
||||
// onNotification(), ping and terminate are answered for free — this service
|
||||
// gained the whole lifecycle contract by deleting its hand-rolled loop.
|
||||
runtime.service.run(protocol.message_maximum, .{
|
||||
.service = .vfs,
|
||||
.init = initialise,
|
||||
.on_message = handle,
|
||||
.on_notification = onNotification,
|
||||
});
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
|
||||
Reference in New Issue
Block a user