118 lines
6.4 KiB
Zig
118 lines
6.4 KiB
Zig
//! The service harness (docs/process-lifecycle.md): one replyWait loop that
|
|
//! folds protocol requests, signals, and subscribed notifications into
|
|
//! callbacks — so the lifecycle contract ("answers ping, exits on terminate")
|
|
//! is satisfied by construction and a service author writes domain logic only.
|
|
//! Nothing is asynchronous inside the process: a callback runs at a point the
|
|
//! loop chose, never on a hijacked stack — the whole reason signals are
|
|
//! messages.
|
|
//!
|
|
//! One rule a service author does have to know, and it is stated on
|
|
//! `Callbacks.on_message`: **a capability that arrives belongs to the turn** —
|
|
//! the loop closes it unless the callback claims it with `take()`. Forgetting is
|
|
//! therefore safe, and keeping is explicit; the opposite arrangement quietly
|
|
//! spends a handle-table slot per request.
|
|
//!
|
|
//! The liveness probe: a **zero-length request is the universal ping**, answered
|
|
//! with a zero-length reply by the harness itself. No protocol's requests start
|
|
//! at length zero, so the encoding cannot collide, and there is nothing for a
|
|
//! service author to implement — a wedged service simply fails to answer, which
|
|
//! is the diagnosis (see docs/ipc.md).
|
|
|
|
const channel = @import("channel");
|
|
const ipc = @import("ipc");
|
|
const process = @import("process");
|
|
|
|
pub const Callbacks = struct {
|
|
/// Called once with the service's endpoint before the loop starts — the
|
|
/// place to subscribe to exit events, bind IRQs, or announce readiness.
|
|
/// Return false to abort startup (the process exits).
|
|
init: ?*const fn (endpoint: ipc.Handle) bool = null,
|
|
/// One protocol request from `sender` (a task id): write the reply into
|
|
/// `reply`, return its length. The zero-length ping never reaches this.
|
|
///
|
|
/// `arrived` is the capability the request carried (M13 cap passing — how a
|
|
/// subscriber hands over its endpoint), and it comes with **an ownership
|
|
/// rule: the turn owns it, and a handler that wants to keep it must say so
|
|
/// with `take()`.** Whatever is left when this returns, the loop closes.
|
|
/// `peek()` reads it without claiming, which is what a handler that may
|
|
/// still refuse wants — no close of its own on the refusal paths.
|
|
///
|
|
/// The rule is stated here, in the contract, because the alternative has
|
|
/// failed in practice: an implementation that simply ignored a `?ipc.Handle`
|
|
/// argument leaked a handle table slot per request, and every operation
|
|
/// except a subscribe ignores it. Thirty-two such requests — zero-length
|
|
/// pings will do, and they need no authorization — and the service can never
|
|
/// accept another capability for the rest of the boot. See `ipc.Arrival`.
|
|
on_message: *const fn (message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize,
|
|
/// A notification that is not a signal — a subscribed exit event, a bound
|
|
/// IRQ, a timer landing. The raw badge; decode with the ipc helpers.
|
|
on_notification: ?*const fn (badge: u64) void = null,
|
|
/// The reload signal. Default: ignored.
|
|
on_reload: ?*const fn () void = null,
|
|
/// The terminate signal, called before the loop returns. The clean exit is
|
|
/// the return itself — never put *necessary* work here (iron rule 1: a kill
|
|
/// arrives with no warning; this is for graceful extras only).
|
|
on_terminate: ?*const fn () void = null,
|
|
/// The contract this service provides: a name under `/protocol`, mirroring
|
|
/// the `library/protocol/` module that defines the wire format — a program
|
|
/// imports `display-protocol` and the provider binds `"display"`
|
|
/// (docs/os-development/protocol-namespace.md). Bound at startup, before
|
|
/// `init` runs, so the service is reachable the moment it serves. A refusal
|
|
/// (not granted, or a live provider already holds the name) aborts startup.
|
|
service: ?[]const u8 = null,
|
|
};
|
|
|
|
/// Run the service: create the endpoint, bind it under the service's contract
|
|
/// name (if it has one), bind signals to it, call `init`, then serve until
|
|
/// `terminate` arrives — at which point the loop returns and main's return is
|
|
/// the clean exit the supervisor reads as `ExitReason.exited`.
|
|
/// `maximum_message` sizes the receive and reply buffers (a service passes its
|
|
/// protocol's message maximum).
|
|
pub fn run(comptime maximum_message: usize, callbacks: Callbacks) void {
|
|
const endpoint = ipc.createIpcEndpoint() orelse return;
|
|
if (callbacks.service) |name| {
|
|
if (!channel.bindPatiently(name, endpoint)) return;
|
|
}
|
|
_ = process.bindSignals(endpoint);
|
|
if (callbacks.init) |initialise| {
|
|
if (!initialise(endpoint)) return;
|
|
}
|
|
|
|
var reply_buffer: [maximum_message]u8 = undefined;
|
|
var reply_len: usize = 0;
|
|
var receive: [maximum_message]u8 = undefined;
|
|
while (true) {
|
|
const got = ipc.replyWait(endpoint, reply_buffer[0..reply_len], &receive, null);
|
|
// Whatever capability came with this turn is the turn's, and the turn
|
|
// closes it unless a callback claims it (`ipc.Arrival`). Structural
|
|
// rather than a close per branch, because the branches are exactly what
|
|
// gets forgotten: the ping's `continue` below, and every `on_message`
|
|
// that has no use for a capability — which is every operation but a
|
|
// subscribe. A `defer` in a loop body runs on `continue` and on the
|
|
// `return` that ends the loop, so this covers all four exits.
|
|
var arrived: ipc.Arrival = .{ .handle = got.cap };
|
|
defer arrived.release();
|
|
|
|
if (got.isNotification()) {
|
|
reply_len = 0; // nothing owed for a notification
|
|
if (process.signalsFrom(got.badge)) |signals| {
|
|
if (signals.has(.reload)) {
|
|
if (callbacks.on_reload) |onReload| onReload();
|
|
}
|
|
if (signals.has(.terminate)) {
|
|
if (callbacks.on_terminate) |onTerminate| onTerminate();
|
|
return; // the loop's return IS the clean exit
|
|
}
|
|
continue;
|
|
}
|
|
if (callbacks.on_notification) |onNotification| onNotification(got.badge);
|
|
continue;
|
|
}
|
|
if (got.len == 0) {
|
|
reply_len = 0; // the universal ping: a zero-length reply, from the harness
|
|
continue; // any capability it carried goes out through the turn's `defer`
|
|
}
|
|
reply_len = callbacks.on_message(receive[0..got.len], &reply_buffer, got.senderTaskId(), &arrived);
|
|
}
|
|
}
|