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:
@@ -55,6 +55,9 @@ pub const SystemCall = enum(u64) {
|
||||
ipc_send = 26, // ipc_send(handle, message_ptr, message_len) -> 0/-errno: post a payload to an endpoint's async queue without blocking
|
||||
process_exit_reason = 27, // process_exit_reason(id) -> ExitReason/-errno: how a dead child ended (its supervisor only)
|
||||
process_subscribe = 28, // process_subscribe(endpoint) -> 0/-errno: subscribe to published exit events — every death posts a notification
|
||||
signal_bind = 29, // signal_bind(endpoint) -> 0/-errno: nominate the endpoint this process's signals arrive on
|
||||
process_signal = 30, // process_signal(id, signal) -> 0/-errno: post a signal to a child (or to yourself)
|
||||
timer_bind = 31, // timer_bind(endpoint, ms) -> 0/-errno: one-shot timer — posts a notification when ms elapse
|
||||
_,
|
||||
};
|
||||
|
||||
@@ -112,6 +115,35 @@ pub const notify_exit_bit: u64 = 1 << 62;
|
||||
/// broadcasts where a rendezvous is the wrong shape (the input service is the first user).
|
||||
pub const notify_message_bit: u64 = 1 << 61;
|
||||
|
||||
/// Set (alongside `notify_badge_bit`) in the badge of a **signal notification** —
|
||||
/// the process-lifecycle vocabulary of docs/process-lifecycle.md, delivered to the
|
||||
/// endpoint the process nominated with `signal_bind`. The low bits carry the
|
||||
/// coalesced pending mask (bit positions = `Signal` values): signals are
|
||||
/// statements, not questions, and two pending terminates are one terminate.
|
||||
pub const notify_signal_bit: u64 = 1 << 60;
|
||||
|
||||
/// Set (alongside `notify_badge_bit`) in the badge of a **timer notification** —
|
||||
/// a one-shot `timer_bind` deadline landing. No payload bits: what to do when the
|
||||
/// deadline fires is whatever the receiver armed it for (a stop-sequence
|
||||
/// escalation, a restart backoff, an alarm).
|
||||
pub const notify_timer_bit: u64 = 1 << 59;
|
||||
|
||||
/// The signal vocabulary (docs/process-lifecycle.md): POSIX's concepts, danos's
|
||||
/// names, message delivery. The value is the bit position in the pending mask — a
|
||||
/// private kernel/runtime detail, free to change while they ship together. Kill
|
||||
/// is not here (it is `process_kill`, unhandleable by definition); faults are not
|
||||
/// here (they are `ExitReason`s — recovery is restart, not a handler); liveness is
|
||||
/// not here (a question, asked as the zero-length ping call, not a statement).
|
||||
pub const Signal = enum(u5) {
|
||||
terminate = 0, // finish up and exit (the polite half of the stop sequence)
|
||||
reload = 1, // re-read configuration / re-scan
|
||||
interrupt = 2, // interactive interrupt (no sender until a console exists)
|
||||
quit = 3, // as interrupt, by convention more final
|
||||
alarm = 4, // a timer the process armed for itself (unbuilt: no consumer yet)
|
||||
user_1 = 5, // service-defined
|
||||
user_2 = 6, // service-defined
|
||||
};
|
||||
|
||||
/// Capacity of `ProcessDescriptor.name` — matches the longest name `system_spawn`
|
||||
/// accepts, so a process's recorded name (its argv[0]) is never truncated.
|
||||
pub const maximum_process_name = 64;
|
||||
|
||||
@@ -137,6 +137,7 @@ pub fn init() void {
|
||||
architecture.setSystemCallHandler(system_call);
|
||||
scheduler.terminate_current_hook = terminateCurrentLocked;
|
||||
scheduler.reap_task_hook = reapTaskLocked;
|
||||
scheduler.timer_tick_hook = timerSweepLocked;
|
||||
}
|
||||
|
||||
/// Return -1 (as an unsigned bit pattern) in the system_call result register.
|
||||
@@ -202,6 +203,9 @@ fn system_call(state: *architecture.CpuState) void {
|
||||
.process_kill => systemProcessKill(state),
|
||||
.process_exit_reason => systemProcessExitReason(state),
|
||||
.process_subscribe => systemProcessSubscribe(state),
|
||||
.signal_bind => systemSignalBind(state),
|
||||
.process_signal => systemProcessSignal(state),
|
||||
.timer_bind => systemTimerBind(state),
|
||||
_ => fail(state),
|
||||
}
|
||||
}
|
||||
@@ -575,6 +579,20 @@ fn releaseTaskResourcesLocked(t: *scheduler.Task) void {
|
||||
recordExitLocked(t);
|
||||
irq.releaseOwner(t.id);
|
||||
devices_broker.releaseAllOwnedBy(t.id);
|
||||
// The dying task's signal endpoint and one-shot timers go with it.
|
||||
if (t.signal_endpoint) |raw| {
|
||||
ipc.dropRef(@ptrCast(@alignCast(raw)));
|
||||
t.signal_endpoint = null;
|
||||
}
|
||||
t.pending_signals = 0;
|
||||
for (&one_shot_timers) |*slot| {
|
||||
if (slot.*) |timer| {
|
||||
if (timer.owner == t.id) {
|
||||
ipc.dropRef(timer.endpoint);
|
||||
slot.* = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// A dead subscriber's own subscriptions go first: it must not hear about
|
||||
// itself, and the slots' endpoint references drop with it.
|
||||
for (&exit_subscribers) |*slot| {
|
||||
@@ -735,6 +753,92 @@ fn systemProcessSubscribe(state: *architecture.CpuState) void {
|
||||
failErr(state, ipc.ENOSPC);
|
||||
}
|
||||
|
||||
/// signal_bind(endpoint): nominate where this process's signals arrive — the
|
||||
/// IRQ-as-IPC pattern a fourth time (docs/process-lifecycle.md). Replacing a
|
||||
/// binding drops the old reference; signals that pended while unbound are
|
||||
/// delivered immediately on bind, coalesced into one notification.
|
||||
fn systemSignalBind(state: *architecture.CpuState) void {
|
||||
const t = scheduler.current();
|
||||
if (t.aspace == 0) return fail(state);
|
||||
const endpoint = ipc.resolveHandle(t, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
||||
const flags = sync.enter();
|
||||
defer sync.leave(flags);
|
||||
if (t.signal_endpoint) |raw| ipc.dropRef(@ptrCast(@alignCast(raw)));
|
||||
endpoint.refcount += 1;
|
||||
t.signal_endpoint = @ptrCast(endpoint);
|
||||
if (t.pending_signals != 0) {
|
||||
ipc.notifyLocked(endpoint, abi.notify_signal_bit | t.pending_signals);
|
||||
t.pending_signals = 0;
|
||||
}
|
||||
architecture.setSystemCallResult(state, 0);
|
||||
}
|
||||
|
||||
/// process_signal(id, signal): post a signal — a one-way, coalescing statement,
|
||||
/// never a question (docs/process-lifecycle.md). The authority gate is the
|
||||
/// supervision link, like kill; a process may also signal itself. Unbound
|
||||
/// targets accumulate the signal in their pending mask.
|
||||
fn systemProcessSignal(state: *architecture.CpuState) void {
|
||||
const t = scheduler.current();
|
||||
if (t.aspace == 0) return fail(state);
|
||||
const id = architecture.systemCallArg(state, 0);
|
||||
const signal = architecture.systemCallArg(state, 1);
|
||||
if (id > std.math.maxInt(u32)) return failErr(state, ipc.ESRCH);
|
||||
if (signal > 31) return failErr(state, ipc.EBADF); // not a Signal bit position
|
||||
const flags = sync.enter();
|
||||
defer sync.leave(flags);
|
||||
const target = scheduler.taskByIdLocked(@intCast(id)) orelse return failErr(state, ipc.ESRCH);
|
||||
if (target.aspace == 0) return failErr(state, ipc.ESRCH);
|
||||
if (target.supervisor != t.id and target.id != t.id) return failErr(state, ipc.EPERM);
|
||||
target.pending_signals |= @as(u32, 1) << @intCast(signal);
|
||||
if (target.signal_endpoint) |raw| {
|
||||
const endpoint: *ipc.Endpoint = @ptrCast(@alignCast(raw));
|
||||
ipc.notifyLocked(endpoint, abi.notify_signal_bit | target.pending_signals);
|
||||
target.pending_signals = 0;
|
||||
}
|
||||
architecture.setSystemCallResult(state, 0);
|
||||
}
|
||||
|
||||
/// The one-shot timers of timer_bind: the missing timed wait. A service arms a
|
||||
/// deadline and keeps serving; the expiry arrives in the same replyWait as
|
||||
/// everything else (notify_timer_bit). What stop-sequence escalation, hello
|
||||
/// deadlines, and restart backoff are built from — and later, `alarm`.
|
||||
const timer_capacity = 16;
|
||||
const OneShotTimer = struct { deadline: u64, endpoint: *ipc.Endpoint, owner: u32 };
|
||||
var one_shot_timers: [timer_capacity]?OneShotTimer = .{null} ** timer_capacity;
|
||||
|
||||
/// Sweep expired timers — hung on scheduler.timer_tick_hook, so it runs on every
|
||||
/// tick with the big kernel lock held, like the sleeper wake it rides beside.
|
||||
fn timerSweepLocked() void {
|
||||
const now = architecture.millis();
|
||||
for (&one_shot_timers) |*slot| {
|
||||
if (slot.*) |timer| {
|
||||
if (now >= timer.deadline) {
|
||||
ipc.notifyLocked(timer.endpoint, abi.notify_timer_bit);
|
||||
ipc.dropRef(timer.endpoint);
|
||||
slot.* = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// timer_bind(endpoint, ms): arm a one-shot timer. -ENOSPC when the table is full.
|
||||
fn systemTimerBind(state: *architecture.CpuState) void {
|
||||
const t = scheduler.current();
|
||||
if (t.aspace == 0) return fail(state);
|
||||
const endpoint = ipc.resolveHandle(t, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
||||
const ms = architecture.systemCallArg(state, 1);
|
||||
const flags = sync.enter();
|
||||
defer sync.leave(flags);
|
||||
for (&one_shot_timers) |*slot| {
|
||||
if (slot.* == null) {
|
||||
endpoint.refcount += 1;
|
||||
slot.* = .{ .deadline = architecture.millis() + ms, .endpoint = endpoint, .owner = t.id };
|
||||
return architecture.setSystemCallResult(state, 0);
|
||||
}
|
||||
}
|
||||
failErr(state, ipc.ENOSPC);
|
||||
}
|
||||
|
||||
fn systemProcessExitReason(state: *architecture.CpuState) void {
|
||||
const t = scheduler.current();
|
||||
if (t.aspace == 0) return fail(state);
|
||||
|
||||
@@ -54,6 +54,13 @@ pub const Task = struct {
|
||||
// before the reap records it for `process_exit_reason`. Meaningless while
|
||||
// the task lives.
|
||||
exit_reason: abi.ExitReason = .exited,
|
||||
// Endpoint this process's signals arrive on (signal_bind), or null — same
|
||||
// ownership rules as exit_endpoint (holds a reference; opaque here).
|
||||
signal_endpoint: ?*anyopaque = null,
|
||||
// Signals posted but not yet delivered: the coalescing pending mask
|
||||
// (docs/process-lifecycle.md). Bits are abi.Signal values. Signals pend here
|
||||
// until an endpoint is bound; two pending terminates are one terminate.
|
||||
pending_signals: u32 = 0,
|
||||
// Set by process_kill on a task that is running on another core; the kernel
|
||||
// finishes the kill at that task's next system call or timer tick.
|
||||
kill_pending: bool = false,
|
||||
@@ -647,9 +654,15 @@ fn reapKillPendingLocked() void {
|
||||
/// other critical section, but releases it *without* touching the interrupt flag
|
||||
/// — the handler's `iretq` restores the interrupted context's flags, so
|
||||
/// re-enabling here would open a nested-interrupt window before the return.
|
||||
/// Called from the tick with the big kernel lock held — process.zig hangs the
|
||||
/// one-shot timer sweep here (timer_bind), the same call-up pattern as the
|
||||
/// teardown hooks below.
|
||||
pub var timer_tick_hook: ?*const fn () void = null;
|
||||
|
||||
pub fn tick() void {
|
||||
_ = sync.enter();
|
||||
wakeExpired();
|
||||
if (timer_tick_hook) |hook| hook();
|
||||
reapKillPendingLocked();
|
||||
if (preemption_enabled) schedule();
|
||||
sync.leaveIsr();
|
||||
|
||||
@@ -136,6 +136,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
||||
claimReleaseTest(boot_information);
|
||||
} else if (eql(case, "vfs-client-death")) {
|
||||
vfsClientDeathTest(boot_information);
|
||||
} else if (eql(case, "signals")) {
|
||||
signalsTest(boot_information);
|
||||
} else if (eql(case, "initial-ramdisk")) {
|
||||
initialRamdiskTest(boot_information);
|
||||
} else if (eql(case, "vfs")) {
|
||||
@@ -1605,6 +1607,53 @@ fn vfsClientDeathTest(boot_information: *const BootInformation) void {
|
||||
result();
|
||||
}
|
||||
|
||||
/// M17.4 from ring 3: process-test's signal-run role drives the whole lifecycle
|
||||
/// surface — the zero-length ping (answered by the harness), signals as
|
||||
/// statements (reload logged, terminate = clean exit), the one-shot timer, and
|
||||
/// both endings of the stop sequence (polite -> exited, deaf -> killed at the
|
||||
/// deadline). Its "process-test: signals ok" is the pass marker.
|
||||
fn signalsTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: signals\n", .{});
|
||||
if (boot_information.initial_ramdisk_len == 0) {
|
||||
check("bootloader handed over an initial_ramdisk", false);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||
const rd = initial_ramdisk.Reader.init(image) orelse {
|
||||
check("initial_ramdisk image is valid", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
|
||||
process.setInitialRamdisk(image); // the parent system_spawns its children by name
|
||||
process.write_count = 0;
|
||||
var runner: u32 = 0;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "process-test")) continue;
|
||||
runner = process.spawnProcessSupervised(item.blob, 4, &.{ "process-test", "signal-run" }, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
check("signal-run parent spawned", runner != 0);
|
||||
|
||||
const pass_marker = "process-test: signals ok";
|
||||
const fail_marker = "process-test: FAIL";
|
||||
scheduler.setPriority(1);
|
||||
const deadline = architecture.millis() + 15000;
|
||||
var saw_pass = false;
|
||||
var saw_fail = false;
|
||||
while (architecture.millis() < deadline and !saw_pass and !saw_fail) {
|
||||
if (process.write_len >= pass_marker.len and eql(process.write_buffer[0..pass_marker.len], pass_marker)) saw_pass = true;
|
||||
if (process.write_len >= fail_marker.len and eql(process.write_buffer[0..fail_marker.len], fail_marker)) saw_fail = true;
|
||||
scheduler.yield();
|
||||
}
|
||||
scheduler.setPriority(4);
|
||||
check("the signal-run parent reported ok", saw_pass and !saw_fail);
|
||||
result();
|
||||
}
|
||||
|
||||
/// The whole user-side surface at once: spawn process-test's supervisor role,
|
||||
/// which — entirely from ring 3 — creates an exit endpoint, spawns its two
|
||||
/// children supervised, sees them in process_enumerate, kills them (one blocked,
|
||||
|
||||
@@ -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