mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
Split interface store into modules.
This commit is contained in:
+119
-772
@@ -1,810 +1,157 @@
|
||||
# Macro-Generated Leaf Interface Design
|
||||
# Template Leaf Interface Design
|
||||
|
||||
**Status:** Draft
|
||||
**Last updated:** 2026-05-28
|
||||
**Primary use case:** Remote PTY sessions over hook-backed UnShell packets
|
||||
**Status:** Implemented draft
|
||||
**Last updated:** 2026-05-31
|
||||
**Primary use case:** Small generated leaf wrappers without proc-macro machinery
|
||||
|
||||
## Summary
|
||||
|
||||
This document proposes a generated leaf interface for UnShell. The goal is to make
|
||||
stateful leaves, such as a remote PTY leaf, easy to write without forcing every leaf
|
||||
author to hand-code packet draining, procedure dispatch, session lookup, hook
|
||||
lifetime handling, and retry-safe final-frame cleanup.
|
||||
Leaf generation now uses a declarative `unshell_leaf!` template instead of the old
|
||||
`#[unshell_leaf]` proc macro. The goal is to make generated code obvious, closer to
|
||||
an HTML template than an AST transformation.
|
||||
|
||||
The user writes the application logic:
|
||||
The macro only fills slots:
|
||||
|
||||
- the leaf state struct
|
||||
- one or more session types for long-running hook-backed conversations
|
||||
- one or more procedure types for one-packet operations
|
||||
- payload encoding and decoding
|
||||
- OS-specific behavior, such as spawning or polling a PTY
|
||||
- wrapper name
|
||||
- user state type
|
||||
- leaf id
|
||||
- interface metadata
|
||||
- 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
|
||||
- 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:
|
||||
## User Shape
|
||||
|
||||
```rust
|
||||
pub trait Leaf {
|
||||
fn get_id(&self) -> u32;
|
||||
|
||||
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,
|
||||
pub struct FakePtyState {
|
||||
pub active_count: usize,
|
||||
pub total_opened: u64,
|
||||
}
|
||||
|
||||
impl RemotePtyLeaf {
|
||||
pub fn new(state: RemotePtyState) -> Self { ... }
|
||||
}
|
||||
|
||||
impl Leaf for RemotePtyLeaf {
|
||||
fn get_id(&self) -> u32 {
|
||||
LEAF_REMOTE_PTY
|
||||
}
|
||||
|
||||
fn update(&mut self, endpoint: &mut Endpoint) {
|
||||
... generated dispatch ...
|
||||
unshell_leaf! {
|
||||
pub leaf FakePtyLeaf for FakePtyState {
|
||||
id: LEAF_FAKE_PTY,
|
||||
meta: unshell::protocol::LeafMeta {
|
||||
name: "Fake PTY Leaf",
|
||||
identifier: "dev.unshell.v1.pty",
|
||||
version: "v0",
|
||||
authors: unshell::alloc::vec!["ASTATIN3"],
|
||||
},
|
||||
sessions {
|
||||
pty: PtySession,
|
||||
}
|
||||
procedures {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The leaf wrapper is the object stored in `Endpoint::leaves`. The state struct stays
|
||||
small and owned by the user.
|
||||
The field name before each session type is explicit. The macro does not invent a
|
||||
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
|
||||
Endpoint inbound queue
|
||||
|
|
||||
v
|
||||
+-------------------------------+
|
||||
| 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
|
||||
InterfaceStore
|
||||
events: Vec<InterfaceEvent>
|
||||
sessions: BTreeMap<SessionKey, SessionView>
|
||||
procedures: BTreeMap<ProcedureKey, ProcedureView>
|
||||
```
|
||||
|
||||
Procedures and sessions can both mutate `RemotePtyState`. They cannot directly
|
||||
borrow or mutate each other. Communication between them must happen through leaf
|
||||
state or through packets.
|
||||
Generated leaves receive an optional mutable store during `update_interface`. The
|
||||
helpers create and update the appropriate session/procedure views when packets are
|
||||
dispatched, sessions update, and outbound routes succeed or fail.
|
||||
|
||||
## Sessions
|
||||
|
||||
A session is a long-running hook-backed conversation. PTY is the main example.
|
||||
Time remains caller-supplied:
|
||||
|
||||
```rust
|
||||
pub trait Session<L> {
|
||||
/// All packets for this session type use this outer procedure id.
|
||||
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;
|
||||
}
|
||||
interface.set_now_ns(Some(now_ns));
|
||||
leaf.update_interface(endpoint, &mut interface);
|
||||
```
|
||||
|
||||
`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
|
||||
pub struct SessionInit {
|
||||
hook_id: HookID,
|
||||
packet_path: Vec<u32>,
|
||||
}
|
||||
leaf.render_ratatui(frame, area, &mut interface);
|
||||
```
|
||||
|
||||
pub enum SessionInitResult<S> {
|
||||
Created(S),
|
||||
Rejected,
|
||||
RejectedWith(Packet),
|
||||
Session rendering is an associated function because session families are type-level
|
||||
contracts, not stored objects:
|
||||
|
||||
```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
|
||||
protocol-level failure response, such as "too many PTY sessions". The generated leaf
|
||||
still owns routing and retry for that packet.
|
||||
Procedure rendering is also associated and renders from leaf state plus the caller
|
||||
owned procedure view.
|
||||
|
||||
The PTY `Open` payload should include the caller reply path because `Packet` does
|
||||
not currently carry source path.
|
||||
## Why This Replaced The Proc Macro
|
||||
|
||||
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
|
||||
Open payload:
|
||||
opcode
|
||||
reply_path_len
|
||||
reply_path segments
|
||||
rows
|
||||
cols
|
||||
command/env/options
|
||||
macro template -> named fields and loops
|
||||
runtime helpers -> behavior
|
||||
caller InterfaceStore -> UI/log state
|
||||
```
|
||||
|
||||
The session stores that reply path and uses it for upward output packets.
|
||||
|
||||
### 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.
|
||||
That is the whole game.
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
///
|
||||
/// 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) {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use event::{InterfaceEvent, InterfaceEventKind};
|
||||
pub use key::{ProcedureKey, SessionKey};
|
||||
pub use store::InterfaceStore;
|
||||
pub use view::{ProcedureView, SessionView, SessionViewStatus};
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ macro_rules! unshell_leaf {
|
||||
mut interface: Option<&mut $crate::interface::InterfaceStore>,
|
||||
) {
|
||||
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 {
|
||||
return;
|
||||
@@ -94,7 +94,7 @@ macro_rules! unshell_leaf {
|
||||
self.__unshell_dispatch_packet(
|
||||
endpoint,
|
||||
packet,
|
||||
$crate::interface::borrow_store(&mut interface),
|
||||
&mut interface,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,18 +103,18 @@ macro_rules! unshell_leaf {
|
||||
leaf_id,
|
||||
&mut self.state,
|
||||
&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(
|
||||
&mut self,
|
||||
endpoint: &mut $crate::protocol::Endpoint,
|
||||
packet: $crate::protocol::Packet,
|
||||
mut interface: Option<&mut $crate::interface::InterfaceStore>,
|
||||
interface: &mut Option<&mut $crate::interface::InterfaceStore>,
|
||||
) {
|
||||
let leaf_id = $id;
|
||||
|
||||
@@ -128,7 +128,7 @@ macro_rules! unshell_leaf {
|
||||
&mut self.$session_field,
|
||||
packet,
|
||||
&mut self.outbox,
|
||||
$crate::interface::borrow_store(&mut interface),
|
||||
interface,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -145,7 +145,7 @@ macro_rules! unshell_leaf {
|
||||
endpoint,
|
||||
packet,
|
||||
&mut self.outbox,
|
||||
$crate::interface::borrow_store(&mut interface),
|
||||
interface,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -158,7 +158,7 @@ macro_rules! unshell_leaf {
|
||||
fn __unshell_flush_all(
|
||||
&mut self,
|
||||
endpoint: &mut $crate::protocol::Endpoint,
|
||||
mut interface: Option<&mut $crate::interface::InterfaceStore>,
|
||||
interface: &mut Option<&mut $crate::interface::InterfaceStore>,
|
||||
) {
|
||||
let leaf_id = $id;
|
||||
|
||||
@@ -166,7 +166,7 @@ macro_rules! unshell_leaf {
|
||||
endpoint,
|
||||
leaf_id,
|
||||
&mut self.outbox,
|
||||
$crate::interface::borrow_store(&mut interface),
|
||||
interface,
|
||||
);
|
||||
|
||||
$(
|
||||
@@ -174,7 +174,7 @@ macro_rules! unshell_leaf {
|
||||
endpoint,
|
||||
leaf_id,
|
||||
&mut self.$session_field,
|
||||
$crate::interface::borrow_store(&mut interface),
|
||||
interface,
|
||||
);
|
||||
)*
|
||||
}
|
||||
|
||||
+18
-23
@@ -61,14 +61,14 @@ pub fn dispatch_session<L, S>(
|
||||
family: &mut SessionFamily<S::State>,
|
||||
packet: Packet,
|
||||
outbox: &mut LeafOutbox,
|
||||
mut interface: Option<&mut InterfaceStore>,
|
||||
interface: &mut Option<&mut InterfaceStore>,
|
||||
) where
|
||||
S: Session<L>,
|
||||
{
|
||||
let hook_id = packet.hook_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);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ pub fn dispatch_session<L, S>(
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -94,17 +94,17 @@ pub fn dispatch_session<L, S>(
|
||||
SessionInitResult::Created(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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
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_outbound_queued(leaf_id, &packet);
|
||||
}
|
||||
@@ -119,7 +119,7 @@ pub fn update_session_family<L, S>(
|
||||
leaf_id: u32,
|
||||
leaf: &mut L,
|
||||
family: &mut SessionFamily<S::State>,
|
||||
mut interface: Option<&mut InterfaceStore>,
|
||||
interface: &mut Option<&mut InterfaceStore>,
|
||||
) where
|
||||
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);
|
||||
|
||||
if let Some(store) = crate::interface::borrow_store(&mut interface) {
|
||||
if let Some(store) = interface.as_mut() {
|
||||
store.record_session_update(
|
||||
leaf_id,
|
||||
S::PROCEDURE_ID,
|
||||
@@ -161,13 +161,13 @@ pub fn dispatch_procedure<L, P>(
|
||||
endpoint: &mut Endpoint,
|
||||
packet: Packet,
|
||||
outbox: &mut LeafOutbox,
|
||||
mut interface: Option<&mut InterfaceStore>,
|
||||
interface: &mut Option<&mut InterfaceStore>,
|
||||
) where
|
||||
P: Procedure<L>,
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ pub fn dispatch_procedure<L, P>(
|
||||
|
||||
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);
|
||||
|
||||
for packet in &packets {
|
||||
@@ -195,7 +195,7 @@ pub fn flush_leaf_outbox(
|
||||
endpoint: &mut Endpoint,
|
||||
leaf_id: u32,
|
||||
outbox: &mut LeafOutbox,
|
||||
interface: Option<&mut InterfaceStore>,
|
||||
interface: &mut Option<&mut InterfaceStore>,
|
||||
) -> bool {
|
||||
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,
|
||||
leaf_id: u32,
|
||||
family: &mut SessionFamily<S::State>,
|
||||
mut interface: Option<&mut InterfaceStore>,
|
||||
interface: &mut Option<&mut InterfaceStore>,
|
||||
) where
|
||||
S: Session<L>,
|
||||
{
|
||||
for entry in &mut family.entries {
|
||||
flush_packet_queue_with_interface(
|
||||
endpoint,
|
||||
leaf_id,
|
||||
&mut entry.outbox,
|
||||
crate::interface::borrow_store(&mut interface),
|
||||
);
|
||||
flush_packet_queue_with_interface(endpoint, leaf_id, &mut entry.outbox, interface);
|
||||
}
|
||||
|
||||
family
|
||||
@@ -232,23 +227,23 @@ pub fn flush_packet_queue_with_interface(
|
||||
endpoint: &mut Endpoint,
|
||||
leaf_id: u32,
|
||||
outbox: &mut PacketQueue,
|
||||
mut interface: Option<&mut InterfaceStore>,
|
||||
interface: &mut Option<&mut InterfaceStore>,
|
||||
) -> bool {
|
||||
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);
|
||||
}
|
||||
|
||||
match endpoint.add_outbound(packet.clone()) {
|
||||
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);
|
||||
}
|
||||
|
||||
outbox.pop_front();
|
||||
}
|
||||
Err(error) => {
|
||||
if let Some(store) = interface {
|
||||
if let Some(store) = interface.as_mut() {
|
||||
store.record_route_failure(leaf_id, &packet, error);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user