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
impl ServiceContext
Sourcepub const QUIT_KEYS: &'static [u8]
pub const QUIT_KEYS: &'static [u8]
The keys Self::request_with_reply_qhint leaves on: q, Q and Escape.
Sourcepub fn capability(&self, name: &str) -> Result<CapHandle, CapError>
pub fn capability(&self, name: &str) -> Result<CapHandle, CapError>
Look up a named capability from this service’s cap table.
Sourcepub fn recv(&self) -> Message
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.
Sourcepub fn try_recv(&self) -> Option<Message>
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).
Sourcepub fn recv_timeout(&self, timeout_cycles: u64) -> Option<Message>
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.
Sourcepub fn irq_unmask(&self, vector: u8)
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.
Sourcepub fn sleep(&self, cycles: u64)
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).
Sourcepub fn recv_result(&self) -> Result<Message, IpcError>
pub fn recv_result(&self) -> Result<Message, IpcError>
Block until a message arrives; returns the error instead of looping silently.
Sourcepub fn send(&self, peer: &str, msg: &Message) -> Result<(), IpcError>
pub fn send(&self, peer: &str, msg: &Message) -> Result<(), IpcError>
Send to a named peer declared in ipc_send. Blocking.
Sourcepub fn try_send(&self, peer: &str, msg: &Message) -> Result<(), IpcError>
pub fn try_send(&self, peer: &str, msg: &Message) -> Result<(), IpcError>
Non-blocking send; returns QueueFull immediately if the queue is full.
Sourcepub fn reacquire_cap(&self, peer: &str) -> Result<CapHandle, CapError>
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.
Sourcepub fn reacquire_cap_detail(
&self,
peer: &str,
) -> Result<CapHandle, AcquireFailure>
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).
Sourcepub fn derive_cap(&self, held: CapHandle) -> Option<CapHandle>
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.
Sourcepub fn probe_mode(&self) -> u32
pub fn probe_mode(&self) -> u32
Return the probe mode written by the kernel at spawn (0 for all production services).
Sourcepub fn drain_stale_replies(&self) -> usize
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).
Sourcepub fn drain_owed_replies(&self, owed: usize, max_ms: u64) -> Option<usize>
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.
Sourcepub fn recv_handle(&self) -> Option<CapHandle>
pub fn recv_handle(&self) -> Option<CapHandle>
Return the recv cap handle for direct-handle use (e.g. wrong-right test probing).
Sourcepub fn trace_as(&self, name: &str)
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.
Sourcepub fn trace_op_at(&self, peer: &str, at: u8)
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.
Sourcepub fn metric(&self, name: &str, value: u64)
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).
Sourcepub fn request_with_reply(&self, peer: &str, msg: &Message) -> Option<Message>
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.
Sourcepub fn request_with_reply_call_err(
&self,
peer: &str,
msg: &Message,
max_secs: i64,
) -> Result<Option<Message>, IpcError>
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.
Sourcepub fn request_with_reply_deadline_into(
&self,
peer: &str,
req: &[u8],
buf: &mut [u8],
max_secs: i64,
) -> Option<usize>
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.
Sourcepub fn request_with_reply_deadline(
&self,
peer: &str,
msg: &Message,
max_secs: i64,
) -> Option<Message>
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).
Sourcepub fn request_with_reply_ms(
&self,
peer: &str,
msg: &Message,
max_ms: u64,
) -> Option<Message>
pub fn request_with_reply_ms( &self, peer: &str, msg: &Message, max_ms: u64, ) -> Option<Message>
request_with_reply_deadline with a millisecond budget.
Sourcepub fn request_with_reply_deadline_outcome(
&self,
peer: &str,
msg: &Message,
max_secs: i64,
) -> DeadlineOutcome
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.
Sourcepub fn request_with_reply_deadline_sifted(
&self,
peer: &str,
msg: &Message,
max_secs: i64,
mine: impl FnMut(&Message) -> bool,
) -> DeadlineOutcome
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.
Sourcepub fn request_with_reply_ms_sifted(
&self,
peer: &str,
msg: &Message,
max_ms: u64,
mine: impl FnMut(&Message) -> bool,
) -> Option<Message>
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.
Sourcepub fn request_with_reply_abortable(
&self,
peer: &str,
msg: &Message,
max_secs: i64,
) -> ReqOutcome
pub fn request_with_reply_abortable( &self, peer: &str, msg: &Message, max_secs: i64, ) -> ReqOutcome
Bounded request/reply the user can abandon with q.
Sourcepub fn request_with_reply_qhint(
&self,
peer: &str,
msg: &Message,
hint_after_secs: i64,
max_secs: i64,
on_linger: impl FnOnce(),
) -> ReqOutcome
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.
Sourcepub 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
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.
Sourcepub 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
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.
Sourcepub 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
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.
Sourcepub 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
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.
Sourcepub fn stack_mark(&self) -> usize
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.
Sourcepub fn stack_used(&self) -> usize
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).
Sourcepub fn request_with_reply_call(
&self,
peer: &str,
msg: &Message,
max_secs: i64,
) -> Option<Message>
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).
Sourcepub fn request_with_reply_deadline_outcome_into(
&self,
peer: &str,
req: &[u8],
buf: &mut [u8],
max_secs: i64,
) -> DeadlineOutcomeInto
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.
Sourcepub fn recv_abortable_deadline(&self, max_secs: i64) -> ReqOutcome
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).
Sourcepub fn recv_abortable_deadline_keeping(
&self,
max_secs: i64,
keep: &dyn Fn(u8),
) -> ReqOutcome
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).
Sourcepub fn recv_abortable_deadline_noticing(
&self,
max_secs: i64,
hint_after_secs: i64,
on_linger: &dyn Fn(),
keep: &dyn Fn(u8),
) -> ReqOutcome
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.
Sourcepub fn reacquire_by_name(&self, peer: &str) -> bool
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.
Sourcepub fn self_grant_handle(&self) -> Option<CapHandle>
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.
Sourcepub fn send_peer_handle(&self, peer: &str) -> Option<CapHandle>
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.
Sourcepub fn send_peer_at(&self, idx: usize) -> Option<CapHandle>
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).
Sourcepub fn try_send_by_handle(
&self,
handle: CapHandle,
msg: &Message,
) -> Result<(), IpcError>
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).
Sourcepub fn send_with_cap(&self, peer: &str, msg: &Message) -> Result<(), IpcError>
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.
Sourcepub fn take_pending_cap(&self) -> Option<CapHandle>
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.
Sourcepub fn acquire_send_grant_cap(&self, peer: &str) -> Option<CapHandle>
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.
Sourcepub fn acquire_send_cap(&self, peer: &str) -> Option<CapHandle>
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.
Sourcepub fn inspect_endpoint_generation(&self, name: &str) -> u64
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).
Sourcepub fn com2_byte(&self) -> Option<u8>
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).
Sourcepub fn board_mac(&self) -> Option<[u8; 6]>
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.
Sourcepub fn fire_irq(&self, irq: u8) -> bool
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.
Sourcepub fn inspect_kernel_alloc_bytes(&self) -> u64
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).
Sourcepub fn inspect_kernel_endpoint_count(&self) -> Option<u32>
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).
Sourcepub fn usb_disk_sectors(&self) -> u64
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.
Sourcepub fn usb_disk_read(&self, lba: u64, dst: &mut [u8; 512]) -> bool
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.
Sourcepub fn usb_disk_write(&self, lba: u64, src: &[u8; 512]) -> bool
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.
Sourcepub fn usb_disk_read_status(&self, lba: u64, dst: &mut [u8; 512]) -> i64
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.
Sourcepub fn usb_disk_write_status(&self, lba: u64, src: &[u8; 512]) -> i64
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).
Sourcepub fn usb_disk_flush(&self) -> bool
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.
Sourcepub fn emmc_base_clock_hz(&self) -> u32
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.
Sourcepub fn nic_vendor_device(&self) -> u32
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.
Sourcepub fn xhci_present(&self) -> bool
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.
Sourcepub fn ehci_present(&self) -> bool
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.
Sourcepub fn nic_present(&self) -> bool
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.
Sourcepub fn nic_mmio_base(&self) -> u64
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.
Sourcepub fn inspect_kernel_free_frames(&self) -> Option<u64>
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.
Sourcepub fn inspect_kernel_total_frames(&self) -> Option<u64>
pub fn inspect_kernel_total_frames(&self) -> Option<u64>
Return the total usable physical frames at boot time.
Wraps InspectKernel query 5.
Sourcepub fn task_own_endpoint(&self, slot: u32) -> Option<u64>
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.
Sourcepub fn task_awaits_endpoint(&self, slot: u32) -> Option<u64>
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.
Sourcepub fn boot_record_size(&self) -> Option<(usize, usize)>
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.
Sourcepub fn boot_record_read(
&self,
offset: usize,
buf: &mut [u8; 512],
) -> Option<usize>
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.
Sourcepub fn boot_datetime(&self) -> Datetime
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.
Sourcepub fn uptime_secs(&self) -> i64
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).
Sourcepub fn inspect_core_active_ticks(&self, core: u32) -> Option<u64>
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).
Sourcepub fn inspect_core_total_ticks(&self, core: u32) -> Option<u64>
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).
Sourcepub fn inspect_core_count(&self) -> u32
pub fn inspect_core_count(&self) -> u32
Number of CPU cores ready since boot.
Wraps InspectKernel query 8.
Sourcepub fn console_dims(&self) -> (u16, u16)
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.
Sourcepub fn console_history(
&self,
from: u16,
buf: &mut [u8],
) -> Option<(usize, u16, bool, usize)>
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).
Sourcepub fn input_ready(&self) -> bool
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.
Sourcepub fn signal_input_ready(&self)
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).
Sourcepub fn read_tsc(&self) -> u64
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.
Sourcepub fn tsc_ticks_per_10ms(&self) -> u64
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.
Sourcepub fn sleep_ms(&self, ms: u64)
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.
Sourcepub fn duration_cycles(&self, ms: u64) -> u64
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.
Sourcepub fn datetime(&self) -> Datetime
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.
Sourcepub fn epoch_secs_monotonic(&self) -> i64
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.
Sourcepub fn hw_random(&self) -> Option<u32>
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.
Sourcepub fn gpio(&self, op: u32, pin: u32) -> i64
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).
Sourcepub fn task_stat(&self, slot: u32) -> TaskStat
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.
Sourcepub fn task_caps(&self, slot: u32, out: &mut [CapInfo]) -> usize
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.
Sourcepub fn send_by_handle(
&self,
handle: CapHandle,
msg: &Message,
) -> Result<(), IpcError>
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.
Sourcepub fn query_cap_rights(&self, handle: CapHandle) -> Option<u64>
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).
Sourcepub fn remove_cap(&self, handle: CapHandle)
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.
Sourcepub fn send_with_cap_by_handle(
&self,
endpoint: CapHandle,
grant: CapHandle,
msg: &Message,
) -> Result<(), IpcError>
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).
Sourcepub fn resource_mint(&self, rights: u8) -> Option<(u64, CapHandle)>
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.
Sourcepub fn resource_invoke(
&self,
file: CapHandle,
right: u8,
reply: CapHandle,
msg: &Message,
) -> Result<(), IpcError>
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.
Sourcepub fn last_recv_badge(&self) -> Option<(u64, u8)>
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.
Sourcepub fn resource_revoke(&self, resource_id: u64) -> bool
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.
Sourcepub fn console_push(&self, byte: u8)
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.
Sourcepub fn console_read(&self) -> u8
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.
Sourcepub fn try_console_read(&self) -> Option<u8>
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).
Sourcepub fn console_echo(&self, on: bool)
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.
Sourcepub fn console_boot_complete(&self)
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.
Sourcepub fn is_console_foreground(&self) -> bool
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).
Sourcepub fn claim_console_foreground(&self)
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.
Sourcepub fn release_console_foreground(&self)
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.
Sourcepub fn xhci_mmio(&self) -> Option<Mmio>
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.
Sourcepub fn ehci_mmio(&self) -> Option<Mmio>
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.
Sourcepub fn framebuffer(&self) -> Option<Framebuffer>
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).
Sourcepub fn mmio(&self) -> Option<Mmio>
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.
Sourcepub fn dma_region(&self) -> Option<Dma>
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.
Sourcepub fn irq_vector(&self) -> Option<u8>
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.
Sourcepub fn irq_vectors(&self) -> &'static [u8]
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.
Sourcepub fn pci_cfg_read(&self, sel: u32, offset: u16) -> Option<u32>
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.
Sourcepub fn device_power(&self, on: bool) -> bool
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.md47).
Sourcepub fn cpu_clock(&self, max: bool) -> Option<u32>
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.
Sourcepub fn power_off(&self) -> i64
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.
Sourcepub fn power_off_in(&self, secs: u64) -> i64
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).
Sourcepub fn power_off_cancel(&self) -> i64
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.
Sourcepub fn reboot_in(&self, secs: u64) -> i64
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.
Sourcepub fn reboot_cancel(&self) -> i64
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.
Sourcepub fn power_pending(&self) -> Option<PowerPending>
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.
Sourcepub fn power_off_pending(&self) -> Option<u64>
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.
Sourcepub fn reboot_pending(&self) -> Option<u64>
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.
Sourcepub fn alloc_mem(&self, size: usize) -> Result<u64, AllocError>
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.
Sourcepub fn reboot(&self) -> !
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.
Sourcepub fn try_reboot(&self) -> i64
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().
Sourcepub fn exit(&self, status: u32) -> !
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).
Sourcepub fn park(&self) -> !
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.
Sourcepub fn log(&self, msg: &str)
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.
Sourcepub fn print(&self, msg: &str)
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).
Sourcepub fn log_fmt(&self, args: Arguments<'_>)
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).
Sourcepub fn console_write(&self, msg: &str)
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).
Sourcepub fn console_writeln(&self, msg: &str)
pub fn console_writeln(&self, msg: &str)
Write a string to the interactive console followed by a newline.
Sourcepub fn console_write_fmt(&self, args: Arguments<'_>)
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).
Sourcepub fn console_writeln_fmt(&self, args: Arguments<'_>)
pub fn console_writeln_fmt(&self, args: Arguments<'_>)
Write a formatted message to the interactive console, followed by a newline.
Sourcepub fn console_line(&self, clear_eol: bool, msg: &str)
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.
Sourcepub fn console_line_fmt(&self, clear_eol: bool, args: Arguments<'_>)
pub fn console_line_fmt(&self, clear_eol: bool, args: Arguments<'_>)
Formatted variant of Self::console_line.
Sourcepub fn spawn(&self, name: &str) -> Result<(), Error>
pub fn spawn(&self, name: &str) -> Result<(), Error>
Spawn a service by name on the kernel-selected core.
Sourcepub fn spawn_on(&self, name: &str, core: u32) -> Result<(), Error>
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.
Sourcepub fn spawn_via_supervisor(
&self,
name: &str,
core: u32,
peers: &[&str],
) -> Result<Option<CapHandle>, Error>
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.
Sourcepub fn spawn_image(
&self,
req: &mut SpawnRequest,
peers_buf: &mut [u8],
peers: &[&str],
) -> Result<Option<CapHandle>, Error>
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.
Sourcepub fn kill(&self, name: &str) -> Result<(), Error>
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.
Sourcepub fn restart(
&self,
name: &str,
core_override: Option<u32>,
) -> Result<(), Error>
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).