mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
Work on implementing interface.
This commit is contained in:
@@ -1 +1,406 @@
|
||||
use alloc::{collections::BTreeMap, vec::Vec};
|
||||
|
||||
use crate::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) {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-3
@@ -1,7 +1,7 @@
|
||||
use crate::protocol::Endpoint;
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
use crate::protocol::leaf_meta::LeafMeta;
|
||||
use crate::{interface::InterfaceStore, protocol::leaf_meta::LeafMeta};
|
||||
|
||||
/// Application extension point hosted by an [`Endpoint`].
|
||||
///
|
||||
@@ -18,9 +18,27 @@ pub trait Leaf {
|
||||
/// state, then enqueue outbound packets with [`Endpoint::add_outbound`].
|
||||
fn update(&mut self, _: &mut Endpoint);
|
||||
|
||||
/// Advances the leaf while recording caller-owned interface state.
|
||||
///
|
||||
/// Plain handwritten leaves can ignore the interface store and reuse their normal
|
||||
/// update path. Generated leaves override this to route through the same template
|
||||
/// helpers with packet flow logging enabled.
|
||||
#[cfg(feature = "interface")]
|
||||
fn get_meta(&self) -> LeafMeta;
|
||||
fn update_interface(&mut self, endpoint: &mut Endpoint, _: &mut InterfaceStore) {
|
||||
self.update(endpoint);
|
||||
}
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
fn get_meta(&self) -> LeafMeta {
|
||||
LeafMeta::anonymous()
|
||||
}
|
||||
|
||||
#[cfg(feature = "interface_ratatui")]
|
||||
fn render_ratatui(&mut self, _: &mut ratatui::Frame<'_>, _: ratatui::layout::Rect) {}
|
||||
fn render_ratatui(
|
||||
&mut self,
|
||||
_: &mut ratatui::Frame<'_>,
|
||||
_: ratatui::layout::Rect,
|
||||
_: &mut InterfaceStore,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
/// Human-facing metadata for a leaf implementation.
|
||||
///
|
||||
/// This is intentionally static text plus an allocated author list. It is only used
|
||||
/// by interface frontends and diagnostics, not by hot packet routing.
|
||||
pub struct LeafMeta {
|
||||
pub name: &'static str,
|
||||
pub identifier: &'static str,
|
||||
pub version: &'static str,
|
||||
pub authors: Vec<&'static str>,
|
||||
}
|
||||
|
||||
impl LeafMeta {
|
||||
/// Builds metadata for leaves that have not opted into a richer interface label.
|
||||
pub fn anonymous() -> Self {
|
||||
Self {
|
||||
name: "Unnamed Leaf",
|
||||
identifier: "dev.unshell.unknown",
|
||||
version: "v0",
|
||||
authors: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
/// Declares a generated leaf wrapper using a small template-like syntax.
|
||||
///
|
||||
/// The macro deliberately requires callers to name every generated session field. It
|
||||
/// does not infer identifiers, inspect struct fields, or hide behavior inside a proc
|
||||
/// macro. All real dispatch and retry behavior lives in normal Rust helpers.
|
||||
#[macro_export]
|
||||
macro_rules! unshell_leaf {
|
||||
(
|
||||
$vis:vis leaf $Leaf:ident for $State:ty {
|
||||
id: $id:expr,
|
||||
meta: $meta:expr,
|
||||
sessions { $( $session_field:ident : $Session:ty ),* $(,)? }
|
||||
procedures { $( $procedure_field:ident : $Procedure:ty ),* $(,)? }
|
||||
}
|
||||
) => {
|
||||
$vis struct $Leaf {
|
||||
state: $State,
|
||||
outbox: $crate::protocol::LeafOutbox,
|
||||
$(
|
||||
$session_field: $crate::protocol::SessionFamily<
|
||||
<$Session as $crate::protocol::Session<$State>>::State,
|
||||
>,
|
||||
)*
|
||||
}
|
||||
|
||||
impl $Leaf {
|
||||
/// Creates the generated leaf wrapper around user-owned state.
|
||||
pub fn new(state: $State) -> Self {
|
||||
Self {
|
||||
state,
|
||||
outbox: $crate::protocol::LeafOutbox::new(),
|
||||
$(
|
||||
$session_field: $crate::protocol::SessionFamily::new(),
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns immutable access to the user-owned leaf state.
|
||||
pub fn state(&self) -> &$State {
|
||||
&self.state
|
||||
}
|
||||
|
||||
/// Returns mutable access to the user-owned leaf state.
|
||||
pub fn state_mut(&mut self) -> &mut $State {
|
||||
&mut self.state
|
||||
}
|
||||
|
||||
/// Returns the number of active session entries across all families.
|
||||
pub fn active_session_count(&self) -> usize {
|
||||
0usize $(+ self.$session_field.entries.len())*
|
||||
}
|
||||
|
||||
/// Returns queued packets owned by this generated leaf.
|
||||
pub fn pending_packet_count(&self) -> usize {
|
||||
let mut count = self.outbox.len();
|
||||
$(
|
||||
count += self.$session_field.pending_packet_count();
|
||||
)*
|
||||
count
|
||||
}
|
||||
|
||||
fn __unshell_packet_is_owned(packet: &$crate::protocol::Packet) -> bool {
|
||||
false
|
||||
$(
|
||||
|| packet.procedure_id
|
||||
== <$Session as $crate::protocol::Session<$State>>::PROCEDURE_ID
|
||||
)*
|
||||
$(
|
||||
|| packet.procedure_id
|
||||
== <$Procedure as $crate::protocol::Procedure<$State>>::PROCEDURE_ID
|
||||
)*
|
||||
}
|
||||
|
||||
fn __unshell_update_inner(
|
||||
&mut self,
|
||||
endpoint: &mut $crate::protocol::Endpoint,
|
||||
mut interface: Option<&mut $crate::interface::InterfaceStore>,
|
||||
) {
|
||||
let leaf_id = $id;
|
||||
self.__unshell_flush_all(endpoint, $crate::interface::borrow_store(&mut interface));
|
||||
|
||||
let Some(local_id) = endpoint.path.last().copied() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut packets = $crate::alloc::vec::Vec::new();
|
||||
endpoint.take_inbound_matching(
|
||||
local_id,
|
||||
Self::__unshell_packet_is_owned,
|
||||
|packet| packets.push(packet),
|
||||
);
|
||||
|
||||
for packet in packets {
|
||||
self.__unshell_dispatch_packet(
|
||||
endpoint,
|
||||
packet,
|
||||
$crate::interface::borrow_store(&mut interface),
|
||||
);
|
||||
}
|
||||
|
||||
$(
|
||||
$crate::protocol::update_session_family::<$State, $Session>(
|
||||
leaf_id,
|
||||
&mut self.state,
|
||||
&mut self.$session_field,
|
||||
$crate::interface::borrow_store(&mut interface),
|
||||
);
|
||||
)*
|
||||
|
||||
self.__unshell_flush_all(endpoint, $crate::interface::borrow_store(&mut interface));
|
||||
}
|
||||
|
||||
fn __unshell_dispatch_packet(
|
||||
&mut self,
|
||||
endpoint: &mut $crate::protocol::Endpoint,
|
||||
packet: $crate::protocol::Packet,
|
||||
mut interface: Option<&mut $crate::interface::InterfaceStore>,
|
||||
) {
|
||||
let leaf_id = $id;
|
||||
|
||||
$(
|
||||
if packet.procedure_id
|
||||
== <$Session as $crate::protocol::Session<$State>>::PROCEDURE_ID
|
||||
{
|
||||
$crate::protocol::dispatch_session::<$State, $Session>(
|
||||
leaf_id,
|
||||
&mut self.state,
|
||||
&mut self.$session_field,
|
||||
packet,
|
||||
&mut self.outbox,
|
||||
$crate::interface::borrow_store(&mut interface),
|
||||
);
|
||||
return;
|
||||
}
|
||||
)*
|
||||
|
||||
$(
|
||||
if packet.procedure_id
|
||||
== <$Procedure as $crate::protocol::Procedure<$State>>::PROCEDURE_ID
|
||||
{
|
||||
let _ = stringify!($procedure_field);
|
||||
$crate::protocol::dispatch_procedure::<$State, $Procedure>(
|
||||
leaf_id,
|
||||
&mut self.state,
|
||||
endpoint,
|
||||
packet,
|
||||
&mut self.outbox,
|
||||
$crate::interface::borrow_store(&mut interface),
|
||||
);
|
||||
return;
|
||||
}
|
||||
)*
|
||||
|
||||
let _ = endpoint;
|
||||
let _ = packet;
|
||||
}
|
||||
|
||||
fn __unshell_flush_all(
|
||||
&mut self,
|
||||
endpoint: &mut $crate::protocol::Endpoint,
|
||||
mut interface: Option<&mut $crate::interface::InterfaceStore>,
|
||||
) {
|
||||
let leaf_id = $id;
|
||||
|
||||
$crate::protocol::flush_leaf_outbox(
|
||||
endpoint,
|
||||
leaf_id,
|
||||
&mut self.outbox,
|
||||
$crate::interface::borrow_store(&mut interface),
|
||||
);
|
||||
|
||||
$(
|
||||
$crate::protocol::flush_session_family::<$State, $Session>(
|
||||
endpoint,
|
||||
leaf_id,
|
||||
&mut self.$session_field,
|
||||
$crate::interface::borrow_store(&mut interface),
|
||||
);
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
impl $crate::protocol::Leaf for $Leaf {
|
||||
fn get_id(&self) -> u32 {
|
||||
$id
|
||||
}
|
||||
|
||||
fn update(&mut self, endpoint: &mut $crate::protocol::Endpoint) {
|
||||
self.__unshell_update_inner(endpoint, None);
|
||||
}
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
fn update_interface(
|
||||
&mut self,
|
||||
endpoint: &mut $crate::protocol::Endpoint,
|
||||
interface: &mut $crate::interface::InterfaceStore,
|
||||
) {
|
||||
self.__unshell_update_inner(endpoint, Some(interface));
|
||||
}
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
fn get_meta(&self) -> $crate::protocol::LeafMeta {
|
||||
$meta
|
||||
}
|
||||
|
||||
#[cfg(feature = "interface_ratatui")]
|
||||
fn render_ratatui(
|
||||
&mut self,
|
||||
frame: &mut $crate::protocol::ratatui::Frame<'_>,
|
||||
area: $crate::protocol::ratatui::layout::Rect,
|
||||
interface: &mut $crate::interface::InterfaceStore,
|
||||
) {
|
||||
let leaf_id = $id;
|
||||
|
||||
$(
|
||||
for entry in &mut self.$session_field.entries {
|
||||
let view = interface.session_view_mut(
|
||||
leaf_id,
|
||||
<$Session as $crate::protocol::Session<$State>>::PROCEDURE_ID,
|
||||
entry.hook_id,
|
||||
);
|
||||
<$Session as $crate::protocol::Session<$State>>::render_ratatui(
|
||||
&self.state,
|
||||
&entry.state,
|
||||
view,
|
||||
frame,
|
||||
area,
|
||||
);
|
||||
}
|
||||
)*
|
||||
|
||||
$(
|
||||
{
|
||||
let _ = stringify!($procedure_field);
|
||||
let view = interface.procedure_view_mut(
|
||||
leaf_id,
|
||||
<$Procedure as $crate::protocol::Procedure<$State>>::PROCEDURE_ID,
|
||||
);
|
||||
<$Procedure as $crate::protocol::Procedure<$State>>::render_ratatui(
|
||||
&self.state,
|
||||
view,
|
||||
frame,
|
||||
area,
|
||||
);
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
+7
-1
@@ -2,18 +2,24 @@ mod endpoint;
|
||||
mod error;
|
||||
mod leaf;
|
||||
mod leaf_meta;
|
||||
mod leaf_template;
|
||||
mod packet;
|
||||
mod procedure;
|
||||
mod runtime;
|
||||
mod session;
|
||||
|
||||
pub use crate::unshell_leaf;
|
||||
pub use endpoint::{Endpoint, HookID};
|
||||
pub use error::*;
|
||||
pub use leaf::Leaf;
|
||||
pub use leaf_meta::LeafMeta;
|
||||
pub use packet::Packet;
|
||||
pub use procedure::*;
|
||||
pub use runtime::*;
|
||||
pub use session::*;
|
||||
pub use unshell_macros::unshell_leaf;
|
||||
|
||||
#[cfg(feature = "interface_ratatui")]
|
||||
pub use ratatui;
|
||||
|
||||
// Various named types used for brevity
|
||||
use alloc::{
|
||||
|
||||
@@ -2,6 +2,9 @@ use alloc::vec::Vec;
|
||||
|
||||
use crate::protocol::{Endpoint, HookID, Packet, PacketQueue};
|
||||
|
||||
#[cfg(feature = "interface_ratatui")]
|
||||
use crate::interface::ProcedureView;
|
||||
|
||||
/// Contract implemented by one generated one-packet procedure handler.
|
||||
///
|
||||
/// Procedures are for stateless or short-lived operations such as ping, capabilities,
|
||||
@@ -13,6 +16,15 @@ pub trait Procedure<L> {
|
||||
|
||||
/// Handles one packet and optionally queues response packets in `out`.
|
||||
fn handle(leaf: &mut L, endpoint: &mut Endpoint, packet: Packet, out: &mut ProcedureOut);
|
||||
|
||||
#[cfg(feature = "interface_ratatui")]
|
||||
fn render_ratatui(
|
||||
_: &L,
|
||||
_: &mut ProcedureView,
|
||||
_: &mut ratatui::Frame<'_>,
|
||||
_: ratatui::layout::Rect,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Output accumulator passed to [`Procedure::handle`].
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
use crate::{
|
||||
interface::InterfaceStore,
|
||||
protocol::{
|
||||
Endpoint, Packet, PacketQueue, Procedure, ProcedureOut, Session, SessionCtx, SessionEntry,
|
||||
SessionFamily, SessionInit, SessionInitResult, SessionStatus,
|
||||
},
|
||||
};
|
||||
|
||||
/// Retry queue shared by generated leaves.
|
||||
///
|
||||
/// Sessions already own per-hook outboxes. This leaf-level queue is for rejected
|
||||
/// session initialization responses and one-shot procedures, both of which need the
|
||||
/// same retry semantics as session output without becoming separate framework types.
|
||||
pub struct LeafOutbox {
|
||||
packets: PacketQueue,
|
||||
}
|
||||
|
||||
impl LeafOutbox {
|
||||
/// Creates an empty leaf-level outbox.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
packets: PacketQueue::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds one packet to the retry queue.
|
||||
pub fn push(&mut self, packet: Packet) {
|
||||
self.packets.push_back(packet);
|
||||
}
|
||||
|
||||
/// Adds all packets from `packets` in FIFO order.
|
||||
pub fn extend(&mut self, packets: PacketQueue) {
|
||||
self.packets.extend(packets);
|
||||
}
|
||||
|
||||
/// Returns the number of queued packets.
|
||||
pub fn len(&self) -> usize {
|
||||
self.packets.len()
|
||||
}
|
||||
|
||||
/// Returns true when the queue has no pending packets.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.packets.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LeafOutbox {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatches one packet into a generated session family.
|
||||
///
|
||||
/// The macro picks `S` and the family field. This helper owns the boring details:
|
||||
/// find the hook, initialize missing sessions, queue rejected responses, and update
|
||||
/// interface state when a caller supplied one.
|
||||
pub fn dispatch_session<L, S>(
|
||||
leaf_id: u32,
|
||||
leaf: &mut L,
|
||||
family: &mut SessionFamily<S::State>,
|
||||
packet: Packet,
|
||||
outbox: &mut LeafOutbox,
|
||||
mut interface: 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) {
|
||||
store.record_inbound(leaf_id, &packet);
|
||||
}
|
||||
|
||||
if let Some(entry) = family
|
||||
.entries
|
||||
.iter_mut()
|
||||
.find(|entry| entry.hook_id == hook_id)
|
||||
{
|
||||
entry.inbox.push_back(packet);
|
||||
|
||||
if let Some(store) = interface {
|
||||
store.record_session_packet_queued(leaf_id, procedure_id, hook_id);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let started_ns = interface.as_ref().and_then(|store| store.now_ns());
|
||||
let packet_path = packet.path.clone();
|
||||
let mut init = SessionInit::new(hook_id, packet_path);
|
||||
|
||||
match S::init(leaf, packet, &mut init) {
|
||||
SessionInitResult::Created(state) => {
|
||||
family.entries.push(SessionEntry::new(hook_id, state));
|
||||
|
||||
if let Some(store) = interface {
|
||||
store.record_session_created(leaf_id, procedure_id, hook_id, started_ns);
|
||||
}
|
||||
}
|
||||
SessionInitResult::Rejected => {
|
||||
if let Some(store) = interface {
|
||||
store.record_session_rejected(leaf_id, procedure_id, hook_id, started_ns);
|
||||
}
|
||||
}
|
||||
SessionInitResult::RejectedWith(packet) => {
|
||||
if let Some(store) = interface {
|
||||
store.record_session_rejected(leaf_id, procedure_id, hook_id, started_ns);
|
||||
store.record_outbound_queued(leaf_id, &packet);
|
||||
}
|
||||
|
||||
outbox.push(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates every live session in one generated session family.
|
||||
pub fn update_session_family<L, S>(
|
||||
leaf_id: u32,
|
||||
leaf: &mut L,
|
||||
family: &mut SessionFamily<S::State>,
|
||||
mut interface: Option<&mut InterfaceStore>,
|
||||
) where
|
||||
S: Session<L>,
|
||||
{
|
||||
for entry in &mut family.entries {
|
||||
if entry.closed {
|
||||
continue;
|
||||
}
|
||||
|
||||
let started_ns = interface.as_ref().and_then(|store| store.now_ns());
|
||||
let reply_path = S::reply_path(&entry.state).to_vec();
|
||||
let mut ctx = SessionCtx::new(
|
||||
entry.hook_id,
|
||||
reply_path,
|
||||
S::PROCEDURE_ID,
|
||||
&mut entry.outbox,
|
||||
);
|
||||
let status = S::update(leaf, &mut entry.state, &mut entry.inbox, &mut ctx);
|
||||
|
||||
if let Some(store) = crate::interface::borrow_store(&mut interface) {
|
||||
store.record_session_update(
|
||||
leaf_id,
|
||||
S::PROCEDURE_ID,
|
||||
entry.hook_id,
|
||||
status,
|
||||
started_ns,
|
||||
);
|
||||
}
|
||||
|
||||
if matches!(status, SessionStatus::Closed) {
|
||||
entry.closed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatches one packet into a generated one-shot procedure.
|
||||
pub fn dispatch_procedure<L, P>(
|
||||
leaf_id: u32,
|
||||
leaf: &mut L,
|
||||
endpoint: &mut Endpoint,
|
||||
packet: Packet,
|
||||
outbox: &mut LeafOutbox,
|
||||
mut interface: 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) {
|
||||
store.record_inbound(leaf_id, &packet);
|
||||
}
|
||||
|
||||
let hook_id = packet.hook_id;
|
||||
let mut procedure_out =
|
||||
ProcedureOut::new(hook_id, parent_reply_path(endpoint), P::PROCEDURE_ID);
|
||||
|
||||
P::handle(leaf, endpoint, packet, &mut procedure_out);
|
||||
|
||||
let packets = procedure_out.into_packets();
|
||||
|
||||
if let Some(store) = interface {
|
||||
store.record_procedure_call(leaf_id, P::PROCEDURE_ID, hook_id, started_ns);
|
||||
|
||||
for packet in &packets {
|
||||
store.record_outbound_queued(leaf_id, packet);
|
||||
}
|
||||
}
|
||||
|
||||
outbox.extend(packets);
|
||||
}
|
||||
|
||||
/// Flushes a generated leaf-level outbox through endpoint routing.
|
||||
pub fn flush_leaf_outbox(
|
||||
endpoint: &mut Endpoint,
|
||||
leaf_id: u32,
|
||||
outbox: &mut LeafOutbox,
|
||||
interface: Option<&mut InterfaceStore>,
|
||||
) -> bool {
|
||||
flush_packet_queue_with_interface(endpoint, leaf_id, &mut outbox.packets, interface)
|
||||
}
|
||||
|
||||
/// Flushes and retains one generated session family.
|
||||
pub fn flush_session_family<L, S>(
|
||||
endpoint: &mut Endpoint,
|
||||
leaf_id: u32,
|
||||
family: &mut SessionFamily<S::State>,
|
||||
mut interface: 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),
|
||||
);
|
||||
}
|
||||
|
||||
family
|
||||
.entries
|
||||
.retain(|entry| !entry.closed || !entry.outbox.is_empty());
|
||||
}
|
||||
|
||||
/// Flushes a retry queue through [`Endpoint::add_outbound`].
|
||||
///
|
||||
/// This is the interface-aware version of [`crate::protocol::flush_packet_queue`]. It
|
||||
/// logs route attempts before trying them, then logs either success or the route error
|
||||
/// without dropping the packet on failure.
|
||||
pub fn flush_packet_queue_with_interface(
|
||||
endpoint: &mut Endpoint,
|
||||
leaf_id: u32,
|
||||
outbox: &mut PacketQueue,
|
||||
mut interface: Option<&mut InterfaceStore>,
|
||||
) -> bool {
|
||||
while let Some(packet) = outbox.front().cloned() {
|
||||
if let Some(store) = crate::interface::borrow_store(&mut interface) {
|
||||
store.record_route_attempt(leaf_id, &packet);
|
||||
}
|
||||
|
||||
match endpoint.add_outbound(packet.clone()) {
|
||||
Ok(()) => {
|
||||
if let Some(store) = crate::interface::borrow_store(&mut interface) {
|
||||
store.record_route_success(leaf_id, &packet);
|
||||
}
|
||||
|
||||
outbox.pop_front();
|
||||
}
|
||||
Err(error) => {
|
||||
if let Some(store) = interface {
|
||||
store.record_route_failure(leaf_id, &packet, error);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns the path used by generated procedure responses.
|
||||
fn parent_reply_path(endpoint: &Endpoint) -> alloc::vec::Vec<u32> {
|
||||
if endpoint.path.len() > 1 {
|
||||
endpoint.path[..endpoint.path.len() - 1].to_vec()
|
||||
} else {
|
||||
endpoint.path.clone()
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@ use alloc::vec::Vec;
|
||||
|
||||
use crate::protocol::{Endpoint, HookID, Packet, PacketQueue};
|
||||
|
||||
#[cfg(feature = "interface_ratatui")]
|
||||
use crate::interface::SessionView;
|
||||
|
||||
/// Contract implemented by one hook-backed generated session family.
|
||||
///
|
||||
/// A session family maps one outer `procedure_id` to many live hook instances. The
|
||||
@@ -74,6 +77,16 @@ pub trait Session<L> {
|
||||
incoming: &mut PacketQueue,
|
||||
ctx: &mut SessionCtx<'_>,
|
||||
) -> SessionStatus;
|
||||
|
||||
#[cfg(feature = "interface_ratatui")]
|
||||
fn render_ratatui(
|
||||
_: &L,
|
||||
_: &Self::State,
|
||||
_: &mut SessionView,
|
||||
_: &mut ratatui::Frame<'_>,
|
||||
_: ratatui::layout::Rect,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Context passed to [`Session::init`].
|
||||
@@ -249,6 +262,42 @@ pub struct SessionEntry<S> {
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
/// Generated storage for one session family.
|
||||
///
|
||||
/// The macro only names this field and picks the concrete `Session` type. All update,
|
||||
/// retry, and cleanup behavior lives in normal Rust helpers so the template stays
|
||||
/// small and readable.
|
||||
pub struct SessionFamily<S> {
|
||||
/// Active hook-backed sessions for this family.
|
||||
pub entries: Vec<SessionEntry<S>>,
|
||||
}
|
||||
|
||||
impl<S> SessionFamily<S> {
|
||||
/// Creates an empty session family.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Counts packets retained by this family for retry or future session work.
|
||||
pub fn pending_packet_count(&self) -> usize {
|
||||
let mut count = 0usize;
|
||||
|
||||
for entry in &self.entries {
|
||||
count += entry.inbox.len() + entry.outbox.len();
|
||||
}
|
||||
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Default for SessionFamily<S> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> SessionEntry<S> {
|
||||
/// Creates one active session entry for `hook_id`.
|
||||
pub fn new(hook_id: HookID, state: S) -> Self {
|
||||
|
||||
Reference in New Issue
Block a user