From ba3a419bb2d4f74da1d725df1f99b7f51ee105a1 Mon Sep 17 00:00:00 2001 From: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Sun, 31 May 2026 12:21:33 -0600 Subject: [PATCH] Split interface store into modules. --- LEAF_MACRO_INTERFACE.md | 891 +++++----------------------------- src/interface/event.rs | 76 +++ src/interface/key.rs | 24 + src/interface/mod.rs | 416 +--------------- src/interface/store.rs | 311 ++++++++++++ src/interface/view.rs | 65 +++ src/protocol/leaf_template.rs | 20 +- src/protocol/runtime.rs | 41 +- 8 files changed, 635 insertions(+), 1209 deletions(-) create mode 100644 src/interface/event.rs create mode 100644 src/interface/key.rs create mode 100644 src/interface/store.rs create mode 100644 src/interface/view.rs diff --git a/LEAF_MACRO_INTERFACE.md b/LEAF_MACRO_INTERFACE.md index 9fa798e..c8f3d87 100644 --- a/LEAF_MACRO_INTERFACE.md +++ b/LEAF_MACRO_INTERFACE.md @@ -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>` or `Vec>` 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(&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, - 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<>::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 + sessions: BTreeMap + procedures: BTreeMap ``` -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 { - /// 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; - - /// 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, -} +leaf.render_ratatui(frame, area, &mut interface); +``` -pub enum SessionInitResult { - 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 { - 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, - 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> -Vec> -``` - -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 { - hook_id: HookID, - state: S, - inbox: PacketQueue, - outbox: PacketQueue, - retry: Option, - 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, - worker: PtyWorker, - saw_stdin_eof: bool, -} - -impl Session for PtySession { - const PROCEDURE_ID: u32 = PROC_PTY; - - type State = PtyState; - - fn init( - leaf: &mut RemotePtyState, - packet: Packet, - ctx: &mut SessionInit, - ) -> SessionInitResult { - 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. diff --git a/src/interface/event.rs b/src/interface/event.rs new file mode 100644 index 0000000..f69aaa3 --- /dev/null +++ b/src/interface/event.rs @@ -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, + + /// 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, + finished_ns: Option, + }, + + /// A packet could not create a new session. + SessionRejected { + procedure_id: u32, + hook_id: HookID, + started_ns: Option, + finished_ns: Option, + }, + + /// One live session received an update tick. + SessionUpdated { + procedure_id: u32, + hook_id: HookID, + status: SessionStatus, + started_ns: Option, + finished_ns: Option, + }, + + /// One one-shot procedure handler ran. + ProcedureCalled { + procedure_id: u32, + hook_id: HookID, + started_ns: Option, + finished_ns: Option, + }, + + /// 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, + }, +} diff --git a/src/interface/key.rs b/src/interface/key.rs new file mode 100644 index 0000000..4d2420c --- /dev/null +++ b/src/interface/key.rs @@ -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, +} diff --git a/src/interface/mod.rs b/src/interface/mod.rs index 29186a1..923e381 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -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, - events: Vec, - sessions: BTreeMap, - procedures: BTreeMap, -} - -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) { - self.now_ns = now_ns; - } - - /// Returns the timestamp that will be attached to new events. - pub fn now_ns(&self) -> Option { - 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 { - &self.sessions - } - - /// Returns all procedure views keyed by leaf and procedure id. - pub fn procedure_views(&self) -> &BTreeMap { - &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, - ) { - 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, - ) { - 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, - ) { - 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, - ) { - 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, - 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, - finished_ns: Option, - }, - SessionRejected { - procedure_id: u32, - hook_id: HookID, - started_ns: Option, - finished_ns: Option, - }, - SessionUpdated { - procedure_id: u32, - hook_id: HookID, - status: SessionStatus, - started_ns: Option, - finished_ns: Option, - }, - ProcedureCalled { - procedure_id: u32, - hook_id: HookID, - started_ns: Option, - finished_ns: Option, - }, - 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, -} - -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, -} - -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}; diff --git a/src/interface/store.rs b/src/interface/store.rs new file mode 100644 index 0000000..0318303 --- /dev/null +++ b/src/interface/store.rs @@ -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, + events: Vec, + sessions: BTreeMap, + procedures: BTreeMap, +} + +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) { + self.now_ns = now_ns; + } + + /// Returns the timestamp that will be attached to new events. + pub fn now_ns(&self) -> Option { + 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 { + &self.sessions + } + + /// Returns all procedure views keyed by leaf and procedure id. + pub fn procedure_views(&self) -> &BTreeMap { + &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, + ) { + 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, + ) { + 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, + ) { + 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, + ) { + 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, + 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() + } +} diff --git a/src/interface/view.rs b/src/interface/view.rs new file mode 100644 index 0000000..b012b87 --- /dev/null +++ b/src/interface/view.rs @@ -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, +} + +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, +} + +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, + } + } +} diff --git a/src/protocol/leaf_template.rs b/src/protocol/leaf_template.rs index 15c9727..cd54fde 100644 --- a/src/protocol/leaf_template.rs +++ b/src/protocol/leaf_template.rs @@ -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, ); )* } diff --git a/src/protocol/runtime.rs b/src/protocol/runtime.rs index 049c613..ca9cb1b 100644 --- a/src/protocol/runtime.rs +++ b/src/protocol/runtime.rs @@ -61,14 +61,14 @@ pub fn dispatch_session( family: &mut SessionFamily, packet: Packet, outbox: &mut LeafOutbox, - mut interface: Option<&mut InterfaceStore>, + interface: &mut Option<&mut InterfaceStore>, ) where S: Session, { 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( { 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( 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( leaf_id: u32, leaf: &mut L, family: &mut SessionFamily, - mut interface: Option<&mut InterfaceStore>, + interface: &mut Option<&mut InterfaceStore>, ) where S: Session, { @@ -138,7 +138,7 @@ pub fn update_session_family( ); 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( endpoint: &mut Endpoint, packet: Packet, outbox: &mut LeafOutbox, - mut interface: Option<&mut InterfaceStore>, + interface: &mut Option<&mut InterfaceStore>, ) where P: Procedure, { 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( 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( endpoint: &mut Endpoint, leaf_id: u32, family: &mut SessionFamily, - mut interface: Option<&mut InterfaceStore>, + interface: &mut Option<&mut InterfaceStore>, ) where S: Session, { 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); }