Split interface store into modules.

This commit is contained in:
Michael Mikovsky
2026-05-31 12:21:33 -06:00
parent 43a84c46f7
commit ba3a419bb2
8 changed files with 635 additions and 1209 deletions
+119 -772
View File
@@ -1,810 +1,157 @@
# Macro-Generated Leaf Interface Design # Template Leaf Interface Design
**Status:** Draft **Status:** Implemented draft
**Last updated:** 2026-05-28 **Last updated:** 2026-05-31
**Primary use case:** Remote PTY sessions over hook-backed UnShell packets **Primary use case:** Small generated leaf wrappers without proc-macro machinery
## Summary ## Summary
This document proposes a generated leaf interface for UnShell. The goal is to make Leaf generation now uses a declarative `unshell_leaf!` template instead of the old
stateful leaves, such as a remote PTY leaf, easy to write without forcing every leaf `#[unshell_leaf]` proc macro. The goal is to make generated code obvious, closer to
author to hand-code packet draining, procedure dispatch, session lookup, hook an HTML template than an AST transformation.
lifetime handling, and retry-safe final-frame cleanup.
The user writes the application logic: The macro only fills slots:
- the leaf state struct - wrapper name
- one or more session types for long-running hook-backed conversations - user state type
- one or more procedure types for one-packet operations - leaf id
- payload encoding and decoding - interface metadata
- OS-specific behavior, such as spawning or polling a PTY - named session families
- named procedure families
The macro generates the plumbing: All real behavior lives in normal Rust helpers under `src/protocol/runtime.rs`.
Those helpers are testable without macro parsing, `syn`, `quote`, or generated name
inference.
- the `Leaf` implementation ## User Shape
- the generated wrapper that owns session stores and retry queues
- inbound packet filtering for the leaf's procedure ids
- per-packet procedure dispatch
- per-hook session dispatch
- retry-safe outbound flushing
- final-frame session removal only after routing succeeds
The macro should generate ordinary Rust. No runtime registry, no boxed procedure
objects, and no hidden dynamic dispatch in the hot path.
## Problem
The current `Leaf` trait is deliberately small:
```rust ```rust
pub trait Leaf { pub struct FakePtyState {
fn get_id(&self) -> u32; pub active_count: usize,
pub total_opened: u64,
fn update(&mut self, endpoint: &mut Endpoint);
}
```
That makes the protocol runtime flexible, but it also means every non-trivial leaf
has to solve the same set of problems by hand:
- select only the inbound packets that belong to this leaf
- distinguish one-shot procedures from long-running sessions
- group session packets by `hook_id`
- keep per-session application state
- build response packets with the right `hook_id`, path, procedure id, and `end_hook`
- retry failed outbound packets without losing stream progress
- remove session state only after final packets route successfully
- avoid consuming packets intended for other leaves
A remote PTY leaf makes this pain obvious. A PTY session is bidirectional and long
lived. The same hook carries `Open`, `Input`, `Resize`, `StdinEof`, `Output`, `Exit`,
and errors. The endpoint already owns hook authorization, but the leaf still needs a
safe session state machine.
## Goals
- Automate the repetitive `Leaf::update` machinery.
- Keep application logic explicit and testable.
- Keep `unshell-protocol` minimal and no_std-friendly.
- Keep OS-specific PTY code outside `unshell-protocol`.
- Preserve the new endpoint hook model: downward packets pave hooks, final packets close hooks.
- Make final-frame retries hard to get wrong.
- Keep generated runtime code static and size-conscious.
- Let sessions and procedures mutate shared leaf state without directly accessing each other.
- Make multiple sessions of the same type cheap and predictable.
## Non-Goals
- Do not define a full actor framework.
- Do not add async requirements to the protocol layer.
- Do not make sessions discover or mutate other sessions directly.
- Do not introduce `Vec<Box<dyn Procedure>>` or `Vec<Box<dyn Session>>` runtime registries.
- Do not hide PTY business logic inside the macro.
- Do not add source-path fields to `Packet` just for PTY. The PTY `Open` payload can carry the reply path.
## Current Protocol Assumptions
The endpoint routing layer now owns hook lifetime:
```text
validated downward packet, end_hook=false -> open or refresh peer-bound hook
validated downward packet, end_hook=true -> close hook after successful route or delivery
validated upward packet -> require matching hook
validated upward packet, end_hook=true -> close hook after successful route or delivery
```
For PTY, this means:
- `Open` uses `end_hook = false` because it expects returned output.
- `Input`, `Resize`, `StdinEof`, and `Terminate` use `end_hook = false`.
- `StdinEof` is not `end_hook`. EOF closes stdin, not the whole PTY session.
- `Abort` may use `end_hook = true` if no acknowledgement is expected.
- `Output` uses `end_hook = false`.
- `Exit` or fatal `Error` uses `end_hook = true`.
## Crate Boundary
```text
unshell-protocol
Endpoint
Packet
Leaf
hook routing rules
filtered inbound drain API
unshell-runtime or unshell-leaves
real PTY worker implementation
std-only integrations
portable-pty adapter
unshell-macros
tiny proc-macro shim
unshell-macros-core
syn, quote, proc-macro2, deluxe or darling
parser and code generator tests
```
The generated code runs in the final binary. Macro parsing dependencies do not.
That means `syn`, `quote`, `deluxe`, and `darling` are acceptable in the macro crates,
but not in the protocol runtime.
## Required Endpoint Addition
The generated leaf must be able to drain only the packets it owns. The current
`take_inbound_clear` drains a whole local queue, which is unsafe once multiple
application leaves share an endpoint.
Add a filtered drain API:
```rust
impl Endpoint {
/// Drains packets from `local_id` that match `predicate`, preserving all other
/// packets in their original relative order.
pub fn take_inbound_matching<P, F>(&mut self, local_id: u32, predicate: P, f: F)
where
P: FnMut(&Packet) -> bool,
F: FnMut(&Packet);
}
```
For generated leaves, matching usually means:
```rust
packet.procedure_id == PROC_PTY
|| packet.procedure_id == PROC_PING
|| packet.procedure_id == PROC_CAPABILITIES
```
This is the one endpoint API the macro needs before it can be safe in mixed-leaf endpoints.
## User-Facing Macro
Use an attribute macro that wraps a user-owned state struct and generates a leaf type.
A derive macro alone cannot add storage fields to the original struct, so the macro
must generate a companion wrapper.
```rust
#[unshell_leaf(
leaf = RemotePtyLeaf,
id = LEAF_REMOTE_PTY,
sessions(PtySession),
procedures(PingProcedure, CapabilitiesProcedure)
)]
pub struct RemotePtyState {
max_sessions: usize,
default_rows: u16,
default_cols: u16,
}
```
The macro emits roughly:
```rust
pub struct RemotePtyLeaf {
state: RemotePtyState,
pty_sessions: SessionStore<PtySession>,
retry: PacketQueue,
} }
impl RemotePtyLeaf { unshell_leaf! {
pub fn new(state: RemotePtyState) -> Self { ... } pub leaf FakePtyLeaf for FakePtyState {
} id: LEAF_FAKE_PTY,
meta: unshell::protocol::LeafMeta {
impl Leaf for RemotePtyLeaf { name: "Fake PTY Leaf",
fn get_id(&self) -> u32 { identifier: "dev.unshell.v1.pty",
LEAF_REMOTE_PTY version: "v0",
} authors: unshell::alloc::vec!["ASTATIN3"],
},
fn update(&mut self, endpoint: &mut Endpoint) { sessions {
... generated dispatch ... pty: PtySession,
}
procedures {}
} }
} }
``` ```
The leaf wrapper is the object stored in `Endpoint::leaves`. The state struct stays The field name before each session type is explicit. The macro does not invent a
small and owned by the user. field name from the Rust type.
## Core Model ## Generated Shape
The example above expands to the equivalent of:
```rust
pub struct FakePtyLeaf {
state: FakePtyState,
outbox: LeafOutbox,
pty: SessionFamily<<PtySession as Session<FakePtyState>>::State>,
}
```
The wrapper implements:
- `new(state)`
- `state()`
- `state_mut()`
- `active_session_count()`
- `pending_packet_count()`
- `Leaf::get_id()`
- `Leaf::update()`
- feature-gated `Leaf::update_interface()`
- feature-gated `Leaf::get_meta()`
- feature-gated `Leaf::render_ratatui()`
## Runtime Helpers
The macro delegates behavior to small helpers:
- `dispatch_session`
- `update_session_family`
- `dispatch_procedure`
- `flush_leaf_outbox`
- `flush_session_family`
- `flush_packet_queue_with_interface`
This keeps the macro readable. The helper functions own the mechanics of session
lookup, initialization, retry-safe flushing, and optional interface logging.
## Interface Store
`InterfaceStore` is caller-owned. It records packet flow and timing without putting
UI state inside `Endpoint` or the leaf wrapper.
```text ```text
Endpoint inbound queue InterfaceStore
| events: Vec<InterfaceEvent>
v sessions: BTreeMap<SessionKey, SessionView>
+-------------------------------+ procedures: BTreeMap<ProcedureKey, ProcedureView>
| generated RemotePtyLeaf |
| |
| state: RemotePtyState |
| sessions: SessionStore |
| retry: PacketQueue |
+-------------------------------+
|
+--> Procedure packet -> Procedure::handle(...)
|
+--> Session packet -> by hook_id
create or update session
session queues outbound frames
generated flush handles retry
``` ```
Procedures and sessions can both mutate `RemotePtyState`. They cannot directly Generated leaves receive an optional mutable store during `update_interface`. The
borrow or mutate each other. Communication between them must happen through leaf helpers create and update the appropriate session/procedure views when packets are
state or through packets. dispatched, sessions update, and outbound routes succeed or fail.
## Sessions Time remains caller-supplied:
A session is a long-running hook-backed conversation. PTY is the main example.
```rust ```rust
pub trait Session<L> { interface.set_now_ns(Some(now_ns));
/// All packets for this session type use this outer procedure id. leaf.update_interface(endpoint, &mut interface);
const PROCEDURE_ID: u32;
/// State owned for one active hook/session.
type State;
/// Attempts to create a new session from an incoming packet.
fn init(
leaf: &mut L,
packet: Packet,
ctx: &mut SessionInit,
) -> SessionInitResult<Self::State>;
/// Advances one session. The generated leaf passes all queued packets for this
/// hook and one context that can enqueue outbound frames.
fn update(
leaf: &mut L,
session: &mut Self::State,
incoming: &mut PacketQueue,
ctx: &mut SessionCtx,
) -> SessionStatus;
}
``` ```
`L` is the user state type, for example `RemotePtyState`. No clock is embedded in the no_std protocol layer.
### Session Init Context ## Ratatui Rendering
Ratatui rendering is a plain feature-gated pass:
```rust ```rust
pub struct SessionInit { leaf.render_ratatui(frame, area, &mut interface);
hook_id: HookID, ```
packet_path: Vec<u32>,
}
pub enum SessionInitResult<S> { Session rendering is an associated function because session families are type-level
Created(S), contracts, not stored objects:
Rejected,
RejectedWith(Packet), ```rust
fn render_ratatui(
leaf: &LeafState,
session: &Self::State,
view: &mut SessionView,
frame: &mut ratatui::Frame<'_>,
area: ratatui::layout::Rect,
) {
} }
``` ```
`RejectedWith(Packet)` is intended for cases where the initializer can build a Procedure rendering is also associated and renders from leaf state plus the caller
protocol-level failure response, such as "too many PTY sessions". The generated leaf owned procedure view.
still owns routing and retry for that packet.
The PTY `Open` payload should include the caller reply path because `Packet` does ## Why This Replaced The Proc Macro
not currently carry source path.
The old proc macro had to parse attributes, infer names, generate many code paths,
and duplicate runtime logic inside codegen. That made the generator harder to reason
about than the leaf behavior it was trying to simplify.
The new design is intentionally boring:
```text ```text
Open payload: macro template -> named fields and loops
opcode runtime helpers -> behavior
reply_path_len caller InterfaceStore -> UI/log state
reply_path segments
rows
cols
command/env/options
``` ```
The session stores that reply path and uses it for upward output packets. That is the whole game.
### Session Update Context
Sessions should use a context wrapper rather than directly constructing packets.
The context can still carry restricted endpoint access when absolutely necessary,
but the normal output path should be helper methods.
```rust
pub struct SessionCtx<'a> {
endpoint: &'a mut Endpoint,
hook_id: HookID,
reply_path: &'a [u32],
procedure_id: u32,
outbox: &'a mut PacketQueue,
}
```
Helpers:
```rust
impl<'a> SessionCtx<'a> {
pub fn send(&mut self, opcode: u8, data: &[u8]);
pub fn send_final(&mut self, opcode: u8, data: &[u8]);
pub fn error(&mut self, code: u8, data: &[u8]);
pub fn error_final(&mut self, code: u8, data: &[u8]);
}
```
These helpers build packets like:
```rust
Packet {
hook_id: self.hook_id,
end_hook,
path: self.reply_path.to_vec(),
procedure_id: self.procedure_id,
data: encode_frame(opcode, payload),
}
```
The helper only queues packets. It does not route them immediately. The generated
leaf owns flushing and retry.
### Session Status
```rust
pub enum SessionStatus {
Running,
Closing,
Closed,
}
```
`Closed` means the session has no more application work. The generated leaf still
must keep the session until any final packet has routed successfully.
## Procedures
A procedure is a one-packet operation. It is appropriate for introspection, ping,
capabilities, and simple state queries.
```rust
pub trait Procedure<L> {
const PROCEDURE_ID: u32;
fn handle(
leaf: &mut L,
endpoint: &mut Endpoint,
packet: Packet,
out: &mut ProcedureOut,
);
}
```
Procedure helpers mirror session helpers but operate on one incoming packet:
```rust
pub struct ProcedureOut {
hook_id: HookID,
reply_path: Vec<u32>,
procedure_id: u32,
outbox: PacketQueue,
}
impl ProcedureOut {
pub fn send(&mut self, data: &[u8]);
pub fn send_final(&mut self, data: &[u8]);
}
```
Procedures do not directly access sessions. If a procedure needs information about
sessions, that information should be mirrored into leaf state by the session code.
## PTY Binary Protocol
One PTY session uses one hook and one outer procedure id. The inner PTY protocol is
a tiny binary frame in `Packet::data`.
If API names use `end_state` at the session layer, it maps directly to
`Packet::end_hook` at the protocol layer.
```text
Packet {
hook_id: session hook,
procedure_id: PROC_PTY,
data: [opcode, payload...],
end_hook: session lifetime marker,
}
```
Suggested opcodes:
| Opcode | Direction | Meaning | end_hook |
|---:|---|---|---|
| 0 | Downward | Open PTY | false |
| 1 | Upward | Opened | false |
| 2 | Downward | Input bytes | false |
| 3 | Downward | Resize | false |
| 4 | Downward | Stdin EOF | false |
| 5 | Downward | Terminate process | false |
| 6 | Downward | Abort session without acknowledgement | true |
| 7 | Upward | Output bytes | false |
| 8 | Upward | Exit status | true |
| 9 | Upward | Fatal error | true |
`StdinEof` must not set `end_hook = true`. The remote process may still emit output
after stdin closes.
## Generated Update Loop
The generated `Leaf::update` should follow this shape:
```text
update(endpoint)
1. flush retry queue first
2. drain matching inbound packets only
3. dispatch procedure packets
4. group session packets by hook_id
5. create sessions for open packets
6. update active sessions
7. flush outbound frames
8. remove sessions whose final packet routed successfully
```
More concrete flow:
```text
+----------------------------+
| generated update |
+----------------------------+
|
v
flush retry packets
|
v
take_inbound_matching(local_id, owns_packet)
|
+--> procedure id match -> Procedure::handle
|
+--> session id match -> session inbox by hook_id
|
v
for each session inbox
|
+--> existing hook -> Session::update
|
+--> no hook -> Session::init, then Session::update
|
v
flush session/procedure outbox
|
+--> route success -> advance/remove when safe
|
+--> route failure -> keep retry packet and keep state
```
The retry rule is the most important generated invariant:
```text
If endpoint.add_outbound(packet) fails, the generated leaf must not:
- drop the packet
- advance a final frame
- remove the session
- consume state that cannot be reconstructed
```
## Generated Dispatch
The macro should emit static matches, not runtime registries.
Good:
```rust
match packet.procedure_id {
PtySession::PROCEDURE_ID => self.dispatch_pty_session(packet),
PingProcedure::PROCEDURE_ID => PingProcedure::handle(...),
CapabilitiesProcedure::PROCEDURE_ID => CapabilitiesProcedure::handle(...),
_ => self.handle_unknown(packet),
}
```
Avoid:
```rust
Vec<Box<dyn Procedure>>
Vec<Box<dyn Session>>
```
Static dispatch keeps the generated code visible to LLVM and avoids registry setup
costs in constrained binaries.
## Session Store
The first implementation can use a small `Vec` or `VecDeque` store.
```rust
pub struct SessionEntry<S> {
hook_id: HookID,
state: S,
inbox: PacketQueue,
outbox: PacketQueue,
retry: Option<Packet>,
closing: bool,
}
```
For minimal binaries, a later implementation can replace this with a fixed-capacity
table under a feature flag.
Required operations:
- find by `hook_id`
- insert if capacity allows
- push incoming packet to inbox
- enqueue outbound packet
- retain session while retry packet exists
- remove only after session is closed and outbox/retry are empty
## Unknown Packets
The generated leaf should have configurable unknown-packet behavior.
Default behavior:
- unknown procedure id remains in the endpoint queue because `take_inbound_matching` does not drain it
- unknown session opcode for a known session produces a fatal error frame and closes the session
- packet for unknown hook under a session procedure produces a fatal error frame if the payload is not a valid `Open`
For PTY:
```text
unknown hook + Open opcode -> create session
unknown hook + non-Open opcode -> Error end_hook=true
known hook + known opcode -> session update
known hook + unknown opcode -> Error end_hook=true
```
## Access Boundaries
Sessions and procedures can mutate leaf state:
```rust
fn update(leaf: &mut RemotePtyState, ...)
```
They should not directly access each other:
```text
Procedure -> no direct SessionStore access
Session -> no direct Procedure access
```
If cross-cutting data is needed, mirror it into `RemotePtyState`:
```rust
pub struct RemotePtyState {
active_count: usize,
total_spawned: u64,
max_sessions: usize,
}
```
This keeps borrowing simple and avoids turning the generated leaf into a shared
mutable object graph.
## Endpoint Access
The user proposed passing `&mut Endpoint` to sessions and procedures. The safest
design is slightly narrower:
- procedures may receive `&mut Endpoint` because they handle one packet and return immediately
- sessions should receive `SessionCtx`, which can expose narrow endpoint helpers
- a raw endpoint escape hatch can be added later if a real leaf proves it needs one
Rationale: sessions are long-lived and retry-sensitive. If session code calls
`endpoint.add_outbound` directly, it can bypass generated retry handling and lose a
final packet. The helper path keeps the dangerous part centralized.
## Remote PTY Example
User code:
```rust
#[unshell_leaf(
leaf = RemotePtyLeaf,
id = LEAF_REMOTE_PTY,
sessions(PtySession),
procedures(PtyCapabilities)
)]
pub struct RemotePtyState {
max_sessions: usize,
default_rows: u16,
default_cols: u16,
}
pub struct PtySession;
pub struct PtyState {
hook_id: HookID,
reply_path: Vec<u32>,
worker: PtyWorker,
saw_stdin_eof: bool,
}
impl Session<RemotePtyState> for PtySession {
const PROCEDURE_ID: u32 = PROC_PTY;
type State = PtyState;
fn init(
leaf: &mut RemotePtyState,
packet: Packet,
ctx: &mut SessionInit,
) -> SessionInitResult<Self::State> {
let open = decode_open(&packet.data)?;
if leaf.active_count >= leaf.max_sessions {
return SessionInitResult::RejectedWith(pty_error_busy(packet.hook_id));
}
let worker = PtyWorker::spawn(open.command, open.rows, open.cols)?;
SessionInitResult::Created(PtyState {
hook_id: ctx.hook_id(),
reply_path: open.reply_path,
worker,
saw_stdin_eof: false,
})
}
fn update(
leaf: &mut RemotePtyState,
session: &mut Self::State,
incoming: &mut PacketQueue,
ctx: &mut SessionCtx,
) -> SessionStatus {
while let Some(packet) = incoming.pop_front() {
match decode_pty_frame(&packet.data) {
PtyFrame::Input(bytes) => session.worker.write(&bytes),
PtyFrame::Resize { rows, cols } => session.worker.resize(rows, cols),
PtyFrame::StdinEof => {
session.saw_stdin_eof = true;
session.worker.close_stdin();
}
PtyFrame::Terminate => session.worker.terminate(),
PtyFrame::Abort => return SessionStatus::Closed,
_ => ctx.error_final(ERR_BAD_OPCODE, b"bad pty opcode"),
}
}
while let Some(bytes) = session.worker.poll_output() {
ctx.send(OP_OUTPUT, &bytes);
}
if let Some(status) = session.worker.poll_exit() {
ctx.send_final(OP_EXIT, &encode_exit(status));
leaf.active_count -= 1;
return SessionStatus::Closed;
}
SessionStatus::Running
}
}
```
The exact error handling syntax above is illustrative. The final API should avoid
`?` unless `SessionInitResult` has a clear conversion story.
## Generated Remote PTY Flow
```text
Caller Remote endpoint RemotePtyLeaf
------ --------------- -------------
allocate hook
Open end=false -------------> downward route opens hook ----> init PtyState
Input false -------------> hook remains active -----------> write PTY stdin
Resize false -------------> hook remains active -----------> resize PTY
StdinEof false -------------> hook remains active -----------> close PTY stdin
Output false <------------- upward route requires hook <---- ctx.send
Output false <------------- upward route requires hook <---- ctx.send
Exit true <------------- upward route closes hook <---- ctx.send_final
caller cleanup remove session
```
## Proc Macro Implementation
Rust still requires a proc-macro crate for real attribute macros. Keep that crate
thin and put the real logic in a normal testable crate.
```text
unshell-macros
#[proc_macro_attribute]
pub fn unshell_leaf(attr, item) -> TokenStream
unshell-macros-core
parse UnshellLeafArgs
parse state struct
generate wrapper
generate Leaf impl
generate dispatch methods
tests over proc_macro2::TokenStream
```
Recommended parser helper:
- `deluxe` for attribute parsing if the desired syntax uses richer Rust tokens
- `darling` if the syntax stays close to normal Rust meta attributes
Current recommendation: use `deluxe` for attribute parsing and `syn`/`quote` for
code generation.
## Generated Code Requirements
- Must compile without requiring std in `unshell-protocol`.
- Must not allocate handler trait objects.
- Must preserve generic parameters and where clauses on the state struct.
- Must emit useful compile errors for duplicate procedure ids.
- Must emit useful compile errors for duplicate session procedure ids.
- Must reject session and procedure id collisions unless explicitly allowed.
- Must preserve user attributes that are not consumed by the macro.
- Must not require users to manually implement `Leaf` for the generated wrapper.
## Testing Strategy
Phase 1 tests should use a fake PTY session, not `portable-pty`.
Protocol tests:
- `open_pty_paves_hook_and_creates_session`
- `input_and_output_share_one_hook`
- `stdin_eof_keeps_hook_until_exit`
- `exit_end_hook_cleans_route_and_session`
- `failed_final_exit_route_retries_without_losing_session`
- `abort_downward_end_hook_closes_without_ack`
- `unknown_session_input_returns_error_end_hook`
- `two_pty_sessions_interleave_without_crossing_hooks`
- `pty_leaf_does_not_consume_other_leaf_packets`
Macro tests:
- generated leaf implements `Leaf`
- duplicate procedure ids fail at compile time
- duplicate session procedure ids fail at compile time
- generic state structs expand correctly
- generated code routes final frames retry-safely
- generated code preserves unmatched inbound packets
Use `trybuild` for compile-fail macro tests once `unshell-macros` exists.
## Rollout Plan
1. Add `Endpoint::take_inbound_matching`.
2. Write a manual fake PTY leaf that follows the exact generated shape.
3. Add PTY session tests against the manual fake leaf.
4. Create `unshell-macros` and `unshell-macros-core`.
5. Generate the same shape as the manual fake leaf.
6. Port fake PTY tests to the generated leaf.
7. Add compile-fail macro tests.
8. Implement the real std-only PTY worker in `unshell-leaves` or `unshell-runtime`.
9. Add integration tests for the real worker where the platform supports PTYs.
This avoids writing a macro against an unproven design. First make the generated
shape real by hand, then teach the macro to emit it.
## Open Questions
- Should `SessionCtx` expose any raw `Endpoint` access, or only narrow helpers?
- Should session stores be `Vec` first, or should fixed-capacity storage be designed immediately?
- Should unknown opcodes produce an error packet by default, or should each session type decide?
- Should `Open` always carry `reply_path`, or should the packet format eventually add source path?
- Should `ProcedureOut` be retry-safe like session output, or are procedures allowed to fail fast?
- Should macro-generated leaves expose counters for active sessions and retry queue depth?
## Recommendation
Implement this as static generated code with a narrow context API.
Do not build a runtime plugin system. Do not give sessions raw access to session
stores. Do not let session code bypass generated output flushing. The macro should
make the correct routing and hook behavior the easiest path, especially for final
frames.
The first proof point should be fake PTY. If fake PTY works cleanly, real PTY is
mostly OS plumbing.
+76
View File
@@ -0,0 +1,76 @@
use crate::protocol::{EndpointError, HookID, Packet, SessionStatus};
/// Ordered event stored by [`crate::interface::InterfaceStore`].
///
/// Events are append-only. Views store indices into this list instead of copying the
/// same packet-flow records into every renderable bucket.
pub struct InterfaceEvent {
/// Monotonic event sequence assigned by the interface store.
pub sequence: u64,
/// Caller-provided timestamp, if the frontend supplied one.
pub time_ns: Option<u64>,
/// Leaf id that emitted or handled the event.
pub leaf_id: u32,
/// Detailed event payload.
pub kind: InterfaceEventKind,
}
/// Interface-visible event emitted by generated helpers.
pub enum InterfaceEventKind {
/// A packet was delivered to a generated leaf.
Inbound { packet: Packet },
/// A packet was queued into an already-live session inbox.
SessionPacketQueued { procedure_id: u32, hook_id: HookID },
/// A hook-backed session was created successfully.
SessionCreated {
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
finished_ns: Option<u64>,
},
/// A packet could not create a new session.
SessionRejected {
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
finished_ns: Option<u64>,
},
/// One live session received an update tick.
SessionUpdated {
procedure_id: u32,
hook_id: HookID,
status: SessionStatus,
started_ns: Option<u64>,
finished_ns: Option<u64>,
},
/// One one-shot procedure handler ran.
ProcedureCalled {
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
finished_ns: Option<u64>,
},
/// A packet was emitted by leaf logic before route retry handling.
OutboundQueued { packet: Packet },
/// A queued outbound packet is about to enter endpoint routing.
RouteAttempt { packet: Packet },
/// Endpoint routing accepted a queued outbound packet.
RouteSuccess { packet: Packet },
/// Endpoint routing rejected a queued outbound packet.
RouteFailure {
packet: Packet,
error: EndpointError,
},
}
+24
View File
@@ -0,0 +1,24 @@
use crate::protocol::HookID;
/// Stable identity for one generated session view.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SessionKey {
/// Leaf id that owns the generated session family.
pub leaf_id: u32,
/// Procedure id shared by every packet in the session family.
pub procedure_id: u32,
/// Hook id for the live session instance.
pub hook_id: HookID,
}
/// Stable identity for one generated procedure view.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ProcedureKey {
/// Leaf id that owns the generated procedure family.
pub leaf_id: u32,
/// Procedure id handled by this one-shot procedure family.
pub procedure_id: u32,
}
+12 -404
View File
@@ -1,406 +1,14 @@
use alloc::{collections::BTreeMap, vec::Vec}; //! Caller-owned interface state for UI frontends.
//!
//! Protocol leaves stay headless. When a UI wants packet flow, timing, or render
//! state, it passes an [`InterfaceStore`] through the feature-gated interface path.
use crate::protocol::{EndpointError, HookID, Packet, SessionStatus}; mod event;
mod key;
mod store;
mod view;
/// Caller-owned view and packet-flow store for interface frontends. pub use event::{InterfaceEvent, InterfaceEventKind};
/// pub use key::{ProcedureKey, SessionKey};
/// Generated leaves receive a mutable reference to this store during interface-aware pub use store::InterfaceStore;
/// updates. They decide which leaf/session/procedure keys to touch, but the storage pub use view::{ProcedureView, SessionView, SessionViewStatus};
/// itself stays with the renderer or application shell so protocol state remains
/// headless and reusable.
pub struct InterfaceStore {
next_sequence: u64,
now_ns: Option<u64>,
events: Vec<InterfaceEvent>,
sessions: BTreeMap<SessionKey, SessionView>,
procedures: BTreeMap<ProcedureKey, ProcedureView>,
}
impl InterfaceStore {
/// Creates an empty caller-owned interface store.
pub fn new() -> Self {
Self {
next_sequence: 0,
now_ns: None,
events: Vec::new(),
sessions: BTreeMap::new(),
procedures: BTreeMap::new(),
}
}
/// Sets the timestamp attached to later events.
///
/// The core crate stays `no_std`, so the caller supplies time from its runtime.
/// Passing `None` keeps event ordering without pretending the protocol owns a
/// clock.
pub fn set_now_ns(&mut self, now_ns: Option<u64>) {
self.now_ns = now_ns;
}
/// Returns the timestamp that will be attached to new events.
pub fn now_ns(&self) -> Option<u64> {
self.now_ns
}
/// Returns all recorded events in insertion order.
pub fn events(&self) -> &[InterfaceEvent] {
&self.events
}
/// Returns all session views keyed by leaf, procedure, and hook id.
pub fn session_views(&self) -> &BTreeMap<SessionKey, SessionView> {
&self.sessions
}
/// Returns all procedure views keyed by leaf and procedure id.
pub fn procedure_views(&self) -> &BTreeMap<ProcedureKey, ProcedureView> {
&self.procedures
}
/// Returns or creates the view for a hook-backed session.
pub fn session_view_mut(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
) -> &mut SessionView {
self.sessions
.entry(SessionKey {
leaf_id,
procedure_id,
hook_id,
})
.or_insert_with(SessionView::new)
}
/// Returns or creates the view for a one-shot procedure family.
pub fn procedure_view_mut(&mut self, leaf_id: u32, procedure_id: u32) -> &mut ProcedureView {
self.procedures
.entry(ProcedureKey {
leaf_id,
procedure_id,
})
.or_insert_with(ProcedureView::new)
}
/// Records a packet delivered to a generated leaf.
pub fn record_inbound(&mut self, leaf_id: u32, packet: &Packet) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::Inbound {
packet: packet.clone(),
},
);
self.link_packet_event(leaf_id, packet, index);
}
/// Records that a packet was queued for an existing session inbox.
pub fn record_session_packet_queued(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::SessionPacketQueued {
procedure_id,
hook_id,
},
);
self.session_view_mut(leaf_id, procedure_id, hook_id)
.events
.push(index);
}
/// Records successful creation of a new session state.
pub fn record_session_created(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::SessionCreated {
procedure_id,
hook_id,
started_ns,
finished_ns: self.now_ns,
},
);
let view = self.session_view_mut(leaf_id, procedure_id, hook_id);
view.status = SessionViewStatus::Running;
view.events.push(index);
}
/// Records rejection of a packet that could not create a session.
pub fn record_session_rejected(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::SessionRejected {
procedure_id,
hook_id,
started_ns,
finished_ns: self.now_ns,
},
);
let view = self.session_view_mut(leaf_id, procedure_id, hook_id);
view.status = SessionViewStatus::Rejected;
view.events.push(index);
}
/// Records one session update tick.
pub fn record_session_update(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
status: SessionStatus,
started_ns: Option<u64>,
) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::SessionUpdated {
procedure_id,
hook_id,
status,
started_ns,
finished_ns: self.now_ns,
},
);
let view = self.session_view_mut(leaf_id, procedure_id, hook_id);
view.status = SessionViewStatus::from_session_status(status);
view.events.push(index);
}
/// Records one procedure call.
pub fn record_procedure_call(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::ProcedureCalled {
procedure_id,
hook_id,
started_ns,
finished_ns: self.now_ns,
},
);
self.procedure_view_mut(leaf_id, procedure_id)
.events
.push(index);
}
/// Records a packet emitted by leaf logic before route retry handling.
pub fn record_outbound_queued(&mut self, leaf_id: u32, packet: &Packet) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::OutboundQueued {
packet: packet.clone(),
},
);
self.link_packet_event(leaf_id, packet, index);
}
/// Records a route attempt for a queued outbound packet.
pub fn record_route_attempt(&mut self, leaf_id: u32, packet: &Packet) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::RouteAttempt {
packet: packet.clone(),
},
);
self.link_packet_event(leaf_id, packet, index);
}
/// Records a successful route attempt.
pub fn record_route_success(&mut self, leaf_id: u32, packet: &Packet) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::RouteSuccess {
packet: packet.clone(),
},
);
self.link_packet_event(leaf_id, packet, index);
}
/// Records a failed route attempt without removing the packet from retry state.
pub fn record_route_failure(&mut self, leaf_id: u32, packet: &Packet, error: EndpointError) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::RouteFailure {
packet: packet.clone(),
error,
},
);
self.link_packet_event(leaf_id, packet, index);
}
fn push_event(&mut self, leaf_id: u32, kind: InterfaceEventKind) -> usize {
let sequence = self.next_sequence;
self.next_sequence = self.next_sequence.wrapping_add(1);
let index = self.events.len();
self.events.push(InterfaceEvent {
sequence,
time_ns: self.now_ns,
leaf_id,
kind,
});
index
}
fn link_packet_event(&mut self, leaf_id: u32, packet: &Packet, index: usize) {
self.session_view_mut(leaf_id, packet.procedure_id, packet.hook_id)
.events
.push(index);
}
}
impl Default for InterfaceStore {
fn default() -> Self {
Self::new()
}
}
/// Reborrows an optional interface store for one helper call.
///
/// Generated leaf templates pass the same optional store through several helper
/// calls in one update. This small function keeps that reborrow explicit and avoids
/// every generated call site having to spell out `Option<&mut &mut T>` plumbing.
pub fn borrow_store<'a>(
store: &'a mut Option<&mut InterfaceStore>,
) -> Option<&'a mut InterfaceStore> {
store.as_mut().map(|store| &mut **store)
}
/// Stable identity for one generated session view.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SessionKey {
pub leaf_id: u32,
pub procedure_id: u32,
pub hook_id: HookID,
}
/// Stable identity for one generated procedure view.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ProcedureKey {
pub leaf_id: u32,
pub procedure_id: u32,
}
/// Ordered event stored by [`InterfaceStore`].
pub struct InterfaceEvent {
pub sequence: u64,
pub time_ns: Option<u64>,
pub leaf_id: u32,
pub kind: InterfaceEventKind,
}
/// Interface-visible event emitted by generated helpers.
pub enum InterfaceEventKind {
Inbound {
packet: Packet,
},
SessionPacketQueued {
procedure_id: u32,
hook_id: HookID,
},
SessionCreated {
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
finished_ns: Option<u64>,
},
SessionRejected {
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
finished_ns: Option<u64>,
},
SessionUpdated {
procedure_id: u32,
hook_id: HookID,
status: SessionStatus,
started_ns: Option<u64>,
finished_ns: Option<u64>,
},
ProcedureCalled {
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
finished_ns: Option<u64>,
},
OutboundQueued {
packet: Packet,
},
RouteAttempt {
packet: Packet,
},
RouteSuccess {
packet: Packet,
},
RouteFailure {
packet: Packet,
error: EndpointError,
},
}
/// Caller-owned render view for one hook-backed session.
pub struct SessionView {
pub status: SessionViewStatus,
pub events: Vec<usize>,
}
impl SessionView {
fn new() -> Self {
Self {
status: SessionViewStatus::Pending,
events: Vec::new(),
}
}
}
/// Caller-owned render view for one one-shot procedure family.
pub struct ProcedureView {
pub events: Vec<usize>,
}
impl ProcedureView {
fn new() -> Self {
Self { events: Vec::new() }
}
}
/// Interface lifecycle state for one session view.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionViewStatus {
Pending,
Running,
Closing,
Closed,
Rejected,
}
impl SessionViewStatus {
fn from_session_status(status: SessionStatus) -> Self {
match status {
SessionStatus::Running => Self::Running,
SessionStatus::Closing => Self::Closing,
SessionStatus::Closed => Self::Closed,
}
}
}
+311
View File
@@ -0,0 +1,311 @@
use alloc::{collections::BTreeMap, vec::Vec};
use crate::{
interface::{
InterfaceEvent, InterfaceEventKind, ProcedureKey, ProcedureView, SessionKey, SessionView,
SessionViewStatus,
},
protocol::{EndpointError, HookID, Packet, SessionStatus},
};
/// Caller-owned view and packet-flow store for interface frontends.
///
/// Generated leaves receive a mutable reference to this store during interface-aware
/// updates. They decide which leaf/session/procedure keys to touch, but the storage
/// itself stays with the renderer or application shell so protocol state remains
/// headless and reusable.
pub struct InterfaceStore {
next_sequence: u64,
now_ns: Option<u64>,
events: Vec<InterfaceEvent>,
sessions: BTreeMap<SessionKey, SessionView>,
procedures: BTreeMap<ProcedureKey, ProcedureView>,
}
impl InterfaceStore {
/// Creates an empty caller-owned interface store.
pub fn new() -> Self {
Self {
next_sequence: 0,
now_ns: None,
events: Vec::new(),
sessions: BTreeMap::new(),
procedures: BTreeMap::new(),
}
}
/// Sets the timestamp attached to later events.
///
/// The core crate stays `no_std`, so the caller supplies time from its runtime.
/// Passing `None` keeps event ordering without pretending the protocol owns a
/// clock.
pub fn set_now_ns(&mut self, now_ns: Option<u64>) {
self.now_ns = now_ns;
}
/// Returns the timestamp that will be attached to new events.
pub fn now_ns(&self) -> Option<u64> {
self.now_ns
}
/// Returns all recorded events in insertion order.
pub fn events(&self) -> &[InterfaceEvent] {
&self.events
}
/// Returns all session views keyed by leaf, procedure, and hook id.
pub fn session_views(&self) -> &BTreeMap<SessionKey, SessionView> {
&self.sessions
}
/// Returns all procedure views keyed by leaf and procedure id.
pub fn procedure_views(&self) -> &BTreeMap<ProcedureKey, ProcedureView> {
&self.procedures
}
/// Returns or creates the view for a hook-backed session.
pub fn session_view_mut(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
) -> &mut SessionView {
self.sessions
.entry(SessionKey {
leaf_id,
procedure_id,
hook_id,
})
.or_insert_with(SessionView::new)
}
/// Returns or creates the view for a one-shot procedure family.
pub fn procedure_view_mut(&mut self, leaf_id: u32, procedure_id: u32) -> &mut ProcedureView {
self.procedures
.entry(ProcedureKey {
leaf_id,
procedure_id,
})
.or_insert_with(ProcedureView::new)
}
/// Records a packet delivered to a generated leaf.
pub fn record_inbound(&mut self, leaf_id: u32, packet: &Packet) {
self.push_packet_event(
leaf_id,
packet,
InterfaceEventKind::Inbound {
packet: packet.clone(),
},
);
}
/// Records that a packet was queued for an existing session inbox.
pub fn record_session_packet_queued(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
) {
self.push_session_event(
leaf_id,
procedure_id,
hook_id,
None,
InterfaceEventKind::SessionPacketQueued {
procedure_id,
hook_id,
},
);
}
/// Records successful creation of a new session state.
pub fn record_session_created(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
) {
self.push_session_event(
leaf_id,
procedure_id,
hook_id,
Some(SessionViewStatus::Running),
InterfaceEventKind::SessionCreated {
procedure_id,
hook_id,
started_ns,
finished_ns: self.now_ns,
},
);
}
/// Records rejection of a packet that could not create a session.
pub fn record_session_rejected(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
) {
self.push_session_event(
leaf_id,
procedure_id,
hook_id,
Some(SessionViewStatus::Rejected),
InterfaceEventKind::SessionRejected {
procedure_id,
hook_id,
started_ns,
finished_ns: self.now_ns,
},
);
}
/// Records one session update tick.
pub fn record_session_update(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
status: SessionStatus,
started_ns: Option<u64>,
) {
self.push_session_event(
leaf_id,
procedure_id,
hook_id,
Some(SessionViewStatus::from_session_status(status)),
InterfaceEventKind::SessionUpdated {
procedure_id,
hook_id,
status,
started_ns,
finished_ns: self.now_ns,
},
);
}
/// Records one procedure call.
pub fn record_procedure_call(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
) {
self.push_procedure_event(
leaf_id,
procedure_id,
InterfaceEventKind::ProcedureCalled {
procedure_id,
hook_id,
started_ns,
finished_ns: self.now_ns,
},
);
}
/// Records a packet emitted by leaf logic before route retry handling.
pub fn record_outbound_queued(&mut self, leaf_id: u32, packet: &Packet) {
self.push_packet_event(
leaf_id,
packet,
InterfaceEventKind::OutboundQueued {
packet: packet.clone(),
},
);
}
/// Records a route attempt for a queued outbound packet.
pub fn record_route_attempt(&mut self, leaf_id: u32, packet: &Packet) {
self.push_packet_event(
leaf_id,
packet,
InterfaceEventKind::RouteAttempt {
packet: packet.clone(),
},
);
}
/// Records a successful route attempt.
pub fn record_route_success(&mut self, leaf_id: u32, packet: &Packet) {
self.push_packet_event(
leaf_id,
packet,
InterfaceEventKind::RouteSuccess {
packet: packet.clone(),
},
);
}
/// Records a failed route attempt without removing the packet from retry state.
pub fn record_route_failure(&mut self, leaf_id: u32, packet: &Packet, error: EndpointError) {
self.push_packet_event(
leaf_id,
packet,
InterfaceEventKind::RouteFailure {
packet: packet.clone(),
error,
},
);
}
fn push_packet_event(&mut self, leaf_id: u32, packet: &Packet, kind: InterfaceEventKind) {
let index = self.push_event(leaf_id, kind);
self.link_packet_event(leaf_id, packet, index);
}
fn push_session_event(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
status: Option<SessionViewStatus>,
kind: InterfaceEventKind,
) {
let index = self.push_event(leaf_id, kind);
let view = self.session_view_mut(leaf_id, procedure_id, hook_id);
if let Some(status) = status {
view.status = status;
}
view.events.push(index);
}
fn push_procedure_event(&mut self, leaf_id: u32, procedure_id: u32, kind: InterfaceEventKind) {
let index = self.push_event(leaf_id, kind);
self.procedure_view_mut(leaf_id, procedure_id)
.events
.push(index);
}
fn push_event(&mut self, leaf_id: u32, kind: InterfaceEventKind) -> usize {
let sequence = self.next_sequence;
self.next_sequence = self.next_sequence.wrapping_add(1);
let index = self.events.len();
self.events.push(InterfaceEvent {
sequence,
time_ns: self.now_ns,
leaf_id,
kind,
});
index
}
fn link_packet_event(&mut self, leaf_id: u32, packet: &Packet, index: usize) {
self.session_view_mut(leaf_id, packet.procedure_id, packet.hook_id)
.events
.push(index);
}
}
impl Default for InterfaceStore {
fn default() -> Self {
Self::new()
}
}
+65
View File
@@ -0,0 +1,65 @@
use alloc::vec::Vec;
use crate::protocol::SessionStatus;
/// Caller-owned render view for one hook-backed session.
pub struct SessionView {
/// Latest known lifecycle status.
pub status: SessionViewStatus,
/// Indices into the store's append-only event list.
pub events: Vec<usize>,
}
impl SessionView {
/// Creates an empty pending view.
pub(crate) fn new() -> Self {
Self {
status: SessionViewStatus::Pending,
events: Vec::new(),
}
}
}
/// Caller-owned render view for one one-shot procedure family.
pub struct ProcedureView {
/// Indices into the store's append-only event list.
pub events: Vec<usize>,
}
impl ProcedureView {
/// Creates an empty procedure view.
pub(crate) fn new() -> Self {
Self { events: Vec::new() }
}
}
/// Interface lifecycle state for one session view.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionViewStatus {
/// The view exists because packets referenced it, but no live state exists yet.
Pending,
/// The session is active.
Running,
/// The session is winding down but may still emit packets.
Closing,
/// The session reported that application work is complete.
Closed,
/// The leaf rejected the packet that would have created this session.
Rejected,
}
impl SessionViewStatus {
/// Converts the protocol session status into a renderable status.
pub(crate) fn from_session_status(status: SessionStatus) -> Self {
match status {
SessionStatus::Running => Self::Running,
SessionStatus::Closing => Self::Closing,
SessionStatus::Closed => Self::Closed,
}
}
}
+10 -10
View File
@@ -77,7 +77,7 @@ macro_rules! unshell_leaf {
mut interface: Option<&mut $crate::interface::InterfaceStore>, mut interface: Option<&mut $crate::interface::InterfaceStore>,
) { ) {
let leaf_id = $id; let leaf_id = $id;
self.__unshell_flush_all(endpoint, $crate::interface::borrow_store(&mut interface)); self.__unshell_flush_all(endpoint, &mut interface);
let Some(local_id) = endpoint.path.last().copied() else { let Some(local_id) = endpoint.path.last().copied() else {
return; return;
@@ -94,7 +94,7 @@ macro_rules! unshell_leaf {
self.__unshell_dispatch_packet( self.__unshell_dispatch_packet(
endpoint, endpoint,
packet, packet,
$crate::interface::borrow_store(&mut interface), &mut interface,
); );
} }
@@ -103,18 +103,18 @@ macro_rules! unshell_leaf {
leaf_id, leaf_id,
&mut self.state, &mut self.state,
&mut self.$session_field, &mut self.$session_field,
$crate::interface::borrow_store(&mut interface), &mut interface,
); );
)* )*
self.__unshell_flush_all(endpoint, $crate::interface::borrow_store(&mut interface)); self.__unshell_flush_all(endpoint, &mut interface);
} }
fn __unshell_dispatch_packet( fn __unshell_dispatch_packet(
&mut self, &mut self,
endpoint: &mut $crate::protocol::Endpoint, endpoint: &mut $crate::protocol::Endpoint,
packet: $crate::protocol::Packet, packet: $crate::protocol::Packet,
mut interface: Option<&mut $crate::interface::InterfaceStore>, interface: &mut Option<&mut $crate::interface::InterfaceStore>,
) { ) {
let leaf_id = $id; let leaf_id = $id;
@@ -128,7 +128,7 @@ macro_rules! unshell_leaf {
&mut self.$session_field, &mut self.$session_field,
packet, packet,
&mut self.outbox, &mut self.outbox,
$crate::interface::borrow_store(&mut interface), interface,
); );
return; return;
} }
@@ -145,7 +145,7 @@ macro_rules! unshell_leaf {
endpoint, endpoint,
packet, packet,
&mut self.outbox, &mut self.outbox,
$crate::interface::borrow_store(&mut interface), interface,
); );
return; return;
} }
@@ -158,7 +158,7 @@ macro_rules! unshell_leaf {
fn __unshell_flush_all( fn __unshell_flush_all(
&mut self, &mut self,
endpoint: &mut $crate::protocol::Endpoint, endpoint: &mut $crate::protocol::Endpoint,
mut interface: Option<&mut $crate::interface::InterfaceStore>, interface: &mut Option<&mut $crate::interface::InterfaceStore>,
) { ) {
let leaf_id = $id; let leaf_id = $id;
@@ -166,7 +166,7 @@ macro_rules! unshell_leaf {
endpoint, endpoint,
leaf_id, leaf_id,
&mut self.outbox, &mut self.outbox,
$crate::interface::borrow_store(&mut interface), interface,
); );
$( $(
@@ -174,7 +174,7 @@ macro_rules! unshell_leaf {
endpoint, endpoint,
leaf_id, leaf_id,
&mut self.$session_field, &mut self.$session_field,
$crate::interface::borrow_store(&mut interface), interface,
); );
)* )*
} }
+18 -23
View File
@@ -61,14 +61,14 @@ pub fn dispatch_session<L, S>(
family: &mut SessionFamily<S::State>, family: &mut SessionFamily<S::State>,
packet: Packet, packet: Packet,
outbox: &mut LeafOutbox, outbox: &mut LeafOutbox,
mut interface: Option<&mut InterfaceStore>, interface: &mut Option<&mut InterfaceStore>,
) where ) where
S: Session<L>, S: Session<L>,
{ {
let hook_id = packet.hook_id; let hook_id = packet.hook_id;
let procedure_id = S::PROCEDURE_ID; let procedure_id = S::PROCEDURE_ID;
if let Some(store) = crate::interface::borrow_store(&mut interface) { if let Some(store) = interface.as_mut() {
store.record_inbound(leaf_id, &packet); store.record_inbound(leaf_id, &packet);
} }
@@ -79,7 +79,7 @@ pub fn dispatch_session<L, S>(
{ {
entry.inbox.push_back(packet); entry.inbox.push_back(packet);
if let Some(store) = interface { if let Some(store) = interface.as_mut() {
store.record_session_packet_queued(leaf_id, procedure_id, hook_id); store.record_session_packet_queued(leaf_id, procedure_id, hook_id);
} }
@@ -94,17 +94,17 @@ pub fn dispatch_session<L, S>(
SessionInitResult::Created(state) => { SessionInitResult::Created(state) => {
family.entries.push(SessionEntry::new(hook_id, state)); family.entries.push(SessionEntry::new(hook_id, state));
if let Some(store) = interface { if let Some(store) = interface.as_mut() {
store.record_session_created(leaf_id, procedure_id, hook_id, started_ns); store.record_session_created(leaf_id, procedure_id, hook_id, started_ns);
} }
} }
SessionInitResult::Rejected => { SessionInitResult::Rejected => {
if let Some(store) = interface { if let Some(store) = interface.as_mut() {
store.record_session_rejected(leaf_id, procedure_id, hook_id, started_ns); store.record_session_rejected(leaf_id, procedure_id, hook_id, started_ns);
} }
} }
SessionInitResult::RejectedWith(packet) => { SessionInitResult::RejectedWith(packet) => {
if let Some(store) = interface { if let Some(store) = interface.as_mut() {
store.record_session_rejected(leaf_id, procedure_id, hook_id, started_ns); store.record_session_rejected(leaf_id, procedure_id, hook_id, started_ns);
store.record_outbound_queued(leaf_id, &packet); store.record_outbound_queued(leaf_id, &packet);
} }
@@ -119,7 +119,7 @@ pub fn update_session_family<L, S>(
leaf_id: u32, leaf_id: u32,
leaf: &mut L, leaf: &mut L,
family: &mut SessionFamily<S::State>, family: &mut SessionFamily<S::State>,
mut interface: Option<&mut InterfaceStore>, interface: &mut Option<&mut InterfaceStore>,
) where ) where
S: Session<L>, S: Session<L>,
{ {
@@ -138,7 +138,7 @@ pub fn update_session_family<L, S>(
); );
let status = S::update(leaf, &mut entry.state, &mut entry.inbox, &mut ctx); let status = S::update(leaf, &mut entry.state, &mut entry.inbox, &mut ctx);
if let Some(store) = crate::interface::borrow_store(&mut interface) { if let Some(store) = interface.as_mut() {
store.record_session_update( store.record_session_update(
leaf_id, leaf_id,
S::PROCEDURE_ID, S::PROCEDURE_ID,
@@ -161,13 +161,13 @@ pub fn dispatch_procedure<L, P>(
endpoint: &mut Endpoint, endpoint: &mut Endpoint,
packet: Packet, packet: Packet,
outbox: &mut LeafOutbox, outbox: &mut LeafOutbox,
mut interface: Option<&mut InterfaceStore>, interface: &mut Option<&mut InterfaceStore>,
) where ) where
P: Procedure<L>, P: Procedure<L>,
{ {
let started_ns = interface.as_ref().and_then(|store| store.now_ns()); let started_ns = interface.as_ref().and_then(|store| store.now_ns());
if let Some(store) = crate::interface::borrow_store(&mut interface) { if let Some(store) = interface.as_mut() {
store.record_inbound(leaf_id, &packet); store.record_inbound(leaf_id, &packet);
} }
@@ -179,7 +179,7 @@ pub fn dispatch_procedure<L, P>(
let packets = procedure_out.into_packets(); let packets = procedure_out.into_packets();
if let Some(store) = interface { if let Some(store) = interface.as_mut() {
store.record_procedure_call(leaf_id, P::PROCEDURE_ID, hook_id, started_ns); store.record_procedure_call(leaf_id, P::PROCEDURE_ID, hook_id, started_ns);
for packet in &packets { for packet in &packets {
@@ -195,7 +195,7 @@ pub fn flush_leaf_outbox(
endpoint: &mut Endpoint, endpoint: &mut Endpoint,
leaf_id: u32, leaf_id: u32,
outbox: &mut LeafOutbox, outbox: &mut LeafOutbox,
interface: Option<&mut InterfaceStore>, interface: &mut Option<&mut InterfaceStore>,
) -> bool { ) -> bool {
flush_packet_queue_with_interface(endpoint, leaf_id, &mut outbox.packets, interface) flush_packet_queue_with_interface(endpoint, leaf_id, &mut outbox.packets, interface)
} }
@@ -205,17 +205,12 @@ pub fn flush_session_family<L, S>(
endpoint: &mut Endpoint, endpoint: &mut Endpoint,
leaf_id: u32, leaf_id: u32,
family: &mut SessionFamily<S::State>, family: &mut SessionFamily<S::State>,
mut interface: Option<&mut InterfaceStore>, interface: &mut Option<&mut InterfaceStore>,
) where ) where
S: Session<L>, S: Session<L>,
{ {
for entry in &mut family.entries { for entry in &mut family.entries {
flush_packet_queue_with_interface( flush_packet_queue_with_interface(endpoint, leaf_id, &mut entry.outbox, interface);
endpoint,
leaf_id,
&mut entry.outbox,
crate::interface::borrow_store(&mut interface),
);
} }
family family
@@ -232,23 +227,23 @@ pub fn flush_packet_queue_with_interface(
endpoint: &mut Endpoint, endpoint: &mut Endpoint,
leaf_id: u32, leaf_id: u32,
outbox: &mut PacketQueue, outbox: &mut PacketQueue,
mut interface: Option<&mut InterfaceStore>, interface: &mut Option<&mut InterfaceStore>,
) -> bool { ) -> bool {
while let Some(packet) = outbox.front().cloned() { while let Some(packet) = outbox.front().cloned() {
if let Some(store) = crate::interface::borrow_store(&mut interface) { if let Some(store) = interface.as_mut() {
store.record_route_attempt(leaf_id, &packet); store.record_route_attempt(leaf_id, &packet);
} }
match endpoint.add_outbound(packet.clone()) { match endpoint.add_outbound(packet.clone()) {
Ok(()) => { Ok(()) => {
if let Some(store) = crate::interface::borrow_store(&mut interface) { if let Some(store) = interface.as_mut() {
store.record_route_success(leaf_id, &packet); store.record_route_success(leaf_id, &packet);
} }
outbox.pop_front(); outbox.pop_front();
} }
Err(error) => { Err(error) => {
if let Some(store) = interface { if let Some(store) = interface.as_mut() {
store.record_route_failure(leaf_id, &packet, error); store.record_route_failure(leaf_id, &packet, error);
} }