mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 14:36:01 -06:00
Add "LeafMeta" struct to leaf
This commit is contained in:
Generated
+1102
-18
File diff suppressed because it is too large
Load Diff
+8
-13
@@ -7,8 +7,6 @@ members = [
|
||||
"unshell-macros-core",
|
||||
"unshell-macros",
|
||||
|
||||
# "unshell-protocol",
|
||||
|
||||
"unshell-leaves/leaf-pty",
|
||||
]
|
||||
resolver = "2"
|
||||
@@ -32,6 +30,8 @@ proc-macro2 = "1.0.106"
|
||||
portable-pty = "0.9.0"
|
||||
crossbeam-channel = "0.5.15"
|
||||
|
||||
ratatui = "0.30.0"
|
||||
|
||||
unshell = { path = "." }
|
||||
# unshell-protocol = { path = "./unshell-protocol" }
|
||||
unshell-macros-core = { path = "./unshell-macros-core" }
|
||||
@@ -51,28 +51,23 @@ edition.workspace = true
|
||||
description = "Pure no_std implementation of the UnShell Protocol"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# default = ["interface_ratatui"]
|
||||
|
||||
log = []
|
||||
log_debug = ["log", "dep:chrono"]
|
||||
|
||||
# Leaf features
|
||||
# leaf_endpoint = ["unshell-leaves/leaf_endpoint"]
|
||||
# leaf_tui = ["unshell-leaves/leaf_tui"]
|
||||
|
||||
# obfuscate_aes = ["ush-obfuscate/obfuscate_aes"]
|
||||
# obfuscate_ref = ["ush-obfuscate/obfuscate_ref"]
|
||||
interface = []
|
||||
interface_ratatui = ["interface", "dep:ratatui"]
|
||||
|
||||
[dependencies]
|
||||
rkyv = { workspace = true }
|
||||
thiserror = { workspace = true, optional = true }
|
||||
chrono = { workspace = true, optional = true }
|
||||
# ush-obfuscate = { workspace = true }
|
||||
static_init = { workspace = true }
|
||||
|
||||
ratatui = { workspace = true, optional = true }
|
||||
|
||||
unshell-macros = { workspace = true }
|
||||
# unshell-protocol = { workspace = true }
|
||||
# unshell-runtime = { workspace = true }
|
||||
# unshell-leaves = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
crossbeam-channel.workspace = true
|
||||
|
||||
@@ -120,4 +120,11 @@ impl Endpoint {
|
||||
queue.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter_leaves<F>(&mut self) -> core::slice::IterMut<'_, Box<dyn Leaf + 'static>>
|
||||
where
|
||||
F: FnMut(&Packet),
|
||||
{
|
||||
self.leaves.iter_mut()
|
||||
}
|
||||
}
|
||||
|
||||
+9
-342
@@ -1,6 +1,7 @@
|
||||
use crate::protocol::{Endpoint, HookID, Packet, PacketQueue};
|
||||
use crate::protocol::Endpoint;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
#[cfg(feature = "interface")]
|
||||
use crate::protocol::leaf_meta::LeafMeta;
|
||||
|
||||
/// Application extension point hosted by an [`Endpoint`].
|
||||
///
|
||||
@@ -16,344 +17,10 @@ pub trait Leaf {
|
||||
/// Implementations normally drain matching inbound packets, mutate leaf-owned
|
||||
/// state, then enqueue outbound packets with [`Endpoint::add_outbound`].
|
||||
fn update(&mut self, _: &mut Endpoint);
|
||||
}
|
||||
|
||||
/// Contract implemented by one hook-backed generated session family.
|
||||
///
|
||||
/// A session family maps one outer `procedure_id` to many live hook instances. The
|
||||
/// generated leaf owns packet grouping, retry-safe output flushing, and final cleanup;
|
||||
/// the session implementation owns only application behavior.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// impl Session<MyLeafState> for MySession {
|
||||
/// const PROCEDURE_ID: u32 = 7;
|
||||
/// type State = MySessionState;
|
||||
///
|
||||
/// fn reply_path(state: &Self::State) -> &[u32] {
|
||||
/// &state.reply_path
|
||||
/// }
|
||||
///
|
||||
/// fn init(
|
||||
/// leaf: &mut MyLeafState,
|
||||
/// packet: Packet,
|
||||
/// ctx: &mut SessionInit,
|
||||
/// ) -> SessionInitResult<Self::State> {
|
||||
/// SessionInitResult::Created(MySessionState::from_open(leaf, packet, ctx))
|
||||
/// }
|
||||
///
|
||||
/// fn update(
|
||||
/// leaf: &mut MyLeafState,
|
||||
/// session: &mut Self::State,
|
||||
/// incoming: &mut PacketQueue,
|
||||
/// ctx: &mut SessionCtx<'_>,
|
||||
/// ) -> SessionStatus {
|
||||
/// while let Some(packet) = incoming.pop_front() {
|
||||
/// session.apply(leaf, packet, ctx);
|
||||
/// }
|
||||
/// SessionStatus::Running
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait Session<L> {
|
||||
/// Outer packet procedure id used by every packet in this session family.
|
||||
const PROCEDURE_ID: u32;
|
||||
|
||||
/// Application state stored for one live hook.
|
||||
type State;
|
||||
|
||||
/// Returns the destination path for responses emitted by this session.
|
||||
///
|
||||
/// `Packet` currently carries only a destination path, so protocols that need to
|
||||
/// reply to a caller should capture a reply path during [`Self::init`]. The
|
||||
/// generated leaf clones this path into [`SessionCtx`] before calling update so
|
||||
/// session code can mutably borrow its state while emitting frames.
|
||||
fn reply_path(session: &Self::State) -> &[u32];
|
||||
|
||||
/// Creates one session state from a packet whose hook has no active session.
|
||||
///
|
||||
/// Returning [`SessionInitResult::RejectedWith`] lets the generated leaf route a
|
||||
/// protocol-level failure response with the same retry guarantees as normal
|
||||
/// output. Returning [`SessionInitResult::Rejected`] silently consumes the packet.
|
||||
fn init(leaf: &mut L, packet: Packet, ctx: &mut SessionInit) -> SessionInitResult<Self::State>;
|
||||
|
||||
/// Advances one active hook session.
|
||||
///
|
||||
/// The generated leaf calls this for every live session on each update tick so
|
||||
/// sessions can poll external workers even when no new packet arrived. Outbound
|
||||
/// packets must be queued through `ctx`; direct endpoint routing would bypass the
|
||||
/// generated retry rules.
|
||||
fn update(
|
||||
leaf: &mut L,
|
||||
session: &mut Self::State,
|
||||
incoming: &mut PacketQueue,
|
||||
ctx: &mut SessionCtx<'_>,
|
||||
) -> SessionStatus;
|
||||
}
|
||||
|
||||
/// Contract implemented by one generated one-packet procedure handler.
|
||||
///
|
||||
/// Procedures are for stateless or short-lived operations such as ping, capabilities,
|
||||
/// or health checks. Long-running conversations should use [`Session`] so final
|
||||
/// packet cleanup and retries remain tied to hook state.
|
||||
pub trait Procedure<L> {
|
||||
/// Outer packet procedure id handled by this procedure.
|
||||
const PROCEDURE_ID: u32;
|
||||
|
||||
/// Handles one packet and optionally queues response packets in `out`.
|
||||
fn handle(leaf: &mut L, endpoint: &mut Endpoint, packet: Packet, out: &mut ProcedureOut);
|
||||
}
|
||||
|
||||
/// Context passed to [`Session::init`].
|
||||
///
|
||||
/// This carries routing metadata that the generated leaf already knows before the
|
||||
/// session state exists. Protocols that need source paths should encode them in the
|
||||
/// packet payload; `packet_path` is the destination path that routed the packet here.
|
||||
pub struct SessionInit {
|
||||
hook_id: HookID,
|
||||
packet_path: Vec<u32>,
|
||||
}
|
||||
|
||||
impl SessionInit {
|
||||
/// Creates initialization metadata for a delivered packet.
|
||||
pub fn new(hook_id: HookID, packet_path: Vec<u32>) -> Self {
|
||||
Self {
|
||||
hook_id,
|
||||
packet_path,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the hook id that will identify the new session.
|
||||
pub fn hook_id(&self) -> HookID {
|
||||
self.hook_id
|
||||
}
|
||||
|
||||
/// Returns the destination path from the packet that reached this leaf.
|
||||
pub fn packet_path(&self) -> &[u32] {
|
||||
&self.packet_path
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of trying to create a session from a packet without an active hook entry.
|
||||
pub enum SessionInitResult<S> {
|
||||
/// A new session was created and should be stored by the generated leaf.
|
||||
Created(S),
|
||||
|
||||
/// The packet was intentionally consumed without creating state or a response.
|
||||
Rejected,
|
||||
|
||||
/// The packet was rejected with a response that the generated leaf must route.
|
||||
RejectedWith(Packet),
|
||||
}
|
||||
|
||||
/// Session lifecycle status returned from [`Session::update`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SessionStatus {
|
||||
/// The session is active and should receive future update ticks.
|
||||
Running,
|
||||
|
||||
/// The session is winding down but still needs future update ticks.
|
||||
Closing,
|
||||
|
||||
/// The session has finished application work.
|
||||
///
|
||||
/// The generated leaf still retains the entry until every queued packet routes
|
||||
/// successfully, which prevents a failed final frame from losing session cleanup.
|
||||
Closed,
|
||||
}
|
||||
|
||||
/// Mutable output context passed to [`Session::update`].
|
||||
///
|
||||
/// The context queues packets only; it never routes them immediately. Centralizing
|
||||
/// routing in generated code is what makes final-frame retries reliable.
|
||||
pub struct SessionCtx<'a> {
|
||||
hook_id: HookID,
|
||||
reply_path: Vec<u32>,
|
||||
procedure_id: u32,
|
||||
outbox: &'a mut PacketQueue,
|
||||
}
|
||||
|
||||
impl<'a> SessionCtx<'a> {
|
||||
/// Creates a context for one session update call.
|
||||
pub fn new(
|
||||
hook_id: HookID,
|
||||
reply_path: Vec<u32>,
|
||||
procedure_id: u32,
|
||||
outbox: &'a mut PacketQueue,
|
||||
) -> Self {
|
||||
Self {
|
||||
hook_id,
|
||||
reply_path,
|
||||
procedure_id,
|
||||
outbox,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the hook id used for packets emitted through this context.
|
||||
pub fn hook_id(&self) -> HookID {
|
||||
self.hook_id
|
||||
}
|
||||
|
||||
/// Returns the destination path used for packets emitted through this context.
|
||||
pub fn reply_path(&self) -> &[u32] {
|
||||
&self.reply_path
|
||||
}
|
||||
|
||||
/// Queues a one-byte-opcode frame without closing the hook.
|
||||
pub fn send(&mut self, opcode: u8, data: &[u8]) {
|
||||
self.send_frame(opcode, data, false);
|
||||
}
|
||||
|
||||
/// Queues a one-byte-opcode frame that closes the hook after successful routing.
|
||||
pub fn send_final(&mut self, opcode: u8, data: &[u8]) {
|
||||
self.send_frame(opcode, data, true);
|
||||
}
|
||||
|
||||
/// Queues a protocol-specific error frame without closing the hook.
|
||||
///
|
||||
/// The `code` is used as the frame opcode because the protocol layer does not
|
||||
/// reserve a universal error opcode. Leaves that have a dedicated error opcode can
|
||||
/// pass that value here or call [`Self::send`] directly.
|
||||
pub fn error(&mut self, code: u8, data: &[u8]) {
|
||||
self.send(code, data);
|
||||
}
|
||||
|
||||
/// Queues a protocol-specific error frame that closes the hook after routing.
|
||||
pub fn error_final(&mut self, code: u8, data: &[u8]) {
|
||||
self.send_final(code, data);
|
||||
}
|
||||
|
||||
/// Queues raw packet data without adding an opcode byte.
|
||||
pub fn send_raw(&mut self, data: &[u8]) {
|
||||
self.send_raw_with_end(data, false);
|
||||
}
|
||||
|
||||
/// Queues raw packet data and closes the hook after successful routing.
|
||||
pub fn send_raw_final(&mut self, data: &[u8]) {
|
||||
self.send_raw_with_end(data, true);
|
||||
}
|
||||
|
||||
fn send_frame(&mut self, opcode: u8, data: &[u8], end_hook: bool) {
|
||||
let mut frame = Vec::with_capacity(data.len() + 1);
|
||||
frame.push(opcode);
|
||||
frame.extend_from_slice(data);
|
||||
self.enqueue_data(frame, end_hook);
|
||||
}
|
||||
|
||||
fn send_raw_with_end(&mut self, data: &[u8], end_hook: bool) {
|
||||
self.enqueue_data(data.to_vec(), end_hook);
|
||||
}
|
||||
|
||||
fn enqueue_data(&mut self, data: Vec<u8>, end_hook: bool) {
|
||||
self.outbox.push_back(Packet {
|
||||
hook_id: self.hook_id,
|
||||
end_hook,
|
||||
path: self.reply_path.clone(),
|
||||
procedure_id: self.procedure_id,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Output accumulator passed to [`Procedure::handle`].
|
||||
pub struct ProcedureOut {
|
||||
hook_id: HookID,
|
||||
reply_path: Vec<u32>,
|
||||
procedure_id: u32,
|
||||
outbox: PacketQueue,
|
||||
}
|
||||
|
||||
impl ProcedureOut {
|
||||
/// Creates an empty procedure output queue.
|
||||
pub fn new(hook_id: HookID, reply_path: Vec<u32>, procedure_id: u32) -> Self {
|
||||
Self {
|
||||
hook_id,
|
||||
reply_path,
|
||||
procedure_id,
|
||||
outbox: PacketQueue::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the response path used by later [`Self::send`] calls.
|
||||
pub fn set_reply_path(&mut self, reply_path: Vec<u32>) {
|
||||
self.reply_path = reply_path;
|
||||
}
|
||||
|
||||
/// Queues raw response data without closing the hook.
|
||||
pub fn send(&mut self, data: &[u8]) {
|
||||
self.send_with_end(data, false);
|
||||
}
|
||||
|
||||
/// Queues raw response data that closes the hook after successful routing.
|
||||
pub fn send_final(&mut self, data: &[u8]) {
|
||||
self.send_with_end(data, true);
|
||||
}
|
||||
|
||||
/// Consumes the output accumulator and returns packets for generated retry logic.
|
||||
pub fn into_packets(self) -> PacketQueue {
|
||||
self.outbox
|
||||
}
|
||||
|
||||
fn send_with_end(&mut self, data: &[u8], end_hook: bool) {
|
||||
self.outbox.push_back(Packet {
|
||||
hook_id: self.hook_id,
|
||||
end_hook,
|
||||
path: self.reply_path.clone(),
|
||||
procedure_id: self.procedure_id,
|
||||
data: data.to_vec(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Storage entry used by macro-generated session stores.
|
||||
///
|
||||
/// The fields are public so generated code in downstream crates can keep the update
|
||||
/// loop straightforward and static. Handwritten leaves may also use this type, but it
|
||||
/// is intentionally small rather than a full session framework.
|
||||
pub struct SessionEntry<S> {
|
||||
/// Hook id associated with this live session.
|
||||
pub hook_id: HookID,
|
||||
|
||||
/// Application-owned session state.
|
||||
pub state: S,
|
||||
|
||||
/// Packets delivered for this hook but not yet consumed by the session.
|
||||
pub inbox: PacketQueue,
|
||||
|
||||
/// Packets emitted by the session but not yet accepted by endpoint routing.
|
||||
pub outbox: PacketQueue,
|
||||
|
||||
/// Whether application logic has finished and only retry flushing may remain.
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
impl<S> SessionEntry<S> {
|
||||
/// Creates one active session entry for `hook_id`.
|
||||
pub fn new(hook_id: HookID, state: S) -> Self {
|
||||
Self {
|
||||
hook_id,
|
||||
state,
|
||||
inbox: PacketQueue::new(),
|
||||
outbox: PacketQueue::new(),
|
||||
closed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Flushes a retry queue through [`Endpoint::add_outbound`].
|
||||
///
|
||||
/// The packet at the front is cloned for each attempt and removed only after routing
|
||||
/// succeeds. This preserves final frames when a route is temporarily unavailable.
|
||||
/// The return value is true when the queue was fully drained.
|
||||
pub fn flush_packet_queue(endpoint: &mut Endpoint, outbox: &mut PacketQueue) -> bool {
|
||||
while let Some(packet) = outbox.front().cloned() {
|
||||
if endpoint.add_outbound(packet).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
outbox.pop_front();
|
||||
}
|
||||
|
||||
true
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
fn get_meta(&self) -> LeafMeta;
|
||||
|
||||
#[cfg(feature = "interface_ratatui")]
|
||||
fn render_ratatui(&mut self, _: &mut ratatui::Frame<'_>, _: ratatui::layout::Rect) {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
pub struct LeafMeta {
|
||||
pub name: &'static str,
|
||||
pub identifier: &'static str,
|
||||
pub version: &'static str,
|
||||
pub authors: Vec<&'static str>,
|
||||
}
|
||||
+7
-1
@@ -1,12 +1,18 @@
|
||||
mod endpoint;
|
||||
mod error;
|
||||
mod leaf;
|
||||
mod leaf_meta;
|
||||
mod packet;
|
||||
mod procedure;
|
||||
mod session;
|
||||
|
||||
pub use endpoint::{Endpoint, HookID};
|
||||
pub use error::*;
|
||||
pub use leaf::*;
|
||||
pub use leaf::Leaf;
|
||||
pub use leaf_meta::LeafMeta;
|
||||
pub use packet::Packet;
|
||||
pub use procedure::*;
|
||||
pub use session::*;
|
||||
pub use unshell_macros::unshell_leaf;
|
||||
|
||||
// Various named types used for brevity
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::protocol::{Endpoint, HookID, Packet, PacketQueue};
|
||||
|
||||
/// Contract implemented by one generated one-packet procedure handler.
|
||||
///
|
||||
/// Procedures are for stateless or short-lived operations such as ping, capabilities,
|
||||
/// or health checks. Long-running conversations should use [`Session`] so final
|
||||
/// packet cleanup and retries remain tied to hook state.
|
||||
pub trait Procedure<L> {
|
||||
/// Outer packet procedure id handled by this procedure.
|
||||
const PROCEDURE_ID: u32;
|
||||
|
||||
/// Handles one packet and optionally queues response packets in `out`.
|
||||
fn handle(leaf: &mut L, endpoint: &mut Endpoint, packet: Packet, out: &mut ProcedureOut);
|
||||
}
|
||||
|
||||
/// Output accumulator passed to [`Procedure::handle`].
|
||||
pub struct ProcedureOut {
|
||||
hook_id: HookID,
|
||||
reply_path: Vec<u32>,
|
||||
procedure_id: u32,
|
||||
outbox: PacketQueue,
|
||||
}
|
||||
|
||||
impl ProcedureOut {
|
||||
/// Creates an empty procedure output queue.
|
||||
pub fn new(hook_id: HookID, reply_path: Vec<u32>, procedure_id: u32) -> Self {
|
||||
Self {
|
||||
hook_id,
|
||||
reply_path,
|
||||
procedure_id,
|
||||
outbox: PacketQueue::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the response path used by later [`Self::send`] calls.
|
||||
pub fn set_reply_path(&mut self, reply_path: Vec<u32>) {
|
||||
self.reply_path = reply_path;
|
||||
}
|
||||
|
||||
/// Queues raw response data without closing the hook.
|
||||
pub fn send(&mut self, data: &[u8]) {
|
||||
self.send_with_end(data, false);
|
||||
}
|
||||
|
||||
/// Queues raw response data that closes the hook after successful routing.
|
||||
pub fn send_final(&mut self, data: &[u8]) {
|
||||
self.send_with_end(data, true);
|
||||
}
|
||||
|
||||
/// Consumes the output accumulator and returns packets for generated retry logic.
|
||||
pub fn into_packets(self) -> PacketQueue {
|
||||
self.outbox
|
||||
}
|
||||
|
||||
fn send_with_end(&mut self, data: &[u8], end_hook: bool) {
|
||||
self.outbox.push_back(Packet {
|
||||
hook_id: self.hook_id,
|
||||
end_hook,
|
||||
path: self.reply_path.clone(),
|
||||
procedure_id: self.procedure_id,
|
||||
data: data.to_vec(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::protocol::{Endpoint, HookID, Packet, PacketQueue};
|
||||
|
||||
/// Contract implemented by one hook-backed generated session family.
|
||||
///
|
||||
/// A session family maps one outer `procedure_id` to many live hook instances. The
|
||||
/// generated leaf owns packet grouping, retry-safe output flushing, and final cleanup;
|
||||
/// the session implementation owns only application behavior.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// impl Session<MyLeafState> for MySession {
|
||||
/// const PROCEDURE_ID: u32 = 7;
|
||||
/// type State = MySessionState;
|
||||
///
|
||||
/// fn reply_path(state: &Self::State) -> &[u32] {
|
||||
/// &state.reply_path
|
||||
/// }
|
||||
///
|
||||
/// fn init(
|
||||
/// leaf: &mut MyLeafState,
|
||||
/// packet: Packet,
|
||||
/// ctx: &mut SessionInit,
|
||||
/// ) -> SessionInitResult<Self::State> {
|
||||
/// SessionInitResult::Created(MySessionState::from_open(leaf, packet, ctx))
|
||||
/// }
|
||||
///
|
||||
/// fn update(
|
||||
/// leaf: &mut MyLeafState,
|
||||
/// session: &mut Self::State,
|
||||
/// incoming: &mut PacketQueue,
|
||||
/// ctx: &mut SessionCtx<'_>,
|
||||
/// ) -> SessionStatus {
|
||||
/// while let Some(packet) = incoming.pop_front() {
|
||||
/// session.apply(leaf, packet, ctx);
|
||||
/// }
|
||||
/// SessionStatus::Running
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait Session<L> {
|
||||
/// Outer packet procedure id used by every packet in this session family.
|
||||
const PROCEDURE_ID: u32;
|
||||
|
||||
/// Application state stored for one live hook.
|
||||
type State;
|
||||
|
||||
/// Returns the destination path for responses emitted by this session.
|
||||
///
|
||||
/// `Packet` currently carries only a destination path, so protocols that need to
|
||||
/// reply to a caller should capture a reply path during [`Self::init`]. The
|
||||
/// generated leaf clones this path into [`SessionCtx`] before calling update so
|
||||
/// session code can mutably borrow its state while emitting frames.
|
||||
fn reply_path(session: &Self::State) -> &[u32];
|
||||
|
||||
/// Creates one session state from a packet whose hook has no active session.
|
||||
///
|
||||
/// Returning [`SessionInitResult::RejectedWith`] lets the generated leaf route a
|
||||
/// protocol-level failure response with the same retry guarantees as normal
|
||||
/// output. Returning [`SessionInitResult::Rejected`] silently consumes the packet.
|
||||
fn init(leaf: &mut L, packet: Packet, ctx: &mut SessionInit) -> SessionInitResult<Self::State>;
|
||||
|
||||
/// Advances one active hook session.
|
||||
///
|
||||
/// The generated leaf calls this for every live session on each update tick so
|
||||
/// sessions can poll external workers even when no new packet arrived. Outbound
|
||||
/// packets must be queued through `ctx`; direct endpoint routing would bypass the
|
||||
/// generated retry rules.
|
||||
fn update(
|
||||
leaf: &mut L,
|
||||
session: &mut Self::State,
|
||||
incoming: &mut PacketQueue,
|
||||
ctx: &mut SessionCtx<'_>,
|
||||
) -> SessionStatus;
|
||||
}
|
||||
|
||||
/// Context passed to [`Session::init`].
|
||||
///
|
||||
/// This carries routing metadata that the generated leaf already knows before the
|
||||
/// session state exists. Protocols that need source paths should encode them in the
|
||||
/// packet payload; `packet_path` is the destination path that routed the packet here.
|
||||
pub struct SessionInit {
|
||||
hook_id: HookID,
|
||||
packet_path: Vec<u32>,
|
||||
}
|
||||
|
||||
impl SessionInit {
|
||||
/// Creates initialization metadata for a delivered packet.
|
||||
pub fn new(hook_id: HookID, packet_path: Vec<u32>) -> Self {
|
||||
Self {
|
||||
hook_id,
|
||||
packet_path,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the hook id that will identify the new session.
|
||||
pub fn hook_id(&self) -> HookID {
|
||||
self.hook_id
|
||||
}
|
||||
|
||||
/// Returns the destination path from the packet that reached this leaf.
|
||||
pub fn packet_path(&self) -> &[u32] {
|
||||
&self.packet_path
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of trying to create a session from a packet without an active hook entry.
|
||||
pub enum SessionInitResult<S> {
|
||||
/// A new session was created and should be stored by the generated leaf.
|
||||
Created(S),
|
||||
|
||||
/// The packet was intentionally consumed without creating state or a response.
|
||||
Rejected,
|
||||
|
||||
/// The packet was rejected with a response that the generated leaf must route.
|
||||
RejectedWith(Packet),
|
||||
}
|
||||
|
||||
/// Session lifecycle status returned from [`Session::update`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SessionStatus {
|
||||
/// The session is active and should receive future update ticks.
|
||||
Running,
|
||||
|
||||
/// The session is winding down but still needs future update ticks.
|
||||
Closing,
|
||||
|
||||
/// The session has finished application work.
|
||||
///
|
||||
/// The generated leaf still retains the entry until every queued packet routes
|
||||
/// successfully, which prevents a failed final frame from losing session cleanup.
|
||||
Closed,
|
||||
}
|
||||
|
||||
/// Mutable output context passed to [`Session::update`].
|
||||
///
|
||||
/// The context queues packets only; it never routes them immediately. Centralizing
|
||||
/// routing in generated code is what makes final-frame retries reliable.
|
||||
pub struct SessionCtx<'a> {
|
||||
hook_id: HookID,
|
||||
reply_path: Vec<u32>,
|
||||
procedure_id: u32,
|
||||
outbox: &'a mut PacketQueue,
|
||||
}
|
||||
|
||||
impl<'a> SessionCtx<'a> {
|
||||
/// Creates a context for one session update call.
|
||||
pub fn new(
|
||||
hook_id: HookID,
|
||||
reply_path: Vec<u32>,
|
||||
procedure_id: u32,
|
||||
outbox: &'a mut PacketQueue,
|
||||
) -> Self {
|
||||
Self {
|
||||
hook_id,
|
||||
reply_path,
|
||||
procedure_id,
|
||||
outbox,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the hook id used for packets emitted through this context.
|
||||
pub fn hook_id(&self) -> HookID {
|
||||
self.hook_id
|
||||
}
|
||||
|
||||
/// Returns the destination path used for packets emitted through this context.
|
||||
pub fn reply_path(&self) -> &[u32] {
|
||||
&self.reply_path
|
||||
}
|
||||
|
||||
/// Queues a one-byte-opcode frame without closing the hook.
|
||||
pub fn send(&mut self, opcode: u8, data: &[u8]) {
|
||||
self.send_frame(opcode, data, false);
|
||||
}
|
||||
|
||||
/// Queues a one-byte-opcode frame that closes the hook after successful routing.
|
||||
pub fn send_final(&mut self, opcode: u8, data: &[u8]) {
|
||||
self.send_frame(opcode, data, true);
|
||||
}
|
||||
|
||||
/// Queues a protocol-specific error frame without closing the hook.
|
||||
///
|
||||
/// The `code` is used as the frame opcode because the protocol layer does not
|
||||
/// reserve a universal error opcode. Leaves that have a dedicated error opcode can
|
||||
/// pass that value here or call [`Self::send`] directly.
|
||||
pub fn error(&mut self, code: u8, data: &[u8]) {
|
||||
self.send(code, data);
|
||||
}
|
||||
|
||||
/// Queues a protocol-specific error frame that closes the hook after routing.
|
||||
pub fn error_final(&mut self, code: u8, data: &[u8]) {
|
||||
self.send_final(code, data);
|
||||
}
|
||||
|
||||
/// Queues raw packet data without adding an opcode byte.
|
||||
pub fn send_raw(&mut self, data: &[u8]) {
|
||||
self.send_raw_with_end(data, false);
|
||||
}
|
||||
|
||||
/// Queues raw packet data and closes the hook after successful routing.
|
||||
pub fn send_raw_final(&mut self, data: &[u8]) {
|
||||
self.send_raw_with_end(data, true);
|
||||
}
|
||||
|
||||
fn send_frame(&mut self, opcode: u8, data: &[u8], end_hook: bool) {
|
||||
let mut frame = Vec::with_capacity(data.len() + 1);
|
||||
frame.push(opcode);
|
||||
frame.extend_from_slice(data);
|
||||
self.enqueue_data(frame, end_hook);
|
||||
}
|
||||
|
||||
fn send_raw_with_end(&mut self, data: &[u8], end_hook: bool) {
|
||||
self.enqueue_data(data.to_vec(), end_hook);
|
||||
}
|
||||
|
||||
fn enqueue_data(&mut self, data: Vec<u8>, end_hook: bool) {
|
||||
self.outbox.push_back(Packet {
|
||||
hook_id: self.hook_id,
|
||||
end_hook,
|
||||
path: self.reply_path.clone(),
|
||||
procedure_id: self.procedure_id,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Storage entry used by macro-generated session stores.
|
||||
///
|
||||
/// The fields are public so generated code in downstream crates can keep the update
|
||||
/// loop straightforward and static. Handwritten leaves may also use this type, but it
|
||||
/// is intentionally small rather than a full session framework.
|
||||
pub struct SessionEntry<S> {
|
||||
/// Hook id associated with this live session.
|
||||
pub hook_id: HookID,
|
||||
|
||||
/// Application-owned session state.
|
||||
pub state: S,
|
||||
|
||||
/// Packets delivered for this hook but not yet consumed by the session.
|
||||
pub inbox: PacketQueue,
|
||||
|
||||
/// Packets emitted by the session but not yet accepted by endpoint routing.
|
||||
pub outbox: PacketQueue,
|
||||
|
||||
/// Whether application logic has finished and only retry flushing may remain.
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
impl<S> SessionEntry<S> {
|
||||
/// Creates one active session entry for `hook_id`.
|
||||
pub fn new(hook_id: HookID, state: S) -> Self {
|
||||
Self {
|
||||
hook_id,
|
||||
state,
|
||||
inbox: PacketQueue::new(),
|
||||
outbox: PacketQueue::new(),
|
||||
closed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Flushes a retry queue through [`Endpoint::add_outbound`].
|
||||
///
|
||||
/// The packet at the front is cloned for each attempt and removed only after routing
|
||||
/// succeeds. This preserves final frames when a route is temporarily unavailable.
|
||||
/// The return value is true when the queue was fully drained.
|
||||
pub fn flush_packet_queue(endpoint: &mut Endpoint, outbox: &mut PacketQueue) -> bool {
|
||||
while let Some(packet) = outbox.front().cloned() {
|
||||
if endpoint.add_outbound(packet).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
outbox.pop_front();
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
@@ -5,6 +5,9 @@ use crossbeam_channel::{Receiver, Sender};
|
||||
|
||||
use crate::protocol::{Endpoint, Leaf, Packet};
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
use crate::protocol::LeafMeta;
|
||||
|
||||
use super::{
|
||||
codec::{decode_block_chunk, decode_child_summary, decode_u32},
|
||||
constants::{
|
||||
@@ -96,6 +99,16 @@ impl Leaf for MockConnectionLeaf {
|
||||
LEAF_MOCK_CONNECTION
|
||||
}
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
fn get_meta(&self) -> LeafMeta {
|
||||
LeafMeta {
|
||||
name: "Merke Connection Leaf",
|
||||
identifier: "dev.unshell.test.merkle.connection",
|
||||
version: "v0",
|
||||
authors: vec!["ASTATIN3"],
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, endpoint: &mut Endpoint) {
|
||||
if !self.started {
|
||||
endpoint
|
||||
@@ -126,6 +139,16 @@ impl Leaf for MerkleCallerLeaf {
|
||||
LEAF_MERKLE_CALLER
|
||||
}
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
fn get_meta(&self) -> LeafMeta {
|
||||
LeafMeta {
|
||||
name: "Merke Caller Leaf",
|
||||
identifier: "dev.unshell.test.merkle.caller",
|
||||
version: "v0",
|
||||
authors: vec!["ASTATIN3"],
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, endpoint: &mut Endpoint) {
|
||||
self.receive_responses(endpoint);
|
||||
self.dispatch_next_request(endpoint);
|
||||
@@ -137,6 +160,16 @@ impl Leaf for MerkleRespondentLeaf {
|
||||
LEAF_MERKLE_RESPONDENT
|
||||
}
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
fn get_meta(&self) -> LeafMeta {
|
||||
LeafMeta {
|
||||
name: "Merke Respondent Leaf",
|
||||
identifier: "dev.unshell.test.merkle.respondent",
|
||||
version: "v0",
|
||||
authors: vec!["ASTATIN3"],
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, endpoint: &mut Endpoint) {
|
||||
self.open_stream_from_request(endpoint);
|
||||
self.send_one_response_frame(endpoint);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use crate::protocol::{Endpoint, Leaf, Packet};
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
use crate::protocol::LeafMeta;
|
||||
|
||||
use alloc::{boxed::Box, format, vec, vec::Vec};
|
||||
|
||||
use super::support::{CommsLeaf, ENDPOINT_A, ENDPOINT_B, assert_hook_present, assert_hook_removed};
|
||||
@@ -82,6 +85,16 @@ impl Leaf for StreamCallerLeaf {
|
||||
LEAF_STREAM_CALLER
|
||||
}
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
fn get_meta(&self) -> LeafMeta {
|
||||
LeafMeta {
|
||||
name: "Stream Caller Leaf",
|
||||
identifier: "dev.unshell.test.stream_caller_leaf",
|
||||
version: "v0",
|
||||
authors: vec!["ASTATIN3"],
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, endpoint: &mut Endpoint) {
|
||||
if self.has_run {
|
||||
return;
|
||||
@@ -98,6 +111,16 @@ impl Leaf for StreamRespondentLeaf {
|
||||
LEAF_STREAM_RESPONDENT
|
||||
}
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
fn get_meta(&self) -> LeafMeta {
|
||||
LeafMeta {
|
||||
name: "Stream Respondant Leaf",
|
||||
identifier: "dev.unshell.test.stream_respondent_leaf",
|
||||
version: "v0",
|
||||
authors: vec!["ASTATIN3"],
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, endpoint: &mut Endpoint) {
|
||||
self.open_stream_from_pending_request(endpoint);
|
||||
self.send_next_frame(endpoint);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use crate::protocol::{Endpoint, Leaf, Packet};
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
use crate::protocol::LeafMeta;
|
||||
|
||||
use alloc::{vec, vec::Vec};
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
|
||||
@@ -112,6 +115,16 @@ impl Leaf for ControllerLeaf {
|
||||
LEAF_CONTROLLER
|
||||
}
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
fn get_meta(&self) -> LeafMeta {
|
||||
LeafMeta {
|
||||
name: "Controller Leaf",
|
||||
identifier: "dev.unshell.test.controller_leaf",
|
||||
version: "v0",
|
||||
authors: vec!["ASTATIN3"],
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, endpoint: &mut Endpoint) {
|
||||
if !self.has_run {
|
||||
// The controller starts exactly one request so the end-to-end test can
|
||||
@@ -129,6 +142,16 @@ impl Leaf for CommsLeaf {
|
||||
LEAF_COMMS
|
||||
}
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
fn get_meta(&self) -> LeafMeta {
|
||||
LeafMeta {
|
||||
name: "Comms Leaf",
|
||||
identifier: "dev.unshell.test.comms_leaf",
|
||||
version: "v0",
|
||||
authors: vec!["ASTATIN3"],
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, endpoint: &mut Endpoint) {
|
||||
if !self.started {
|
||||
endpoint
|
||||
@@ -160,6 +183,16 @@ impl Leaf for ResponderLeaf {
|
||||
LEAF_RESPONDER
|
||||
}
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
fn get_meta(&self) -> LeafMeta {
|
||||
LeafMeta {
|
||||
name: "Responder Leaf",
|
||||
identifier: "dev.unshell.test.responder_leaf",
|
||||
version: "v0",
|
||||
authors: vec!["ASTATIN3"],
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, endpoint: &mut Endpoint) {
|
||||
let local_id = endpoint.path.last().cloned().unwrap_or(0);
|
||||
let mut packets = Vec::new();
|
||||
|
||||
Reference in New Issue
Block a user