Skip to main content

ServiceContext

Struct ServiceContext 

Source
pub struct ServiceContext { /* private fields */ }
Expand description

The service context, re-exported so an ordinary program never has to name godspeed_sdk.

Every program’s entry point takes one, and this library said “you should not need the SDK” while requiring the SDK for the first line of every program. The Stranger Test caught that: a weak model given only the published documentation wrote use godspeed_sdk::ServiceContext; and recorded it as the single thing most likely to stop its program compiling.

It is a re-export rather than a wrapper on purpose - it is the SAME type, so a program that does reach for the SDK later is not holding two different things with one name. Passed by the kernel to service_main. Non-Copy; one per service instance.

Implementations§

Source§

impl ServiceContext

Source

pub const QUIT_KEYS: &'static [u8]

The keys Self::request_with_reply_qhint leaves on: q, Q and Escape.

Source

pub fn capability(&self, name: &str) -> Result<CapHandle, CapError>

Look up a named capability from this service’s cap table.

Source

pub fn recv(&self) -> Message

Block until a message arrives on this service’s primary recv endpoint.

Every failure here now PANICS rather than spinning. All three exits used to be loop {}: a silent, logless, non-yielding tight spin that pegged the core. That is the worst possible response to EndpointDead, which is not corruption but an ordinary runtime truth (§8.6) - the service was told its endpoint died and answered by burning a core forever, telling nobody.

Panicking is now the RIGHT answer because the panic handler was fixed in the same pass: it logs and faults, so the kernel kills the task, bumps its endpoint generation, wakes any peer blocked in call with ReplyDead, and the supervisor restarts it. Loud, and recovered.

A service that wants to HANDLE the error instead of dying should call recv_result.

Source

pub fn try_recv(&self) -> Option<Message>

Non-blocking receive on this service’s primary recv endpoint: Some(msg) if a message was waiting, None if the queue is empty. A busy-polling driver uses this to drain interrupt events (§12) each loop iteration without blocking.

A receive ERROR on this service’s OWN endpoint (it is dead, or the cap was refused) is not an empty queue: the service dies loudly, exactly as Self::recv does, so the supervisor restarts it. It read as None until 2026-10-10, and a loop polling a dead endpoint spun on it forever (backlog/fixed/80 S5).

Source

pub fn recv_timeout(&self, timeout_cycles: u64) -> Option<Message>

Block on this service’s recv endpoint until a message arrives or timeout_cycles (cycle-counter ticks, the unit of read_tsc - convert with duration_cycles) elapse: Some(msg) = message, None = timed out. A receive ERROR on this service’s own endpoint dies loudly, as Self::recv and Self::try_recv do (backlog/fixed/80 S5). timeout_cycles == 0 blocks forever. The kernel turns the count into whole scheduler ticks (at least one), so the bound is never finer than one quantum. A driver uses this to idle on its hardware interrupt while still waking on a timer for auto-repeat (§12 timed-wait).

It returns a 4 KiB Message by value. It once carried #[inline(always)] to save that frame; 8f32a5e9 removed it, because inlining a body that holds a 4 KiB local into a caller that holds one puts both in the same frame.

Source

pub fn irq_unmask(&self, vector: u8)

Re-open the kernel’s IOAPIC gate for a level-triggered IRQ vector after this driver has cleared its device’s interrupt source (§12). The kernel masks a level INTx while the driver handles it (so it can’t storm); call this to let it fire again. Only the driver registered for vector (via its hw_interrupt route) may unmask it. No-op for MSI.

Source

pub fn sleep(&self, cycles: u64)

Block this task for roughly cycles TSC cycles, then return (syscall 37). A real sleep: the core can halt while parked, so a poll/wait loop does not busy-yield (which pegs the core at ~100% and makes every task on it read as fully busy in observe). Like yield, needs no capability. A sleep under one quantum (10 ms) uses the arch’s sub-tick timer where it has one (hires_arm), with a tick backstop; otherwise granularity is one scheduler quantum. Use for UI repaint pacing and “wait for child” loops - not for precise timing. On a 32-bit target cycles is clamped to what one register carries, so a long sleep is shortened to the longest the ABI can express rather than wrapping to a short one (backlog/fixed/80 S5).

Source

pub fn recv_result(&self) -> Result<Message, IpcError>

Block until a message arrives; returns the error instead of looping silently.

Source

pub fn send(&self, peer: &str, msg: &Message) -> Result<(), IpcError>

Send to a named peer declared in ipc_send. Blocking.

Source

pub fn try_send(&self, peer: &str, msg: &Message) -> Result<(), IpcError>

Non-blocking send; returns QueueFull immediately if the queue is full.

Source

pub fn reacquire_cap(&self, peer: &str) -> Result<CapHandle, CapError>

Acquire a fresh SEND cap to peer via the kernel name directory.

Called after try_send returns EndpointDead (§14.2). Updates the per-service dynamic cap cache so subsequent try_send calls use the new slot without going to the kernel again. Every failure collapses to CapNotHeld; reacquire_cap_detail says which.

Source

pub fn reacquire_cap_detail( &self, peer: &str, ) -> Result<CapHandle, AcquireFailure>

reacquire_cap, but saying WHICH failure - see AcquireFailure.

Use this wherever the answer changes what the caller does or prints. The plain reacquire_cap/reacquire_by_name collapse every failure to one value, which is fine for a caller that only retries, and wrong for one that reports: a message naming two possible causes and committing to neither is a message that sends the reader to the wrong service (§26.7).

Source

pub fn derive_cap(&self, held: CapHandle) -> Option<CapHandle>

Derive a duplicate of a capability this service holds with the GRANT right into a fresh slot (syscall 29 = DeriveCap). The copy carries the same resource, generation, and (non-widened) rights.

Used to hand out many copies of a held endpoint cap: derive a copy per recipient and grant that copy away (via send_with_cap_by_handle) while keeping the original. Returns None if the cap lacks GRANT, is stale, or the cap table is full.

Source

pub fn probe_mode(&self) -> u32

Return the probe mode written by the kernel at spawn (0 for all production services).

Source

pub fn drain_stale_replies(&self) -> usize

Discard replies left over from requests this service already gave up on. Returns how many.

A DETECTED DESYNC HAS TO BE REPAIRABLE, and this is the repair. fs tags every block request and checks the tag on the reply, so a stale reply is always DETECTED - and detection was all there was. The mismatched reply was discarded and the request failed, which leaves this service’s own reply still queued for the NEXT request to find. Every subsequent request then receives its predecessor’s reply, discards it, fails, and queues another: permanently one reply out of phase, alternating forever, with the volume mounted and every operation failing.

Observed 133 times in one osdev test peer-storm run, still alternating after the storm had stopped. The code that discarded expected the queue to drain on its own - “the caller retries at its own level, by which point the queue has drained” - and it never does, because each retry consumes exactly one stale reply and contributes exactly one more.

WHY DRAINING IS SAFE HERE, which is the whole reason this is in the SDK rather than hand-rolled at a call site: it drains the dedicated REPLY MAILBOX, an endpoint that carries nothing but replies to calls this service made. A caller that is synchronous - one outstanding request at a time, which is what request_with_reply* enforces - can have nothing legitimate waiting there at the moment it is about to issue a new request. Anything present is a reply it stopped waiting for.

It refuses to drain the SHARED endpoint, and that refusal is load-bearing. A service without a reply mailbox awaits replies on the endpoint its CLIENTS send to, so draining there would silently eat client requests - the precise hazard the CallDeadline amendment (§8.2) exists to describe. No mailbox, no drain, and the caller keeps the behaviour it had.

SO THIS REPAIR IS CONDITIONAL, and the condition is not theoretical. A reply mailbox is an OPTIONAL endpoint (routing::try_register_optional): it is refused when the routing table is near full, and a bare-metal T630 refuses three times during selfcheck. A service refused one falls back to its shared endpoint and silently loses this repair - which is correct, because the alternative is eating client traffic, but it means “the desync is fixed” holds only where the mailbox was granted.

Measured rather than assumed: an arm32 Pi 2 run logged ZERO refusals, so fs had its mailbox and the repair was live on that port. spawn[ipc]: '<name>' gets no reply mailbox in a log names each service in the other case (it read routing: reply endpoint refused, unnamed, until backlog/74).

Source

pub fn drain_owed_replies(&self, owed: usize, max_ms: u64) -> Option<usize>

Wait, bounded by max_ms, for up to owed replies to arrive in the reply MAILBOX, and discard them. Returns how many were discarded, or None when this service has no mailbox - in which case owed-reply accounting cannot work (replies land on the shared endpoint) and the caller must fall back. For a synchronous caller that gave up on owed requests whose answers are still in flight: drain_stale_replies clears what has ARRIVED; this also waits for what has not.

Source

pub fn recv_handle(&self) -> Option<CapHandle>

Return the recv cap handle for direct-handle use (e.g. wrong-right test probing).

Source

pub fn trace_as(&self, name: &str)

Declare this service’s own name for the trace ring, once, at startup.

A service cannot ask what it is called - identity is not ambient - so a traced service says. Costs nothing in trust: the whole event is already the emitter’s testimony (see crate::trace). A service that never calls this still traces; its events read ? in the caller column, which is the honest answer rather than a guess.

Source

pub fn trace_op_at(&self, peer: &str, at: u8)

Declare that requests to peer carry their opcode at byte at (default 0).

For protocols that prepend a correlation tag - shell -> fs and fs -> block-driver both do - byte 0 is a request id, so a trace that recorded it was showing noise in a column labelled “op”. The SDK is generic and cannot know; the service that owns the protocol can. See crate::trace::set_op_offset.

Source

pub fn metric(&self, name: &str, value: u64)

Publish a metric sample to events. Fire-and-forget, and never able to slow this service down.

The value is a SET, not an increment: this service owns the counter (it is this service’s own state, §3.8) and publishes what it currently reads; events remembers the last sample. So a service that dies leaves its final value behind rather than taking it along, which is the one useful thing an observer can still learn from a service that is gone.

Shaped exactly like trace_emit, and the shape is the point:

  • try_send, result DISCARDED. An observer must never slow, block or fail the thing it observes. A full sink loses one sample, which the sink counts and reports (invariant 12).
  • No clock read here. The sink stamps it; a CMOS RTC read on every publish once cost the shell its keystrokes.
  • Reacquire on failure only, so the healthy path is unchanged and a restarted sink does not leave every emitter holding a stale generation forever (14.3).

SELF-OBSERVATION NEEDS NO CHECK HERE, and that is deliberate. events holds no send cap to events, so this resolves to u32::MAX inside its own address space and returns - the same cut that stops the sink tracing its own sends. A service reporting on itself must never take an IPC hop, because the hop is itself a reportable event and the ring would fill with itself. events counts its own work locally instead (docs/observability.md 9).

Source

pub fn request_with_reply(&self, peer: &str, msg: &Message) -> Option<Message>

Synchronous request/reply on the caller’s own endpoint (kernel Call, syscall 41). Blocks until the reply arrives, or until the kernel wakes it with ReplyDead because the replier died (8.6). It has NO deadline: a replier that stays alive and never answers holds the caller forever.

Source

pub fn request_with_reply_call_err( &self, peer: &str, msg: &Message, max_secs: i64, ) -> Result<Option<Message>, IpcError>

request_with_reply_call, saying WHY it failed instead of collapsing everything to None.

Source

pub fn request_with_reply_deadline_into( &self, peer: &str, req: &[u8], buf: &mut [u8], max_secs: i64, ) -> Option<usize>

Bounded request/reply that decodes straight into the caller buffer.

This variant collapses “the deadline passed” and “the send never left” into one None, so the ring records both as TIMEOUT. Where that difference matters the caller should be using _call_err or _deadline_outcome, which keep it - and so does the ring.

Source

pub fn request_with_reply_deadline( &self, peer: &str, msg: &Message, max_secs: i64, ) -> Option<Message>

Bounded request/reply. None is “no answer within the deadline” (see _into on the collapse).

Source

pub fn request_with_reply_ms( &self, peer: &str, msg: &Message, max_ms: u64, ) -> Option<Message>

request_with_reply_deadline with a millisecond budget.

Source

pub fn request_with_reply_deadline_outcome( &self, peer: &str, msg: &Message, max_secs: i64, ) -> DeadlineOutcome

Bounded request/reply that distinguishes every way it can fail.

Source

pub fn request_with_reply_deadline_sifted( &self, peer: &str, msg: &Message, max_secs: i64, mine: impl FnMut(&Message) -> bool, ) -> DeadlineOutcome

Bounded request/reply that SIFTS what arrives instead of believing the first thing that lands.

Every other variant on this page returns whatever message turns up next, which is correct for a caller whose endpoint carries nothing but its own replies and WRONG for a caller that also SERVES on it. net-stack is the second kind: it receives client requests and nic-driver replies in one mailbox, so a client that spoke while it was waiting for the driver was consumed as the driver’s answer, misparsed, and never served - the failure docs/net-tags-design.md opens with, and the reason that service still cannot do any background work.

mine is asked about each message as it arrives, BEFORE anything else is done with it, and answers one question: is this the reply I sent for? Say true and the wait ends with it. Say false and the wait continues - the message is the caller’s to deal with, and the caller is the only thing that can, because only it knows whether that means stashing a client request or discarding a reply that arrived too late. This is deliberately not an enum of dispositions: the SDK would then be deciding what to keep, which is policy, and the thing being kept usually has a reply capability that must be reclaimed or answered rather than merely stored (26.10, 8.5).

The kernel’s per-task pending cap and receive badge are both readable from inside mine (take_pending_cap, last_recv_badge), which is the point of calling it at the moment of arrival rather than afterwards: they describe THE MESSAGE JUST RECEIVED and are overwritten by the next one. A discriminator built on the pending cap is what net-stack uses, and it is free

  • a client request carries a reply cap, a driver reply does not.

Everything else matches Self::request_with_reply_deadline_outcome exactly, including the reply-cap reclaim on a failed send (a delivered request’s cap is the peer’s - backlog/67) and the warning attached to Timeout: the request was SENT, so a late reply is still coming and re-sending desyncs the protocol.

Source

pub fn request_with_reply_ms_sifted( &self, peer: &str, msg: &Message, max_ms: u64, mine: impl FnMut(&Message) -> bool, ) -> Option<Message>

Self::request_with_reply_deadline_sifted, bounded in MILLISECONDS.

The same reason the unsifted _ms form exists: a sub-second window cannot be built out of a whole-second bound, and the drain inside net-stack’s ~900 ms ping window is exactly such a window. Bounded by the CYCLE counter for the same reason too - it is the only clock here with sub-second resolution, and it is the one the caller’s own budget is measured in.

Source

pub fn request_with_reply_abortable( &self, peer: &str, msg: &Message, max_secs: i64, ) -> ReqOutcome

Bounded request/reply the user can abandon with q.

Source

pub fn request_with_reply_qhint( &self, peer: &str, msg: &Message, hint_after_secs: i64, max_secs: i64, on_linger: impl FnOnce(), ) -> ReqOutcome

request_with_reply_abortable, plus a callback when the wait starts to linger.

Source

pub fn request_with_reply_qhint_keeping( &self, peer: &str, msg: &Message, hint_after_secs: i64, max_secs: i64, on_linger: impl FnOnce(), keep: &dyn Fn(u8), ) -> ReqOutcome

Self::request_with_reply_qhint that HANDS BACK every key it reads that is not a quit key.

A wait that watches the console for q has to read what is typed, and a key read is a key taken: the plain form discards the rest, so whatever the operator typed ahead while it waited was lost (backlog/fixed/80 H16). This passes each one to keep, in order, for the caller to put where its own input reader will find it first.

Source

pub fn request_with_reply_keyhint( &self, peer: &str, msg: &Message, hint_after_secs: i64, max_secs: i64, leave_keys: &[u8], on_linger: impl FnOnce(), ) -> ReqOutcome

Self::request_with_reply_qhint with the LEAVE KEYS chosen by the caller. The convention (utilities/0_conventions.md) gives q to a task that STOPS when the operator leaves and b to one that keeps running; a request whose peer finishes the work whether or not this caller waits - wifi radio off hard, where the driver cuts the chip’s power either way - is the second kind, and offering q for it would promise a stop that cannot happen. Aborted means one of leave_keys was pressed; what that means for the task is the caller’s to say.

Source

pub fn request_with_reply_keyhint_sifted( &self, peer: &str, msg: &Message, hint_after_secs: i64, max_secs: i64, leave_keys: &[u8], on_linger: impl FnOnce(), mine: impl FnMut(&Message) -> bool, ) -> ReqOutcome

Self::request_with_reply_keyhint that SIFTS what arrives, as Self::request_with_reply_deadline_sifted does for the unabortable wait. mine is asked about each message as it lands; true ends the wait with it, false hands it to the caller - who must release any cap it carries - and the wait goes on, its deadline unchanged by how many arrive. Messages ALREADY queued when it is called are drained first without being asked (the stale-reply drain every abortable wait opens with), so a caller that counts late answers drains before calling.

For a peer whose replies the caller can recognise (a correlation tag) on an endpoint that also takes other traffic: the unsifted form returns the first message whatever it is, so a late answer to a request this caller already gave up on is read as the answer to this one.

Source

pub fn request_with_reply_keyhint_sifted_keeping( &self, peer: &str, msg: &Message, hint_after_secs: i64, max_secs: i64, leave_keys: &[u8], on_linger: impl FnOnce(), mine: impl FnMut(&Message) -> bool, keep: &dyn Fn(u8), ) -> ReqOutcome

Self::request_with_reply_keyhint_sifted that HANDS BACK every key it reads that is not a leave key, in order, as Self::request_with_reply_qhint_keeping does for the unsifted request. For a caller whose waits must say [q] quit and must not lose what the operator types meanwhile.

Source

pub fn stack_mark(&self) -> usize

A mark of how deep the stack currently is, in bytes below the first mark taken.

THE INSTRUMENT THAT WAS MISSING. Five attempts were made to fit correlation tags into fs without ever measuring the frame they had to fit in; each removed one 4 KiB item, the overflow returned at a different pc, and QEMU - which has no disk, so fs never mounts and never makes the deep calls - reported every build clean. A change to stack usage that cannot be measured can only be guessed at, and five guesses is the evidence.

Safe Rust and no kernel involvement: the address of a local IS the stack pointer, near enough. Relative to a mark taken at service start, so it needs no knowledge of where the stack lives.

Source

pub fn stack_used(&self) -> usize

Bytes of stack used at this point, without needing a reference mark.

A mark taken at service start only works if the report is taken at the DEEPEST point, and the first attempt took it after mount had returned - so it measured a popped frame and printed zero. Absolute is harder to misuse: it can be called from anywhere and still means the same thing.

The stack is one region, 256 KiB, and top-aligned, so rounding the current pointer UP to the next 256 KiB boundary lands on its top and the difference is what has been used. That derives the top from the alignment the kernel already guarantees rather than restating the address here, which would be a second copy of a layout decision (Commandment III).

Source

pub fn request_with_reply_call( &self, peer: &str, msg: &Message, max_secs: i64, ) -> Option<Message>

A bounded request/reply through the KERNEL’s CallDeadline, returning the reply as a message.

The same primitive request_with_reply_deadline_into already uses, in the shape the other request helpers have, so a caller can move to it without restructuring its buffers.

Use this rather than request_with_reply_deadline / _deadline_outcome when the caller SERVES clients on the same endpoint it awaits replies on. Those helpers wait with a plain timed recv, which takes whatever is next - so under client load the peer’s reply can be crowded out of a 16-deep queue and the caller reports a timeout that blames the peer for something it did correctly. net-stack hit exactly that: the wire showed 7 DHCP REQUESTs sent and 7 ACKs returned while the log said “6 of 6 REQUESTs never left the host - the driver refused them”.

CallDeadline does not have that failure: call_dequeue matches the reply BY ITS SENDER and leaves every other message queued, which is the entire reason §8.2 added it - hand-rolling the bounded wait out of send + recv is what lost messages. No new kernel surface: syscall 50 and its semantics are existing, ratified law. max_secs <= 0 is a one-second bound, never none: the kernel reads a 0 as NO deadline, and a bounded call turning into an unbounded one by a unit slip is the hang it exists to prevent (backlog/fixed/80 S5).

Source

pub fn request_with_reply_deadline_outcome_into( &self, peer: &str, req: &[u8], buf: &mut [u8], max_secs: i64, ) -> DeadlineOutcomeInto

Bounded request/reply into a caller buffer, reporting WHICH failure happened.

Same work as Self::request_with_reply_deadline_into, same stack shape - the caller supplies the buffer so no reply-sized frame appears here - but it does not throw the reason away. See DeadlineOutcomeInto for why the distinction is not a nicety: the two common failures want opposite responses, and a caller given None has to guess wrong in one direction or the other.

Source

pub fn recv_abortable_deadline(&self, max_secs: i64) -> ReqOutcome

Wait for the next message on our own recv endpoint, ABORTABLE (q/Q/ESC) and bounded by an RTC deadline, WITHOUT sending anything. The failure-aware twin of Self::recv: where recv blocks forever (and panics on error), this returns ReqOutcome::Timeout if no message arrives within max_secs and ReqOutcome::Aborted the instant the user presses q. A receive ERROR is not reported: it reads as no message, so the wait runs to its deadline. Use it to await a reply we already sent by some path OTHER than a named-peer request - a badged resource_invoke (a file/socket capability), or draining a pipe filter’s stream - where Self::request_with_reply_abortable (which does its own send) does not fit. This is the wait half of the abortable request, factored out: a peer that received our invocation but died before replying, or a filter that wedges mid-stream, can no longer hang us (Commandment VIII - wait on truth including failure). The caller owns any reply cap it derived and reclaims it only when the send failed (backlog/67), not on every outcome. A service with no console foreground never sees input, so this degrades to a plain deadline wait. Does NOT drain a stale reply first - a caller that can be re-entered after an abort should while self.try_recv().is_some() {} before it sends (as the request variants do).

Source

pub fn recv_abortable_deadline_keeping( &self, max_secs: i64, keep: &dyn Fn(u8), ) -> ReqOutcome

Self::recv_abortable_deadline that HANDS BACK every key it reads that is not a quit key, in order, as Self::request_with_reply_qhint_keeping does for a request. The plain form reads the console to see a q and discards everything else, so what the operator typed while a pipe stage ran was lost: ask’s answer takes seconds, and the next command typed during it vanished in QEMU (the receive-side half of backlog/fixed/80 H16).

Source

pub fn recv_abortable_deadline_noticing( &self, max_secs: i64, hint_after_secs: i64, on_linger: &dyn Fn(), keep: &dyn Fn(u8), ) -> ReqOutcome

Self::recv_abortable_deadline_keeping that calls on_linger ONCE when no message has come after hint_after_secs - the receive-side twin of Self::request_with_reply_qhint. The SDK supplies the timing and the caller the words, as there: a task with no prompt has nothing to print, so this never writes to the console itself.

Source

pub fn reacquire_by_name(&self, peer: &str) -> bool

Reacquire a fresh SEND cap to peer and point the named-peer cache at it, so subsequent try_send(peer) / send(peer) use the new cap. Returns false if peer cannot currently be resolved (e.g. it has not finished respawning) - the caller should retry on a later tick.

A thin shim over reacquire_cap (syscall 10): name resolution is the kernel name directory, not a service. The directory is populated synchronously at each service’s spawn, so there is no round-trip and no bootstrap chicken-and-egg (the directory lives in the kernel, always reachable). reacquire_cap also updates the send-cap cache.

Source

pub fn self_grant_handle(&self) -> Option<CapHandle>

Handle to this service’s SEND|GRANT cap to its own endpoint, minted at spawn. A service hands a copy of its endpoint to a peer by deriving one (derive_cap) and granting it across - keeping this original so it can derive again later. None if the service has no endpoint.

Source

pub fn send_peer_handle(&self, peer: &str) -> Option<CapHandle>

The send cap for a named peer, or None if this service has no such peer.

The public form of what request_with_reply resolves internally. Exposed for the case that helper cannot serve: a request whose reply must NOT be waited for. A single-threaded service that owns an in-memory answer (the time service and its wall clock) has to be able to send a best-effort request to a slow peer without becoming unable to answer its own clients meanwhile - so it pairs this with derive_cap + send_with_cap_by_handle and picks the reply up later, out of its ordinary receive loop, matched by the protocol’s correlation tag.

Source

pub fn send_peer_at(&self, idx: usize) -> Option<CapHandle>

Return the cap handle for the Nth send-peer entry (0-indexed).

Used by property-test probes (P9) to access multiple cap slots wired to the same endpoint, verifying all are invalidated on endpoint death (§7.5).

Source

pub fn try_send_by_handle( &self, handle: CapHandle, msg: &Message, ) -> Result<(), IpcError>

Send to a specific cap handle directly, bypassing peer-name lookup.

Used by the probe service to test kernel cap enforcement (§22 Tests 3B, 9B).

Source

pub fn send_with_cap(&self, peer: &str, msg: &Message) -> Result<(), IpcError>

Send a message to a named peer WITH an embedded capability grant.

The send-peer cap (which must carry the GRANT right) is transferred to the receiver. On success the calling service loses that cap (§7.6). Returns CapNotGrantable if the cap lacks GRANT - the cap is kept.

Source

pub fn take_pending_cap(&self) -> Option<CapHandle>

Take the next pending received capability, if any.

After recv() delivers a message containing an embedded cap, the kernel installs the cap into this task’s table and queues the slot index. Call this once per embedded cap to retrieve each one.

Source

pub fn acquire_send_grant_cap(&self, peer: &str) -> Option<CapHandle>

Acquire a fresh SEND|GRANT cap to peer via the kernel name directory.

Used by property-test probes that need to transfer capabilities (P3). Returns None if the service is not registered or the cap table is full.

Source

pub fn acquire_send_cap(&self, peer: &str) -> Option<CapHandle>

Acquire a fresh SEND cap to peer via the kernel name directory.

Returns the new cap handle, or None if the name is not registered.

Source

pub fn inspect_endpoint_generation(&self, name: &str) -> u64

Query the current generation of the named endpoint.

Returns the generation counter as a u64, or 0 if the name is not registered OR the caller lacks INTROSPECT (query 2 is gated). Used by property tests P2 and P8 (§7.5, §14.2).

Source

pub fn com2_byte(&self) -> Option<u8>

One byte from the COM2 operator channel, or None when the port is empty (kernel query 21).

The kernel owns the UART - hardware, and §11.4 already sanctions it owning a serial console - and hands bytes out; the control service owns what they MEAN. Transport in the kernel, interpretation in a service (C1-6).

Source

pub fn board_mac(&self) -> Option<[u8; 6]>

The board’s own MAC address (kernel query 23), or None where the board cannot say.

A NIC driver runs in userspace and cannot reach the board identity: on the Pi this address lives in the VideoCore mailbox, nowhere near the controller’s register window. Without it a driver has to invent an address, and an invented address that is hardcoded is the same on every board - which stops being harmless the moment two of them share a network.

Source

pub fn fire_irq(&self, irq: u8) -> bool

Inject a test interrupt. Requires the FIRE_IRQ capability, held only by control; a non-holder gets false rather than a silent no-op.

Source

pub fn inspect_kernel_alloc_bytes(&self) -> u64

Return the bytes dynamically allocated by this task so far.

Wraps InspectKernel query 0. Used by property test P4 (§10.3).

Source

pub fn inspect_kernel_endpoint_count(&self) -> Option<u32>

Return the count of live endpoints in the kernel routing table.

Wraps InspectKernel query 1 (INTROSPECT-gated; 0 when refused). Used by property test P5 (§8.3).

Source

pub fn usb_disk_sectors(&self) -> u64

Capacity of the in-kernel USB mass-storage device in 512-byte sectors, 0 if none is attached. Requires the USB_DISK_RESOURCE capability. Syscall 46. The kernel drives no USB disk on any port now (see USB_DISK_BUSY), so this is always 0 and the block calls below always fail.

Source

pub fn usb_disk_read(&self, lba: u64, dst: &mut [u8; 512]) -> bool

Read the 512-byte block at lba from the USB mass-storage device into dst. Returns false if there is no device, the LBA is past the end, or the transfer failed. Requires USB_DISK_RESOURCE. Syscall 47.

Source

pub fn usb_disk_write(&self, lba: u64, src: &[u8; 512]) -> bool

Write src as the 512-byte block at lba on the USB mass-storage device. Requires USB_DISK_RESOURCE. Syscall 48.

Source

pub fn usb_disk_read_status(&self, lba: u64, dst: &mut [u8; 512]) -> i64

Read a block, returning the raw status so BUSY is distinguishable from FAILED: 0 ok, the device is busy (USB_DISK_BUSY - re-ask, nothing is wrong), anything else a real error. The bool variants above collapse those two, which is exactly the conflation that made a busy stick look broken.

Source

pub fn usb_disk_write_status(&self, lba: u64, src: &[u8; 512]) -> i64

Write a block, returning the raw status (see usb_disk_read_status).

Source

pub fn usb_disk_flush(&self) -> bool

Make previously written blocks durable on the USB mass-storage device (SCSI SYNCHRONIZE CACHE). Requires USB_DISK_RESOURCE WRITE. Syscall 49.

A write is only ACKNOWLEDGED when usb_disk_write returns - the device may still be holding the bytes in a volatile buffer. Anything that promises durability (a format, a journal commit) has to ask for it, and check the answer: false means the data is NOT known to be on the medium.

Source

pub fn emmc_base_clock_hz(&self) -> u32

The SD/EMMC controller’s base clock in Hz (0 if the platform does not report one), via InspectKernel query 20. The block driver needs it to compute its clock divider: on the BCM283x the controller’s own capability register reports the base clock wrongly, and the driver holds only its controller’s registers, so it cannot ask the platform firmware itself. A driver that gets 0 should report that rather than guess - a wrong divider runs the card’s identification clock at the wrong speed, which fails on hardware and not under emulation. Ungated - task-neutral hardware info, like the console geometry and the RTC.

Source

pub fn nic_vendor_device(&self) -> u32

The discovered NIC’s PCI identity as vendor | device<<16 (0 if no NIC), via InspectKernel query 14. A NIC driver reads it to know which chip it is driving (e.g. Intel e1000 = 0x100E_8086 vs Realtek RTL8168 = 0x8168_10EC). Ungated - task-neutral hardware info.

Source

pub fn xhci_present(&self) -> bool

Whether the PCI scan found an xHCI USB host controller (InspectKernel query 18, ungated task-neutral hardware fact). The supervisor reads it to skip spawning the xhci driver on a machine that has no xHCI controller (an idle driver would busy-hold a core). Falls back to true if the query is unavailable, preserving the always-spawn behaviour.

Source

pub fn ehci_present(&self) -> bool

Whether the PCI scan found an EHCI (USB 2.0) host controller (InspectKernel query 18, ungated). The supervisor reads it to skip spawning the ehci driver on a machine with no EHCI at all (e.g. the Wyse 5070), so an idle driver does not busy-hold a core. Falls back to true if the query is unavailable, preserving the always-spawn behaviour.

Source

pub fn nic_present(&self) -> bool

Whether the PCI scan found a NIC this build can actually drive (present AND an e1000 or RTL8168), via InspectKernel query 18 (ungated). The supervisor reads it to skip spawning nic-driver (and its dependent net-stack) on a machine with no usable NIC, so they do not busy-hold cores. Falls back to true if the query is unavailable, preserving the always-spawn behaviour.

Source

pub fn nic_mmio_base(&self) -> u64

The NIC’s register-space MMIO base (the BAR the PCI scan chose), 0 if none. InspectKernel query 15, ungated. A diagnostic - a driver reads it to confirm which BAR it was handed.

Source

pub fn inspect_kernel_free_frames(&self) -> Option<u64>

Return the number of free physical frames.

Wraps InspectKernel query 4. Queries 4-8 are INTROSPECT-gated, and these wrappers read a refusal as 0 (or, for the core count, 1) - a value the caller cannot tell from a real one.

Source

pub fn inspect_kernel_total_frames(&self) -> Option<u64>

Return the total usable physical frames at boot time.

Wraps InspectKernel query 5.

Source

pub fn task_own_endpoint(&self, slot: u32) -> Option<u64>

The endpoint task slot OWNS, or 0 if it owns none (InspectKernel query 24, INTROSPECT).

With Self::task_awaits_endpoint this is the whole of the blocked-chain walk: map the endpoint a stuck task awaits back to the task that owns it, then ask what THAT one awaits. See utilities/47_events.md.

Source

pub fn task_awaits_endpoint(&self, slot: u32) -> Option<u64>

The endpoint task slot is blocked-in-CALL awaiting a reply from, or 0 if no call is in flight (InspectKernel query 25, INTROSPECT).

A best-effort snapshot, on the same contract as Self::task_stat: the kernel reads it without taking the routing lock, so an observer cannot stall the thing it observes.

Source

pub fn boot_record_size(&self) -> Option<(usize, usize)>

The kernel’s BOOT RECORD size: (held, capacity) in bytes, or None without INTROSPECT (InspectKernel query 27). The record is a fixed copy of the first bytes ever logged, which never wraps; held == capacity means it filled, and later lines are only in the kernel’s ring (until it wraps) and on serial.

Source

pub fn boot_record_read( &self, offset: usize, buf: &mut [u8; 512], ) -> Option<usize>

Copy the boot record from offset into buf: Some(n) bytes, Some(0) at the end, None refused (InspectKernel query 27, INTROSPECT). The buffer is the kernel’s chunk size exactly, so no read can ask it to write past the end of one.

Source

pub fn boot_datetime(&self) -> Datetime

The wall-clock datetime captured by the kernel at boot (InspectKernel query 12, ungated). Same packed layout as datetime. Pairs with datetime to compute uptime as a wall-clock delta - portable across timer modes (a tick counter’s rate is not: periodic-mode QEMU ticks at ~10 Hz, TSC-deadline HW at 100 Hz). Returns the epoch (all-zero fields) if not captured.

Source

pub fn uptime_secs(&self) -> i64

System uptime in seconds = now − boot, both from the hardware RTC. Never negative (saturates at 0). The uptime shell command renders this. Wall-clock based, so it is correct regardless of the APIC timer mode (unlike a raw tick counter).

Source

pub fn inspect_core_active_ticks(&self, core: u32) -> Option<u64>

Timer ticks the given core spent running a user task (not idle) since boot.

Wraps InspectKernel query 6 (arg1 = core index).

Source

pub fn inspect_core_total_ticks(&self, core: u32) -> Option<u64>

Total timer ticks seen on the given core since boot.

Wraps InspectKernel query 7 (arg1 = core index).

Source

pub fn inspect_core_count(&self) -> u32

Number of CPU cores ready since boot.

Wraps InspectKernel query 8.

Source

pub fn console_dims(&self) -> (u16, u16)

Terminal geometry as (rows, cols) text cells, or (0, 0) if it cannot be determined.

Asked of the console service, not the kernel. It used to be InspectKernel query 9, which is now deleted: rows and columns are derived from the safe-area inset, the cell size and the font-scale rule, all of which live in the terminal - so the terminal is the only party that can answer, and asking anyone else would be a second source of truth (Commandment III).

(0, 0) means unknown, and callers already treat it that way explicitly rather than substituting a size behind the user’s back (the pager prints unpaged; edit falls back to 24x80 and says so). It is returned when there is no console service, when it is mid-restart, or when it holds no framebuffer - all cases where a guessed geometry would be a silent fallback.

Costs one IPC round trip, so callers that lay out a screen should ask once and keep the answer for that screen rather than per line.

Source

pub fn console_history( &self, from: u16, buf: &mut [u8], ) -> Option<(usize, u16, bool, usize)>

Read a page of the console’s scrollback AS DATA, starting at line from (0 = oldest KEPT).

Returns (lines, total retained, any line has aged out, bytes written). The payload after the 4-byte header is lines records of [len, bytes...]; bytes is the whole reply so a caller can bound its own walk.

The replacement for the deleted console_scroll, which could not be made cheap. Moving the terminal’s own view forces a full repaint before the console can reply - on a 3840x2160 panel the most expensive thing it does, paid inside the caller’s blocking request, from the same core (backlog/fixed/37). This asks for BYTES: the console copies out of its ring and replies, and the caller paints its own screen through ordinary output, which is a send rather than a call and carries no deadline at all.

The history is BOUNDED (32 KiB / 512 lines). aged is how a view says “this is the oldest line kept” rather than implying it is the start of the session (§26.7).

Source

pub fn input_ready(&self) -> bool

Whether the input driver has reported setup complete (syscall 13, query 10). The deterministic end-of-boot signal: the shell watches it to auto-clear the boot screen the moment the keyboard subsystem is up. Ambient.

Source

pub fn signal_input_ready(&self)

Report that input-subsystem setup is complete (syscall 27). Called by the USB keyboard driver (xHCI) in every terminal path once it has finished - the end-of-boot signal. Requires the CONSOLE_PUSH cap (the input driver only).

Source

pub fn read_tsc(&self) -> u64

Read the hardware TSC (Time Stamp Counter) via the kernel.

Returns the arch’s free-running counter (InspectKernel query 3): RDTSC on x86, CNTPCT on ARM; its rate is tsc_ticks_per_10ms. Useful for measuring kernel operation latencies in benchmark probes (§22 Perf B1-B10). Not comparable across hosts.

Source

pub fn tsc_ticks_per_10ms(&self) -> u64

Counter ticks per 10 ms, from the kernel’s boot-time calibration (CPUID on x86, the measured timer rate / 100 on ARM) (InspectKernel query 16). Convert a TSC delta to milliseconds with delta_cycles * 10 / tsc_ticks_per_10ms(). Returns 0 if the TSC was not calibrated (callers should then skip the millisecond conversion). ping uses it to report round-trip time.

Source

pub fn sleep_ms(&self, ms: u64)

Sleep for approximately ms milliseconds - the PORTABLE way to pace a loop.

Prefer this over sleep(cycles) for anything expressing a DURATION. A “cycle” is not a portable unit: on x86 it is a ~2 GHz CPU cycle, on the Pi 2 it is a tick of a ~1 MHz timer, so the same literal means two wildly different lengths of time. Services were written with x86 in mind - 60_000_000 meaning “~30 ms at 2 GHz” - and on ARM that same constant asks for ~60 SECONDS once the quantum is calibrated. Three loops (the shell’s muted poll and observe q-poll, and observe’s repaint pace) were exactly that, and each would have become a minute-long stall.

Converts through the kernel’s own calibration, so it is right on any machine. Floors at one scheduler quantum: the underlying sleep(0) returns immediately, which would turn a paced loop into a busy spin - the opposite of what every caller wants.

Source

pub fn duration_cycles(&self, ms: u64) -> u64

Cycles for approximately ms milliseconds, for the two syscalls that take a cycle count (sleep, recv_timeout). See Self::sleep_ms for why a raw cycle literal is not portable.

Never returns 0. That matters beyond pacing: recv_timeout(0) means block forever, so a zero here would convert a bounded wait into an unbounded one - a hang produced by a unit conversion. An uncalibrated machine (query 16 reads 0) also lands on the floor, which is exactly the one-quantum behaviour those platforms had before any of this existed.

Source

pub fn datetime(&self) -> Datetime

Read the hardware real-time clock (wall-clock date/time) via the kernel.

Ambient - the time of day is task-neutral hardware info, like the TSC. Wraps InspectKernel query 11; the kernel returns the fields packed into a u64 (see kernel/src/arch/x86_64/rtc.rs), which this unpacks.

Source

pub fn epoch_secs_monotonic(&self) -> i64

Deglitched monotonic “now” in epoch seconds (kernel query 17). Unlike datetime().epoch_secs() (the raw RTC, query 11 - a CMOS misread on an in-range year slips through and reads years off), this drops backward / huge-forward glitches. Use it for time-DELTA deadlines and pacing, NOT for display.

Source

pub fn hw_random(&self) -> Option<u32>

A hardware-random u32 from the SoC RNG (the BCM2835 RNG on the Pi 2), or None if this build exposes no hardware RNG. Ungated (entropy confers no authority). The random shell utility consumes it.

Source

pub fn gpio(&self, op: u32, pin: u32) -> i64

Drive a SoC GPIO pin (the shell gpio command; the Pi 2’s BCM2835). op: 0 input / 1 output / 2 high / 3 low / 4 read. Returns the level (0/1) for a read, 0 on success, -1 on a bad pin / unsupported arch. Gated by the GPIO_DEVICE cap (only the shell holds it).

Source

pub fn task_stat(&self, slot: u32) -> TaskStat

Query the kernel task stat for scheduler slot slot (syscall 16).

Returns a best-effort snapshot. If slot is out of range or the task is dead, valid will be false - and also when the caller lacks the INTROSPECT cap the kernel requires, which this cannot tell apart.

Source

pub fn task_caps(&self, slot: u32, out: &mut [CapInfo]) -> usize

List the capabilities held by the task in slot, into out. Returns the number of entries written (capped at out.len() and 64). Requires the INTROSPECT cap; a refusal returns 0, the same as a task holding nothing. Best-effort snapshot - see task_stat.

Source

pub fn send_by_handle( &self, handle: CapHandle, msg: &Message, ) -> Result<(), IpcError>

Send a message via an explicit cap handle (blocking).

Used by benchmark probes that dynamically acquire send caps rather than using named peer slots, avoiding repeated name-lookup overhead.

Source

pub fn query_cap_rights(&self, handle: CapHandle) -> Option<u64>

Query the rights bitfield of the cap at handle.

Returns the rights byte as a u64, or None if the slot is empty. Used by property test P3 to verify rights do not widen on transfer (§7.3).

Source

pub fn remove_cap(&self, handle: CapHandle)

Remove the cap at handle from this task’s cap table.

Idempotent: removing an already-empty slot is a no-op.

Source

pub fn send_with_cap_by_handle( &self, endpoint: CapHandle, grant: CapHandle, msg: &Message, ) -> Result<(), IpcError>

Send a message to endpoint with cap grant embedded.

Unlike send_with_cap (which looks up a peer by name), this takes explicit handles - used by P3 where the endpoint and grant slot are the same (self-referential cap transfer). On success the grant cap is removed from the caller’s table (§7.6).

Source

pub fn resource_mint(&self, rights: u8) -> Option<(u64, CapHandle)>

Mint a delegated resource (§7.10, P2 file-as-capability): the kernel allocates a fresh resource owned by this service and a cap for it carrying rights (use the RIGHT_* bits), returning (resource_id, cap). Requires the RESOURCE_MINT authority (held by fs). The resource_id is what the kernel badges into a later resource_invoke, so this service knows which resource a client is acting on (e.g. fs maps it → file). None if the authority is missing, the band is full, or the cap table is full. Syscall 30 = ResourceMint.

Source

pub fn resource_invoke( &self, file: CapHandle, right: u8, reply: CapHandle, msg: &Message, ) -> Result<(), IpcError>

Use a delegated resource cap (§7.10) - the “send” of file-as-capability. The kernel validates the cap holds right (a read needs RIGHT_READ, a write needs RIGHT_WRITE; a cap lacking it fails CapInsufficientRights - the non-escalation check), then routes msg to the owning service badged with the resource id + the validated right, embedding reply (a SEND|GRANT cap) so the owner can reply. Ok(()) on delivery. Syscall 31.

Source

pub fn last_recv_badge(&self) -> Option<(u64, u8)>

Read (and clear) the delegated-resource badge of the message just recv’d (§7.10). A service that owns delegated resources (e.g. fs) calls this right after recv: Some(( resource_id, right)) means the message was a kernel-validated invocation of a real cap on resource_id with right already checked (the owner enforces op ≤ right); None means an ordinary message (no badge - handle it on the name-addressed path). The badge cannot be forged over a plain send, so its presence is trustworthy. Syscall 33.

Source

pub fn resource_revoke(&self, resource_id: u64) -> bool

Revoke a delegated resource this service owns (§7.10): bumps its generation so every outstanding cap to it goes stale (clients see CapRevoked/EndpointDead on next use). Owner-gated by the kernel (ownership is the check). true on success. Syscall 32.

Source

pub fn console_push(&self, byte: u8)

Inject one byte into the console input ring (syscall 20). Only effective for an input-driver service holding a CONSOLE_PUSH cap (the USB keyboard driver, §12); the byte reaches the shell exactly like a serial keystroke. No-op for services without the cap.

Source

pub fn console_read(&self) -> u8

Block until one byte is available on COM1 console input (syscall 17).

Returns the byte value. Only usable by a service spawned with a CONSOLE_READ cap (SPAWN_FLAG_REQ_CONSOLE in its spawn request: the shell, chaos, observe-live). A kernel error is returned as byte 0, and a missing cap or corrupt context parks the caller in a busy loop.

Source

pub fn try_console_read(&self) -> Option<u8>

Non-blocking console read (syscall 24). Returns Some(byte) if a keystroke is waiting, None if the ring is empty. A foreground full-screen app polls this for q-to-quit between repaints instead of blocking in console_read. Requires the CONSOLE_READ cap (SPAWN_FLAG_REQ_CONSOLE in the spawn request).

Source

pub fn console_echo(&self, on: bool)

Enable (true) or disable (false) console keystroke echo (syscall 25). A foreground full-screen app disables echo while it owns the screen - so its raw key polls do not smear its frame - and re-enables it on exit. Requires the CONSOLE_READ cap.

Source

pub fn console_boot_complete(&self)

End boot-log mirroring to the framebuffer and clear the TV (syscall 26). The shell calls this once, on the first keystroke, so the user sees the boot sequence on the display and then gets a clean interactive console. Requires the CONSOLE_READ cap.

Source

pub fn is_console_foreground(&self) -> bool

Whether THIS task currently owns (or shares, when unclaimed) console input - i.e. its console reads return bytes. False when another task holds the foreground (syscall 40, e.g. chaos): a backgrounded task should then stay quiet (not draw, not read) and redraw its prompt only when this returns true again. InspectKernel query 13 (ungated, caller-specific).

Source

pub fn claim_console_foreground(&self)

Claim exclusive console input (syscall 40, op = 1): after this, only THIS task’s try_console_read returns bytes; every other task reads empty. The chaos service claims it for the duration of a run so a resurrected shell cannot swallow its q-to-quit. Pair with release_console_foreground on exit, after ensuring a live shell exists to hand the keyboard back to. Requires the CONSOLE_READ cap.

Source

pub fn release_console_foreground(&self)

Release exclusive console input (syscall 40, op = 0) back to the unclaimed state, so any CONSOLE_READ holder (the shell) reads normally again. Idempotent.

Source

pub fn core_id(&self) -> u32

Return the core this service was spawned on.

Source

pub fn xhci_mmio(&self) -> Option<Mmio>

Safe MMIO handle for this service’s xHCI controller, if one was granted (§12). The kernel mapped the controller’s BAR into this driver’s address space; the returned crate::Mmio reads/writes the uncached device registers directly. None for non-driver services.

Source

pub fn ehci_mmio(&self) -> Option<Mmio>

Safe MMIO handle for this service’s EHCI controller, if one was granted (§12). Reads the same kernel-mapped controller-BAR window as xhci_mmio - a driver service holds exactly one controller, so the field is shared and unambiguous. None for non-drivers.

Source

pub fn framebuffer(&self) -> Option<Framebuffer>

The framebuffer the kernel granted this service, or None if it holds no grant.

Held only by console, which renders the terminal into it (docs/console-service.md §9). The grant carries PIXEL geometry only - character rows and columns are the terminal’s own business, and the kernel deliberately does not compute or publish them (Commandment III).

Source

pub fn mmio(&self) -> Option<Mmio>

Safe MMIO handle to this service’s device register window, if one was granted (§12) - the neutrally-named accessor for non-USB drivers (e.g. the AHCI block-driver, which maps its HBA ABAR here). Same kernel-mapped window as xhci_mmio. None for non-driver services.

Source

pub fn dma_region(&self) -> Option<Dma>

Safe handle to this service’s DMA arena, if one was granted (§12). The kernel mapped a physically-contiguous region into this driver; the returned crate::Dma gives the CPU view (read/write) and the physical base to program into the controller. None for non-driver services.

Source

pub fn irq_vector(&self) -> Option<u8>

The interrupt vector the kernel granted this service, if any.

A driver must NOT hardcode this. It used to be safe to: every vector was a kernel constant, so both sides knew the number statically. The MSI pool allocates one at spawn instead, and a driver holding a stale copy does not fail loudly - it keeps working (the interrupt still wakes it) while reporting every interrupt as an unknown message. That is exactly what services/xhci did on the T630: 0 MSI, 7799 msg.

Use it to recognise an interrupt notification: the kernel’s wake payload is the vector number.

ⓘ
let irq = ctx.irq_vector();
let is_irq = irq.is_some_and(|v| msg.payload_bytes() == [v]);

None means no interrupt was granted - a non-driver service, or a driver whose device offered neither MSI nor MSI-X. A driver that needs interrupts should say so rather than poll silently.

Source

pub fn irq_vectors(&self) -> &'static [u8]

Every interrupt vector granted to this service (at most four).

Self::irq_vector is the common case; this is for a driver routed more than one line.

Source

pub fn pci_cfg_read(&self, sel: u32, offset: u16) -> Option<u32>

Read one 32-bit PCI configuration register. Requires the PCI_CFG privilege.

sel is a bus/device/function selector in the platform’s own encoding, which is the CALLER’S knowledge - the kernel performs the access and enforces which registers may be reached, but never interprets what the selector names. offset is the register within that function.

ONE call, not a select-then-read pair, because the underlying hardware register pair is stateful: split across two calls, two callers would each read whichever register the other selected, and the KERNEL uses the same pair on its spawn and kill paths. Atomic here, that window does not exist - and this interface cannot write anything at all.

None if the capability is missing, or the access is one this machine will not admit (a bus its bridge does not forward, say - reading past it is an abort on some hardware rather than a harmless all-ones). Both are reported by the kernel too (invariant 12), so a caller need not guess which it was. Note the distinction from Some(0xFFFF_FFFF), which is a SUCCESSFUL read of an absent device - the bus floats high, and that is data, not an error.

Source

pub fn device_power(&self, on: bool) -> bool

Cut (false) or restore (true) the power of the device this service was granted a fixed peripheral window to (DevicePower, syscall 54). Needs DEVICE_POWER, which the kernel mints with the window wherever the machine can power the device; everywhere else, and for every service without such a window, this returns false and the kernel says why on the serial log. How long to hold the device off and how long to wait after restoring it are the caller’s to know

  • they are properties of the device, not of the kernel (docs/wifi.md 47).
Source

pub fn cpu_clock(&self, max: bool) -> Option<u32>

Set the Arm cores to the platform’s maximum (true) or minimum (false) clock, and return the rate they read back in Hz (CpuClock, syscall 55). Needs CPU_CLOCK, which only the power service is spawned with; everyone else asks power for a lease over IPC. None is a refusal or a machine with no control over its clock, and the kernel says which on the serial log.

Source

pub fn power_off(&self) -> i64

Turn the machine off NOW (PowerOff, syscall 56, op 0). Needs POWER_OFF, which the shell holds for the operator. With it, this does not return: the machine is off, or - where the hardware cannot cut its own power - every core is halted and the kernel’s last line says it is safe to switch off. It returns only when refused, with the kernel’s code; the kernel says why on the serial log.

Source

pub fn power_off_in(&self, secs: u64) -> i64

Set the machine’s one pending power-off secs from now (1 ..= 24 h), held by the KERNEL so it survives every service (docs/shutdown.md 6). 0: this is now the pending one. 1: one due sooner was kept (earliest wins). 2: a reboot is pending, and is kept. -1: out of range. -2: refused (no POWER_OFF).

Source

pub fn power_off_cancel(&self) -> i64

Cancel the pending power-off. 0: there was one. 1: none is pending. 2: the pending one is a reboot, and is kept. -2: refused.

Source

pub fn reboot_in(&self, secs: u64) -> i64

Set the machine to reboot secs from now (1 ..= 24 h), on the same one kernel deadline as a power-off (PowerOff op 3). Needs REBOOT. The answers are power_off_in’s, with 2 meaning a power-off is pending. A reboot NOW is reboot.

Source

pub fn reboot_cancel(&self) -> i64

Cancel the pending reboot (PowerOff op 4). The answers are power_off_cancel’s, with 2 meaning the pending one is a power-off.

Source

pub fn power_pending(&self) -> Option<PowerPending>

The machine’s pending power action (InspectKernel query 28): whole seconds until it, rounded up, and whether it is a reboot - or None when none is pending. Readable by every task, deliberately: a pending shutdown or reboot must never be hidden.

Source

pub fn power_off_pending(&self) -> Option<u64>

Whole seconds until the pending power-off, or None when none is pending - a pending reboot is not one.

Source

pub fn reboot_pending(&self) -> Option<u64>

Whole seconds until the pending reboot, or None when none is pending - a pending power-off is not one.

Source

pub fn alloc_mem(&self, size: usize) -> Result<u64, AllocError>

Allocate size bytes of read/write memory within this task’s budget.

Returns the virtual address of the mapping on success, or AllocError if the allocation would exceed the contract memory limit (AllocDenied) or physical memory is exhausted.

Source

pub fn reboot(&self) -> !

Trigger a hardware reset via the kernel reboot syscall (18). Does not return.

The kernel prints reboot: hardware reset to serial before resetting, so the operator sees confirmation before the line goes silent. Requires REBOOT (held by the shell); a refusal is logged and the caller then yields forever.

Source

pub fn try_reboot(&self) -> i64

Attempt a reboot but RETURN the syscall result instead of assuming it never comes back.

A successful reset does not return; a denial returns a negative error code (CapNotHeld = -2 when the caller lacks the REBOOT capability, §3.1). For tests/probes that must observe the denial without resetting the machine - ordinary rebooters use reboot().

Source

pub fn exit(&self, status: u32) -> !

End this program, with status: 0 for success, anything else for a failure it has already said.

The kernel ends the caller and no one else - it takes no capability - and tells the supervisor how it ended (death::EXITED and the status), which is how a program that FINISHED is told apart from one that crashed. A service the supervisor keeps running is restarted all the same: exiting is for a program that does one job (CLAUDE.md 14.5 amendment).

Source

pub fn yield_cpu(&self)

Advisory yield (§9.3).

Source

pub fn park(&self) -> !

Park this task forever: block with no waker. For idle services that have no further work - far better than loop { yield_cpu() }, which keeps the core busy and prevents it from halting (so it never runs cool). Nothing wakes a parked task in v1; the loop re-parks defensively.

Source

pub fn log(&self, msg: &str)

Log a string via the kernel ring buffer and serial (syscall 5, requires the log_write cap). Over 256 bytes it is cut on a character boundary and followed by [TRUNCATED]. Without the cap it does nothing, and a kernel refusal is not reported.

Source

pub fn print(&self, msg: &str)

Write a string to serial WITHOUT a trailing newline (syscall 22, requires the log_write cap). For inline output such as the shell prompt, where log’s newline would push the user’s typed echo to the next line. A string longer than the kernel’s 256-byte limit is written in pieces, each ending on a character boundary - it was dropped whole, with no marker, until 2026-10-10 (backlog/fixed/80 S5).

Source

pub fn log_fmt(&self, args: Arguments<'_>)

Log a formatted message, rendered into a 256-byte stack buffer. A message longer than that is cut on a character boundary and ends ..., so the cut is visible (backlog/fixed/80 S5: it was cut with no marker, and a cut through a character logged (fmt error) instead of the message).

Source

pub fn console_write(&self, msg: &str)

Write a string to the interactive console (serial + framebuffer), WITHOUT a trailing newline (syscall 23, requires log_write cap in Stage 1). This is the user-facing path: the shell prompt, command results, and observe frames. Unlike log/print (now serial-only), this also reaches the framebuffer/TV - the interactive surface (see docs/console-service.md).

Source

pub fn console_writeln(&self, msg: &str)

Write a string to the interactive console followed by a newline.

Source

pub fn console_write_fmt(&self, args: Arguments<'_>)

Write a formatted message to the interactive console, with no trailing newline (e.g. a pager status line the cursor should park on).

Source

pub fn console_writeln_fmt(&self, args: Arguments<'_>)

Write a formatted message to the interactive console, followed by a newline.

Source

pub fn console_line(&self, clear_eol: bool, msg: &str)

Write one console line. When clear_eol is true the line ends with ESC[K (erase to end of line) before the newline - so a full-screen app repainting in place (cursor homed each frame) overwrites a previous, longer line without leaving stale characters, and without a full-screen clear (no flicker). When false, behaves exactly like console_writeln.

Source

pub fn console_line_fmt(&self, clear_eol: bool, args: Arguments<'_>)

Formatted variant of Self::console_line.

Source

pub fn spawn(&self, name: &str) -> Result<(), Error>

Spawn a service by name on the kernel-selected core.

Source

pub fn spawn_on(&self, name: &str, core: u32) -> Result<(), Error>

Spawn a service by name on core (0xFFFF = kernel round-robin).

Asks the SUPERVISOR when this service can reach it, and the kernel otherwise.

Step C moves service images out of the kernel (docs/service-ownership.md), and a service the kernel does not hold cannot be spawned by name through the kernel. Seven separate callers did exactly that - control’s RESTART, the supervisor’s own respawn, the shell’s spawn, the shell’s PIPES, spawncap, spawnwired, and chaos - and each was found by a regression rather than by reading. Routing HERE fixes all of them at once, including the ones not yet found.

With no supervisor peer there is nothing to ask: the kernel’s by-name spawn (syscall 7) was retired, because its catalogue holds supervisor alone and it could only refuse (backlog/fixed/80 K14). So that case is refused here, plainly, rather than sent to a syscall that would refuse it less clearly.

Source

pub fn spawn_via_supervisor( &self, name: &str, core: u32, peers: &[&str], ) -> Result<Option<CapHandle>, Error>

Ask the supervisor to spawn name, returning the SEND|GRANT cap it hands back (None if the service has no recv endpoint, or the cap could not be taken).

The supervisor is RESTARTABLE (6.2), so a cached cap to it goes stale on every respawn. This reacquires by name and retries ONCE when the send NEVER LEFT - a stale cap - and never when it was delivered: not on Ok(None), where the deadline passed, and not on ReplyDead, where the supervisor died holding it. Either may have started the service, and retrying would start it twice. (ReplyDead was retried until 2026-10-10, backlog/fixed/80 S1.) A full queue is not retried either: the supervisor is alive and busy, and the caller decides whether to ask again.

Source

pub fn spawn_image( &self, req: &mut SpawnRequest, peers_buf: &mut [u8], peers: &[&str], ) -> Result<Option<CapHandle>, Error>

Spawn a task from an image THIS service supplies (SpawnImage, syscall 52).

The kernel loads what it is handed instead of looking a name up in its own catalogue - the step that ends that catalogue (docs/service-ownership.md). Requires the SPAWN capability, exactly as spawn does: supplying the image is not authority to start it - AND the caller must hold IMAGE_SPAWN, which only the supervisor has and cannot delegate (CLAUDE.md 14.1). A name already running comes back as AlreadyRunning; every other refusal as InvalidArgument, and the kernel’s serial log says which it was.

peers are NUL-joined into a caller-owned buffer; each name still resolves through the kernel name directory, so this grants nothing a contract’s send_peers would not. Returns a SEND|GRANT cap to the new service’s recv endpoint, or None if it has none - the spawner needs it to record name -> cap and re-wire dependents after a restart.

Source

pub fn kill(&self, name: &str) -> Result<(), Error>

Kill a named service. Requires SERVICE_CONTROL (the kernel refuses with CapNotHeld otherwise); every failure is returned as InvalidArgument.

Source

pub fn restart( &self, name: &str, core_override: Option<u32>, ) -> Result<(), Error>

Kill then respawn a service with optional core override (§14.4).

A service that is not running is simply started - that is NotFound from the kill, and the one kill failure that is passed over. A refusal (no SERVICE_CONTROL) is returned, and nothing is spawned: until 2026-10-10 the kill’s result was discarded, so a caller without the authority to kill went on to spawn a second copy (backlog/fixed/80 S4, CLAUDE.md 13.6).

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.