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:
Generated
-17
@@ -1829,23 +1829,6 @@ dependencies = [
|
|||||||
"rkyv",
|
"rkyv",
|
||||||
"static_init",
|
"static_init",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"unshell-macros",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unshell-macros"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"unshell-macros-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unshell-macros-core"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.117",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ cargo-features = ["trim-paths", "panic-immediate-abort"]
|
|||||||
members = [
|
members = [
|
||||||
"ush-obfuscate",
|
"ush-obfuscate",
|
||||||
"base62",
|
"base62",
|
||||||
"unshell-macros-core",
|
|
||||||
"unshell-macros",
|
|
||||||
|
|
||||||
"unshell-leaves/leaf-pty",
|
"unshell-leaves/leaf-pty",
|
||||||
]
|
]
|
||||||
@@ -24,9 +22,6 @@ rkyv = "0.8.16"
|
|||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
chrono = "0.4.44"
|
chrono = "0.4.44"
|
||||||
static_init = "1.0.4"
|
static_init = "1.0.4"
|
||||||
syn = "2.0.117"
|
|
||||||
quote = "1.0.45"
|
|
||||||
proc-macro2 = "1.0.106"
|
|
||||||
portable-pty = "0.9.0"
|
portable-pty = "0.9.0"
|
||||||
crossbeam-channel = "0.5.15"
|
crossbeam-channel = "0.5.15"
|
||||||
|
|
||||||
@@ -34,8 +29,6 @@ ratatui = "0.30.0"
|
|||||||
|
|
||||||
unshell = { path = "." }
|
unshell = { path = "." }
|
||||||
# unshell-protocol = { path = "./unshell-protocol" }
|
# unshell-protocol = { path = "./unshell-protocol" }
|
||||||
unshell-macros-core = { path = "./unshell-macros-core" }
|
|
||||||
unshell-macros = { path = "./unshell-macros" }
|
|
||||||
|
|
||||||
# ush-obfuscate = { path = "./ush-obfuscate" }
|
# ush-obfuscate = { path = "./ush-obfuscate" }
|
||||||
# base62 = { path = "./base62" }
|
# base62 = { path = "./base62" }
|
||||||
@@ -67,8 +60,6 @@ static_init = { workspace = true }
|
|||||||
|
|
||||||
ratatui = { workspace = true, optional = true }
|
ratatui = { workspace = true, optional = true }
|
||||||
|
|
||||||
unshell-macros = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
crossbeam-channel.workspace = true
|
crossbeam-channel.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
use crate::protocol::Endpoint;
|
||||||
|
|
||||||
#[cfg(feature = "interface")]
|
#[cfg(feature = "interface")]
|
||||||
use crate::protocol::leaf_meta::LeafMeta;
|
use crate::{interface::InterfaceStore, protocol::leaf_meta::LeafMeta};
|
||||||
|
|
||||||
/// Application extension point hosted by an [`Endpoint`].
|
/// Application extension point hosted by an [`Endpoint`].
|
||||||
///
|
///
|
||||||
@@ -18,9 +18,27 @@ pub trait Leaf {
|
|||||||
/// state, then enqueue outbound packets with [`Endpoint::add_outbound`].
|
/// state, then enqueue outbound packets with [`Endpoint::add_outbound`].
|
||||||
fn update(&mut self, _: &mut Endpoint);
|
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")]
|
#[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")]
|
#[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;
|
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 struct LeafMeta {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub identifier: &'static str,
|
pub identifier: &'static str,
|
||||||
pub version: &'static str,
|
pub version: &'static str,
|
||||||
pub authors: Vec<&'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 error;
|
||||||
mod leaf;
|
mod leaf;
|
||||||
mod leaf_meta;
|
mod leaf_meta;
|
||||||
|
mod leaf_template;
|
||||||
mod packet;
|
mod packet;
|
||||||
mod procedure;
|
mod procedure;
|
||||||
|
mod runtime;
|
||||||
mod session;
|
mod session;
|
||||||
|
|
||||||
|
pub use crate::unshell_leaf;
|
||||||
pub use endpoint::{Endpoint, HookID};
|
pub use endpoint::{Endpoint, HookID};
|
||||||
pub use error::*;
|
pub use error::*;
|
||||||
pub use leaf::Leaf;
|
pub use leaf::Leaf;
|
||||||
pub use leaf_meta::LeafMeta;
|
pub use leaf_meta::LeafMeta;
|
||||||
pub use packet::Packet;
|
pub use packet::Packet;
|
||||||
pub use procedure::*;
|
pub use procedure::*;
|
||||||
|
pub use runtime::*;
|
||||||
pub use session::*;
|
pub use session::*;
|
||||||
pub use unshell_macros::unshell_leaf;
|
|
||||||
|
#[cfg(feature = "interface_ratatui")]
|
||||||
|
pub use ratatui;
|
||||||
|
|
||||||
// Various named types used for brevity
|
// Various named types used for brevity
|
||||||
use alloc::{
|
use alloc::{
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ use alloc::vec::Vec;
|
|||||||
|
|
||||||
use crate::protocol::{Endpoint, HookID, Packet, PacketQueue};
|
use crate::protocol::{Endpoint, HookID, Packet, PacketQueue};
|
||||||
|
|
||||||
|
#[cfg(feature = "interface_ratatui")]
|
||||||
|
use crate::interface::ProcedureView;
|
||||||
|
|
||||||
/// Contract implemented by one generated one-packet procedure handler.
|
/// Contract implemented by one generated one-packet procedure handler.
|
||||||
///
|
///
|
||||||
/// Procedures are for stateless or short-lived operations such as ping, capabilities,
|
/// 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`.
|
/// Handles one packet and optionally queues response packets in `out`.
|
||||||
fn handle(leaf: &mut L, endpoint: &mut Endpoint, packet: Packet, out: &mut ProcedureOut);
|
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`].
|
/// 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};
|
use crate::protocol::{Endpoint, HookID, Packet, PacketQueue};
|
||||||
|
|
||||||
|
#[cfg(feature = "interface_ratatui")]
|
||||||
|
use crate::interface::SessionView;
|
||||||
|
|
||||||
/// Contract implemented by one hook-backed generated session family.
|
/// Contract implemented by one hook-backed generated session family.
|
||||||
///
|
///
|
||||||
/// A session family maps one outer `procedure_id` to many live hook instances. The
|
/// 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,
|
incoming: &mut PacketQueue,
|
||||||
ctx: &mut SessionCtx<'_>,
|
ctx: &mut SessionCtx<'_>,
|
||||||
) -> SessionStatus;
|
) -> SessionStatus;
|
||||||
|
|
||||||
|
#[cfg(feature = "interface_ratatui")]
|
||||||
|
fn render_ratatui(
|
||||||
|
_: &L,
|
||||||
|
_: &Self::State,
|
||||||
|
_: &mut SessionView,
|
||||||
|
_: &mut ratatui::Frame<'_>,
|
||||||
|
_: ratatui::layout::Rect,
|
||||||
|
) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Context passed to [`Session::init`].
|
/// Context passed to [`Session::init`].
|
||||||
@@ -249,6 +262,42 @@ pub struct SessionEntry<S> {
|
|||||||
pub closed: bool,
|
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> {
|
impl<S> SessionEntry<S> {
|
||||||
/// Creates one active session entry for `hook_id`.
|
/// Creates one active session entry for `hook_id`.
|
||||||
pub fn new(hook_id: HookID, state: S) -> Self {
|
pub fn new(hook_id: HookID, state: S) -> Self {
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ description = "Hook-backed PTY leaf implementation for UnShell"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
unshell = { workspace = true }
|
unshell = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
interface = ["unshell/interface"]
|
||||||
|
interface_ratatui = ["interface", "unshell/interface_ratatui"]
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
elided_lifetimes_in_paths = "warn"
|
elided_lifetimes_in_paths = "warn"
|
||||||
future_incompatible = { level = "warn", priority = -1 }
|
future_incompatible = { level = "warn", priority = -1 }
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ use crate::{constants::LEAF_FAKE_PTY, session::PtySession};
|
|||||||
|
|
||||||
/// User-owned state for the generated fake PTY leaf.
|
/// User-owned state for the generated fake PTY leaf.
|
||||||
///
|
///
|
||||||
/// The macro-generated `FakePtyLeaf` wrapper stores sessions and retry queues around
|
/// The `unshell_leaf!` template stores sessions and retry queues around this struct.
|
||||||
/// this struct. Keeping counters here makes tests and future procedures observe leaf
|
/// Keeping counters here makes tests and future procedures observe leaf behavior
|
||||||
/// behavior without reaching into generated session storage.
|
/// without reaching into generated session storage.
|
||||||
#[unshell_leaf(leaf = FakePtyLeaf, id = LEAF_FAKE_PTY, sessions(PtySession))]
|
|
||||||
pub struct FakePtyState {
|
pub struct FakePtyState {
|
||||||
/// Number of sessions that application logic considers active.
|
/// Number of sessions that application logic considers active.
|
||||||
pub active_count: usize,
|
pub active_count: usize,
|
||||||
@@ -35,3 +34,19 @@ impl Default for FakePtyState {
|
|||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ use alloc::{vec, vec::Vec};
|
|||||||
|
|
||||||
use unshell::protocol::{Endpoint, Leaf, Packet};
|
use unshell::protocol::{Endpoint, Leaf, Packet};
|
||||||
|
|
||||||
|
#[cfg(feature = "interface")]
|
||||||
|
use unshell::interface::{InterfaceEventKind, InterfaceStore};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
FakePtyLeaf, FakePtyState, OP_ABORT, OP_ERROR, OP_EXIT, OP_INPUT, OP_OPENED, OP_OUTPUT,
|
FakePtyLeaf, FakePtyState, OP_ABORT, OP_ERROR, OP_EXIT, OP_INPUT, OP_OPENED, OP_OUTPUT,
|
||||||
OP_STDIN_EOF, OP_TERMINATE, PROC_PTY, frame_opcode, frame_payload, pty_open_packet, pty_packet,
|
OP_STDIN_EOF, OP_TERMINATE, PROC_PTY, frame_opcode, frame_payload, pty_open_packet, pty_packet,
|
||||||
@@ -391,3 +394,39 @@ fn pty_leaf_does_not_consume_other_leaf_packets() {
|
|||||||
assert_eq!(other_packets[0].procedure_id, PROC_OTHER);
|
assert_eq!(other_packets[0].procedure_id, PROC_OTHER);
|
||||||
assert_eq!(other_packets[0].data, b"leave-me".to_vec());
|
assert_eq!(other_packets[0].data, b"leave-me".to_vec());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "interface")]
|
||||||
|
#[test]
|
||||||
|
fn interface_update_records_session_flow() {
|
||||||
|
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
|
||||||
|
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
||||||
|
let mut interface = InterfaceStore::new();
|
||||||
|
let hook_id = endpoint_a.get_hook_id();
|
||||||
|
|
||||||
|
endpoint_a
|
||||||
|
.add_outbound(pty_open_packet(
|
||||||
|
vec![ENDPOINT_A, ENDPOINT_B],
|
||||||
|
hook_id,
|
||||||
|
&[ENDPOINT_A],
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
transfer_packets(&mut endpoint_a, &mut endpoint_b, ENDPOINT_B, ENDPOINT_A);
|
||||||
|
|
||||||
|
leaf.update_interface(&mut endpoint_b, &mut interface);
|
||||||
|
|
||||||
|
assert_eq!(leaf.active_session_count(), 1);
|
||||||
|
assert!(interface.events().iter().any(|event| {
|
||||||
|
matches!(
|
||||||
|
&event.kind,
|
||||||
|
InterfaceEventKind::SessionCreated { hook_id: recorded_hook, .. }
|
||||||
|
if *recorded_hook == hook_id
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
assert!(interface.events().iter().any(|event| {
|
||||||
|
matches!(
|
||||||
|
&event.kind,
|
||||||
|
InterfaceEventKind::RouteSuccess { packet }
|
||||||
|
if packet.hook_id == hook_id && frame_opcode(packet) == Some(OP_OPENED)
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "unshell-macros-core"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
description = "Parser and code generator for UnShell procedural macros"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
proc-macro2 = { workspace = true }
|
|
||||||
quote = { workspace = true }
|
|
||||||
syn = { workspace = true, features = ["full", "extra-traits"] }
|
|
||||||
|
|
||||||
[lints.rust]
|
|
||||||
elided_lifetimes_in_paths = "warn"
|
|
||||||
future_incompatible = { level = "warn", priority = -1 }
|
|
||||||
nonstandard_style = { level = "warn", priority = -1 }
|
|
||||||
rust_2018_idioms = { level = "warn", priority = -1 }
|
|
||||||
rust_2021_prelude_collisions = "warn"
|
|
||||||
semicolon_in_expressions_from_macros = "warn"
|
|
||||||
unsafe_op_in_unsafe_fn = "warn"
|
|
||||||
unused_import_braces = "warn"
|
|
||||||
unused_lifetimes = "warn"
|
|
||||||
trivial_casts = "allow"
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
use syn::{
|
|
||||||
Expr, Ident, Result, Token, Type,
|
|
||||||
parse::{Parse, ParseStream},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Parsed arguments from `#[unshell_leaf(...)]`.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) struct UnshellLeafArgs {
|
|
||||||
pub(crate) leaf: Ident,
|
|
||||||
pub(crate) id: Expr,
|
|
||||||
pub(crate) sessions: Vec<Type>,
|
|
||||||
pub(crate) procedures: Vec<Type>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Parse for UnshellLeafArgs {
|
|
||||||
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
|
||||||
let mut leaf = None;
|
|
||||||
let mut id = None;
|
|
||||||
let mut sessions = Vec::new();
|
|
||||||
let mut procedures = Vec::new();
|
|
||||||
|
|
||||||
while !input.is_empty() {
|
|
||||||
let key: Ident = input.parse()?;
|
|
||||||
match key.to_string().as_str() {
|
|
||||||
"leaf" => {
|
|
||||||
reject_duplicate(&leaf, &key)?;
|
|
||||||
input.parse::<Token![=]>()?;
|
|
||||||
leaf = Some(input.parse()?);
|
|
||||||
}
|
|
||||||
"id" => {
|
|
||||||
reject_duplicate(&id, &key)?;
|
|
||||||
input.parse::<Token![=]>()?;
|
|
||||||
id = Some(input.parse()?);
|
|
||||||
}
|
|
||||||
"sessions" => {
|
|
||||||
sessions = parse_type_list(input)?;
|
|
||||||
}
|
|
||||||
"procedures" => {
|
|
||||||
procedures = parse_type_list(input)?;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(syn::Error::new(
|
|
||||||
key.span(),
|
|
||||||
"expected `leaf`, `id`, `sessions`, or `procedures`",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.peek(Token![,]) {
|
|
||||||
input.parse::<Token![,]>()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
leaf: leaf.ok_or_else(|| input.error("missing `leaf = WrapperName`"))?,
|
|
||||||
id: id.ok_or_else(|| input.error("missing `id = LEAF_ID`"))?,
|
|
||||||
sessions,
|
|
||||||
procedures,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rejects repeated scalar keys while keeping repeated list keys additive by design.
|
|
||||||
fn reject_duplicate<T>(slot: &Option<T>, key: &Ident) -> Result<()> {
|
|
||||||
if slot.is_some() {
|
|
||||||
Err(syn::Error::new(key.span(), "duplicate key"))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses `name(Type, Type)` argument payloads.
|
|
||||||
fn parse_type_list(input: ParseStream<'_>) -> Result<Vec<Type>> {
|
|
||||||
let content;
|
|
||||||
syn::parenthesized!(content in input);
|
|
||||||
let parsed = content.parse_terminated(Type::parse, Token![,])?;
|
|
||||||
Ok(parsed.into_iter().collect())
|
|
||||||
}
|
|
||||||
@@ -1,434 +0,0 @@
|
|||||||
use proc_macro2::TokenStream;
|
|
||||||
use quote::{format_ident, quote};
|
|
||||||
use syn::{Ident, ItemStruct, Result, Type};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
UnshellLeafArgs,
|
|
||||||
names::{last_type_ident, to_snake_case},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Code generator state for one `#[unshell_leaf]` expansion.
|
|
||||||
pub(crate) struct LeafGenerator {
|
|
||||||
args: UnshellLeafArgs,
|
|
||||||
state: ItemStruct,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LeafGenerator {
|
|
||||||
/// Creates a generator for one parsed state struct.
|
|
||||||
pub(crate) fn new(args: UnshellLeafArgs, state: ItemStruct) -> Self {
|
|
||||||
Self { args, state }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emits the original state struct plus the generated wrapper leaf.
|
|
||||||
pub(crate) fn expand(self) -> Result<TokenStream> {
|
|
||||||
let state = &self.state;
|
|
||||||
let state_ident = &state.ident;
|
|
||||||
let leaf_ident = &self.args.leaf;
|
|
||||||
let leaf_id = &self.args.id;
|
|
||||||
let vis = &state.vis;
|
|
||||||
let generics = &state.generics;
|
|
||||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
|
||||||
let state_type = quote!(#state_ident #ty_generics);
|
|
||||||
|
|
||||||
let session_stores = self.session_stores()?;
|
|
||||||
let fields = self.store_fields(&session_stores, &state_type);
|
|
||||||
let initializers = self.store_initializers(&session_stores);
|
|
||||||
let packet_predicates = self.packet_predicates(&state_type);
|
|
||||||
let dispatch_arms = self.dispatch_arms(&session_stores, &state_type);
|
|
||||||
let session_updates = self.session_updates(&session_stores, &state_type);
|
|
||||||
let session_flushes = self.session_flushes(&session_stores);
|
|
||||||
let session_retains = self.session_retains(&session_stores);
|
|
||||||
let active_count_terms = self.active_count_terms(&session_stores);
|
|
||||||
let pending_count_terms = self.pending_count_terms(&session_stores);
|
|
||||||
let id_checks = self.id_checks(&state_type);
|
|
||||||
|
|
||||||
Ok(quote! {
|
|
||||||
#state
|
|
||||||
|
|
||||||
#vis struct #leaf_ident #generics #where_clause {
|
|
||||||
state: #state_type,
|
|
||||||
__unshell_procedure_outbox: ::unshell::protocol::PacketQueue,
|
|
||||||
#(#fields,)*
|
|
||||||
}
|
|
||||||
|
|
||||||
impl #impl_generics #leaf_ident #ty_generics #where_clause {
|
|
||||||
const __UNSHELL_PROCEDURE_ID_CHECKS: () = {
|
|
||||||
#(#id_checks)*
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Creates the generated leaf wrapper around user-owned state.
|
|
||||||
pub fn new(state: #state_type) -> Self {
|
|
||||||
Self {
|
|
||||||
state,
|
|
||||||
__unshell_procedure_outbox: ::unshell::protocol::PacketQueue::new(),
|
|
||||||
#(#initializers,)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns immutable access to the user-owned leaf state.
|
|
||||||
pub fn state(&self) -> &#state_type {
|
|
||||||
&self.state
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns mutable access to the user-owned leaf state.
|
|
||||||
pub fn state_mut(&mut self) -> &mut #state_type {
|
|
||||||
&mut self.state
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the number of active session entries across all session families.
|
|
||||||
pub fn active_session_count(&self) -> usize {
|
|
||||||
0usize #(+ #active_count_terms)*
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns queued inbound and outbound packets owned by this generated leaf.
|
|
||||||
pub fn pending_packet_count(&self) -> usize {
|
|
||||||
let mut __unshell_count = self.__unshell_procedure_outbox.len();
|
|
||||||
#(#pending_count_terms)*
|
|
||||||
__unshell_count
|
|
||||||
}
|
|
||||||
|
|
||||||
fn __unshell_packet_is_owned(packet: &::unshell::protocol::Packet) -> bool {
|
|
||||||
false #(|| #packet_predicates)*
|
|
||||||
}
|
|
||||||
|
|
||||||
fn __unshell_dispatch(
|
|
||||||
&mut self,
|
|
||||||
endpoint: &mut ::unshell::protocol::Endpoint,
|
|
||||||
packet: ::unshell::protocol::Packet,
|
|
||||||
) {
|
|
||||||
#(#dispatch_arms)*
|
|
||||||
}
|
|
||||||
|
|
||||||
fn __unshell_update_sessions(&mut self) {
|
|
||||||
#(#session_updates)*
|
|
||||||
}
|
|
||||||
|
|
||||||
fn __unshell_flush_all(&mut self, endpoint: &mut ::unshell::protocol::Endpoint) {
|
|
||||||
::unshell::protocol::flush_packet_queue(
|
|
||||||
endpoint,
|
|
||||||
&mut self.__unshell_procedure_outbox,
|
|
||||||
);
|
|
||||||
#(#session_flushes)*
|
|
||||||
#(#session_retains)*
|
|
||||||
}
|
|
||||||
|
|
||||||
fn __unshell_parent_reply_path(
|
|
||||||
endpoint: &::unshell::protocol::Endpoint,
|
|
||||||
) -> ::unshell::alloc::vec::Vec<u32> {
|
|
||||||
if endpoint.path.len() > 1 {
|
|
||||||
endpoint.path[..endpoint.path.len() - 1].to_vec()
|
|
||||||
} else {
|
|
||||||
endpoint.path.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl #impl_generics ::unshell::protocol::Leaf for #leaf_ident #ty_generics #where_clause {
|
|
||||||
fn get_id(&self) -> u32 {
|
|
||||||
#leaf_id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, endpoint: &mut ::unshell::protocol::Endpoint) {
|
|
||||||
self.__unshell_flush_all(endpoint);
|
|
||||||
|
|
||||||
let Some(__unshell_local_id) = endpoint.path.last().copied() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut __unshell_packets = ::unshell::alloc::vec::Vec::new();
|
|
||||||
endpoint.take_inbound_matching(
|
|
||||||
__unshell_local_id,
|
|
||||||
Self::__unshell_packet_is_owned,
|
|
||||||
|packet| __unshell_packets.push(packet),
|
|
||||||
);
|
|
||||||
|
|
||||||
for __unshell_packet in __unshell_packets {
|
|
||||||
self.__unshell_dispatch(endpoint, __unshell_packet);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.__unshell_update_sessions();
|
|
||||||
self.__unshell_flush_all(endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Computes one generated store name per session type.
|
|
||||||
fn session_stores(&self) -> Result<Vec<SessionStore>> {
|
|
||||||
self.args
|
|
||||||
.sessions
|
|
||||||
.iter()
|
|
||||||
.map(|session| {
|
|
||||||
let suffix = last_type_ident(session)?;
|
|
||||||
let field_suffix = to_snake_case(&suffix.to_string());
|
|
||||||
Ok(SessionStore {
|
|
||||||
ty: session.clone(),
|
|
||||||
field: format_ident!("__unshell_{}_sessions", field_suffix),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emits wrapper fields for session stores.
|
|
||||||
fn store_fields(&self, stores: &[SessionStore], state_type: &TokenStream) -> Vec<TokenStream> {
|
|
||||||
stores
|
|
||||||
.iter()
|
|
||||||
.map(|store| {
|
|
||||||
let field = &store.field;
|
|
||||||
let session_ty = &store.ty;
|
|
||||||
quote! {
|
|
||||||
#field: ::unshell::alloc::vec::Vec<
|
|
||||||
::unshell::protocol::SessionEntry<
|
|
||||||
<#session_ty as ::unshell::protocol::Session<#state_type>>::State
|
|
||||||
>
|
|
||||||
>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emits constructor field initializers for session stores.
|
|
||||||
fn store_initializers(&self, stores: &[SessionStore]) -> Vec<TokenStream> {
|
|
||||||
stores
|
|
||||||
.iter()
|
|
||||||
.map(|store| {
|
|
||||||
let field = &store.field;
|
|
||||||
quote!(#field: ::unshell::alloc::vec::Vec::new())
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emits boolean procedure-id ownership checks for the filtered endpoint drain.
|
|
||||||
fn packet_predicates(&self, state_type: &TokenStream) -> Vec<TokenStream> {
|
|
||||||
let session_checks = self.args.sessions.iter().map(|session_ty| {
|
|
||||||
quote! {
|
|
||||||
packet.procedure_id
|
|
||||||
== <#session_ty as ::unshell::protocol::Session<#state_type>>::PROCEDURE_ID
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let procedure_checks = self.args.procedures.iter().map(|procedure_ty| {
|
|
||||||
quote! {
|
|
||||||
packet.procedure_id
|
|
||||||
== <#procedure_ty as ::unshell::protocol::Procedure<#state_type>>::PROCEDURE_ID
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
session_checks.chain(procedure_checks).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emits static dispatch branches for every session and procedure type.
|
|
||||||
fn dispatch_arms(&self, stores: &[SessionStore], state_type: &TokenStream) -> Vec<TokenStream> {
|
|
||||||
let mut arms = Vec::new();
|
|
||||||
|
|
||||||
for store in stores {
|
|
||||||
let field = &store.field;
|
|
||||||
let session_ty = &store.ty;
|
|
||||||
arms.push(quote! {
|
|
||||||
if packet.procedure_id
|
|
||||||
== <#session_ty as ::unshell::protocol::Session<#state_type>>::PROCEDURE_ID
|
|
||||||
{
|
|
||||||
if let Some(__unshell_entry) = self
|
|
||||||
.#field
|
|
||||||
.iter_mut()
|
|
||||||
.find(|entry| entry.hook_id == packet.hook_id)
|
|
||||||
{
|
|
||||||
__unshell_entry.inbox.push_back(packet);
|
|
||||||
} else {
|
|
||||||
let __unshell_hook_id = packet.hook_id;
|
|
||||||
let __unshell_packet_path = packet.path.clone();
|
|
||||||
let mut __unshell_init = ::unshell::protocol::SessionInit::new(
|
|
||||||
__unshell_hook_id,
|
|
||||||
__unshell_packet_path,
|
|
||||||
);
|
|
||||||
|
|
||||||
match <#session_ty as ::unshell::protocol::Session<#state_type>>::init(
|
|
||||||
&mut self.state,
|
|
||||||
packet,
|
|
||||||
&mut __unshell_init,
|
|
||||||
) {
|
|
||||||
::unshell::protocol::SessionInitResult::Created(__unshell_state) => {
|
|
||||||
self.#field.push(::unshell::protocol::SessionEntry::new(
|
|
||||||
__unshell_hook_id,
|
|
||||||
__unshell_state,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
::unshell::protocol::SessionInitResult::Rejected => {}
|
|
||||||
::unshell::protocol::SessionInitResult::RejectedWith(__unshell_packet) => {
|
|
||||||
self.__unshell_procedure_outbox.push_back(__unshell_packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for procedure_ty in &self.args.procedures {
|
|
||||||
arms.push(quote! {
|
|
||||||
if packet.procedure_id
|
|
||||||
== <#procedure_ty as ::unshell::protocol::Procedure<#state_type>>::PROCEDURE_ID
|
|
||||||
{
|
|
||||||
let mut __unshell_out = ::unshell::protocol::ProcedureOut::new(
|
|
||||||
packet.hook_id,
|
|
||||||
Self::__unshell_parent_reply_path(endpoint),
|
|
||||||
<#procedure_ty as ::unshell::protocol::Procedure<#state_type>>::PROCEDURE_ID,
|
|
||||||
);
|
|
||||||
<#procedure_ty as ::unshell::protocol::Procedure<#state_type>>::handle(
|
|
||||||
&mut self.state,
|
|
||||||
endpoint,
|
|
||||||
packet,
|
|
||||||
&mut __unshell_out,
|
|
||||||
);
|
|
||||||
self.__unshell_procedure_outbox.extend(__unshell_out.into_packets());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
arms
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emits the per-session update loop for every session family.
|
|
||||||
fn session_updates(
|
|
||||||
&self,
|
|
||||||
stores: &[SessionStore],
|
|
||||||
state_type: &TokenStream,
|
|
||||||
) -> Vec<TokenStream> {
|
|
||||||
stores
|
|
||||||
.iter()
|
|
||||||
.map(|store| {
|
|
||||||
let field = &store.field;
|
|
||||||
let session_ty = &store.ty;
|
|
||||||
quote! {
|
|
||||||
for __unshell_entry in &mut self.#field {
|
|
||||||
if __unshell_entry.closed {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let __unshell_reply_path =
|
|
||||||
<#session_ty as ::unshell::protocol::Session<#state_type>>::reply_path(
|
|
||||||
&__unshell_entry.state,
|
|
||||||
)
|
|
||||||
.to_vec();
|
|
||||||
let mut __unshell_ctx = ::unshell::protocol::SessionCtx::new(
|
|
||||||
__unshell_entry.hook_id,
|
|
||||||
__unshell_reply_path,
|
|
||||||
<#session_ty as ::unshell::protocol::Session<#state_type>>::PROCEDURE_ID,
|
|
||||||
&mut __unshell_entry.outbox,
|
|
||||||
);
|
|
||||||
let __unshell_status =
|
|
||||||
<#session_ty as ::unshell::protocol::Session<#state_type>>::update(
|
|
||||||
&mut self.state,
|
|
||||||
&mut __unshell_entry.state,
|
|
||||||
&mut __unshell_entry.inbox,
|
|
||||||
&mut __unshell_ctx,
|
|
||||||
);
|
|
||||||
|
|
||||||
if ::core::matches!(
|
|
||||||
__unshell_status,
|
|
||||||
::unshell::protocol::SessionStatus::Closed
|
|
||||||
) {
|
|
||||||
__unshell_entry.closed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emits retry flushing for every session outbox.
|
|
||||||
fn session_flushes(&self, stores: &[SessionStore]) -> Vec<TokenStream> {
|
|
||||||
stores
|
|
||||||
.iter()
|
|
||||||
.map(|store| {
|
|
||||||
let field = &store.field;
|
|
||||||
quote! {
|
|
||||||
for __unshell_entry in &mut self.#field {
|
|
||||||
::unshell::protocol::flush_packet_queue(
|
|
||||||
endpoint,
|
|
||||||
&mut __unshell_entry.outbox,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emits removal of closed sessions whose final packets have routed.
|
|
||||||
fn session_retains(&self, stores: &[SessionStore]) -> Vec<TokenStream> {
|
|
||||||
stores
|
|
||||||
.iter()
|
|
||||||
.map(|store| {
|
|
||||||
let field = &store.field;
|
|
||||||
quote! {
|
|
||||||
self.#field
|
|
||||||
.retain(|entry| !entry.closed || !entry.outbox.is_empty());
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emits additive terms for active session counts.
|
|
||||||
fn active_count_terms(&self, stores: &[SessionStore]) -> Vec<TokenStream> {
|
|
||||||
stores
|
|
||||||
.iter()
|
|
||||||
.map(|store| {
|
|
||||||
let field = &store.field;
|
|
||||||
quote!(self.#field.len())
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emits statements that accumulate pending packet counts.
|
|
||||||
fn pending_count_terms(&self, stores: &[SessionStore]) -> Vec<TokenStream> {
|
|
||||||
stores
|
|
||||||
.iter()
|
|
||||||
.map(|store| {
|
|
||||||
let field = &store.field;
|
|
||||||
quote! {
|
|
||||||
for __unshell_entry in &self.#field {
|
|
||||||
__unshell_count +=
|
|
||||||
__unshell_entry.inbox.len() + __unshell_entry.outbox.len();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emits pairwise const assertions for procedure-id uniqueness.
|
|
||||||
fn id_checks(&self, state_type: &TokenStream) -> Vec<TokenStream> {
|
|
||||||
let mut ids = Vec::new();
|
|
||||||
for session_ty in &self.args.sessions {
|
|
||||||
ids.push(
|
|
||||||
quote!(<#session_ty as ::unshell::protocol::Session<#state_type>>::PROCEDURE_ID),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for procedure_ty in &self.args.procedures {
|
|
||||||
ids.push(
|
|
||||||
quote!(<#procedure_ty as ::unshell::protocol::Procedure<#state_type>>::PROCEDURE_ID),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut checks = Vec::new();
|
|
||||||
for left in 0..ids.len() {
|
|
||||||
for right in (left + 1)..ids.len() {
|
|
||||||
let left_id = &ids[left];
|
|
||||||
let right_id = &ids[right];
|
|
||||||
checks.push(quote! {
|
|
||||||
assert!(
|
|
||||||
#left_id != #right_id,
|
|
||||||
"duplicate unshell procedure id in #[unshell_leaf]"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checks
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generated storage metadata for one session family.
|
|
||||||
struct SessionStore {
|
|
||||||
ty: Type,
|
|
||||||
field: Ident,
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
//! Leaf wrapper macro implementation.
|
|
||||||
//!
|
|
||||||
//! Everything in this module is specific to `#[unshell_leaf]`: argument parsing,
|
|
||||||
//! generated wrapper storage, static dispatch, and retry-safe session output. Future
|
|
||||||
//! macro families should be added as sibling modules instead of sharing this internal
|
|
||||||
//! structure.
|
|
||||||
|
|
||||||
mod args;
|
|
||||||
mod generator;
|
|
||||||
mod names;
|
|
||||||
|
|
||||||
use proc_macro2::TokenStream;
|
|
||||||
use syn::{ItemStruct, Result, parse2};
|
|
||||||
|
|
||||||
pub(crate) use args::UnshellLeafArgs;
|
|
||||||
pub(crate) use generator::LeafGenerator;
|
|
||||||
|
|
||||||
/// Expands `#[unshell_leaf(...)]` into a wrapper leaf and `Leaf` implementation.
|
|
||||||
///
|
|
||||||
/// Errors are returned as tokenized `compile_error!` output so the proc-macro shim can
|
|
||||||
/// stay a thin transport layer from compiler tokens to this core implementation.
|
|
||||||
pub fn expand_unshell_leaf(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
||||||
match expand_unshell_leaf_result(attr, item) {
|
|
||||||
Ok(tokens) => tokens,
|
|
||||||
Err(error) => error.to_compile_error(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fallible expansion path used by unit tests.
|
|
||||||
pub fn expand_unshell_leaf_result(attr: TokenStream, item: TokenStream) -> Result<TokenStream> {
|
|
||||||
let args = parse2::<UnshellLeafArgs>(attr)?;
|
|
||||||
let state = parse2::<ItemStruct>(item)?;
|
|
||||||
LeafGenerator::new(args, state).expand()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use quote::quote;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_leaf_arguments() {
|
|
||||||
let args = parse2::<UnshellLeafArgs>(quote! {
|
|
||||||
leaf = DemoLeaf,
|
|
||||||
id = 42,
|
|
||||||
sessions(DemoSession),
|
|
||||||
procedures(PingProcedure)
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(args.leaf, "DemoLeaf");
|
|
||||||
assert_eq!(args.sessions.len(), 1);
|
|
||||||
assert_eq!(args.procedures.len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn missing_leaf_is_rejected() {
|
|
||||||
let error = parse2::<UnshellLeafArgs>(quote! { id = 42 }).unwrap_err();
|
|
||||||
|
|
||||||
assert!(error.to_string().contains("missing `leaf"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn expansion_contains_static_dispatch() {
|
|
||||||
let expanded = expand_unshell_leaf_result(
|
|
||||||
quote! { leaf = DemoLeaf, id = 9, sessions(DemoSession) },
|
|
||||||
quote! { pub struct DemoState; },
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
assert!(expanded.contains("struct DemoLeaf"));
|
|
||||||
assert!(expanded.contains("impl :: unshell :: protocol :: Leaf for DemoLeaf"));
|
|
||||||
assert!(expanded.contains("DemoSession"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
use syn::{Ident, Result, Type};
|
|
||||||
|
|
||||||
/// Returns the final path segment for a session type.
|
|
||||||
pub(crate) fn last_type_ident(ty: &Type) -> Result<Ident> {
|
|
||||||
let Type::Path(path) = ty else {
|
|
||||||
return Err(syn::Error::new_spanned(
|
|
||||||
ty,
|
|
||||||
"session types must be named paths",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
let Some(segment) = path.path.segments.last() else {
|
|
||||||
return Err(syn::Error::new_spanned(ty, "session type path is empty"));
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(segment.ident.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts a Rust type name into a snake-case fragment for generated private fields.
|
|
||||||
pub(crate) fn to_snake_case(name: &str) -> String {
|
|
||||||
let mut output = String::with_capacity(name.len());
|
|
||||||
let chars: Vec<char> = name.chars().collect();
|
|
||||||
|
|
||||||
for (index, character) in chars.iter().copied().enumerate() {
|
|
||||||
if character.is_ascii_uppercase() {
|
|
||||||
let previous = index
|
|
||||||
.checked_sub(1)
|
|
||||||
.and_then(|previous| chars.get(previous));
|
|
||||||
let next = chars.get(index + 1);
|
|
||||||
let previous_needs_boundary = previous
|
|
||||||
.map(|previous| previous.is_ascii_lowercase() || previous.is_ascii_digit())
|
|
||||||
.unwrap_or(false);
|
|
||||||
let acronym_needs_boundary = previous
|
|
||||||
.map(|previous| previous.is_ascii_uppercase())
|
|
||||||
.unwrap_or(false)
|
|
||||||
&& next.map(|next| next.is_ascii_lowercase()).unwrap_or(false);
|
|
||||||
|
|
||||||
if previous_needs_boundary || acronym_needs_boundary {
|
|
||||||
output.push('_');
|
|
||||||
}
|
|
||||||
output.push(character.to_ascii_lowercase());
|
|
||||||
} else {
|
|
||||||
output.push(character);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::to_snake_case;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn session_store_fields_are_snake_case() {
|
|
||||||
assert_eq!(to_snake_case("PtySession"), "pty_session");
|
|
||||||
assert_eq!(to_snake_case("HTTPServer"), "http_server");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
//! Parser and code generator for UnShell procedural macros.
|
|
||||||
//!
|
|
||||||
//! This crate is intentionally not a proc-macro crate. Keeping each macro family's
|
|
||||||
//! parser and code generator here makes them unit-testable and prevents parsing
|
|
||||||
//! dependencies from leaking into runtime crates.
|
|
||||||
|
|
||||||
mod leaf;
|
|
||||||
|
|
||||||
pub use leaf::{expand_unshell_leaf, expand_unshell_leaf_result};
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "unshell-macros"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
description = "Procedural macros for UnShell leaves"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
proc-macro = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
unshell-macros-core = { workspace = true }
|
|
||||||
|
|
||||||
[lints.rust]
|
|
||||||
elided_lifetimes_in_paths = "warn"
|
|
||||||
future_incompatible = { level = "warn", priority = -1 }
|
|
||||||
nonstandard_style = { level = "warn", priority = -1 }
|
|
||||||
rust_2018_idioms = { level = "warn", priority = -1 }
|
|
||||||
rust_2021_prelude_collisions = "warn"
|
|
||||||
semicolon_in_expressions_from_macros = "warn"
|
|
||||||
unsafe_op_in_unsafe_fn = "warn"
|
|
||||||
unused_import_braces = "warn"
|
|
||||||
unused_lifetimes = "warn"
|
|
||||||
trivial_casts = "allow"
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
//! Procedural macro shim for UnShell.
|
|
||||||
//!
|
|
||||||
//! The real parser and code generator live in `unshell-macros-core` so they can be
|
|
||||||
//! tested as ordinary Rust. This crate only adapts compiler `TokenStream`s.
|
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
|
||||||
|
|
||||||
/// Generates an `unshell_protocol::Leaf` wrapper for a user-owned state struct.
|
|
||||||
///
|
|
||||||
/// See `LEAF_MACRO_INTERFACE.md` for the design contract. The generated wrapper owns
|
|
||||||
/// session stores, retry queues, filtered packet dispatch, and final-frame cleanup.
|
|
||||||
#[proc_macro_attribute]
|
|
||||||
pub fn unshell_leaf(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
||||||
unshell_macros_core::expand_unshell_leaf(attr.into(), item.into()).into()
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user