mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
Redesign interface event ownership.
This commit is contained in:
@@ -106,6 +106,32 @@ Generated leaves receive an optional mutable store during `update_interface`. Th
|
|||||||
helpers create and update the appropriate session/procedure views when packets are
|
helpers create and update the appropriate session/procedure views when packets are
|
||||||
dispatched, sessions update, and outbound routes succeed or fail.
|
dispatched, sessions update, and outbound routes succeed or fail.
|
||||||
|
|
||||||
|
Internally, interface events are target-driven:
|
||||||
|
|
||||||
|
```text
|
||||||
|
generated runtime
|
||||||
|
knows packet owner
|
||||||
|
|
|
||||||
|
v
|
||||||
|
InterfaceTarget::Session(SessionKey)
|
||||||
|
InterfaceTarget::Procedure(ProcedureKey)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
InterfaceStore::record(...)
|
||||||
|
append InterfaceEvent
|
||||||
|
link event index to exactly one view
|
||||||
|
update SessionViewStatus when applicable
|
||||||
|
```
|
||||||
|
|
||||||
|
This is deliberately not inferred from `Packet`. A PTY session packet and a one-shot
|
||||||
|
procedure packet both have `procedure_id` and `hook_id`, but they should not both
|
||||||
|
create session views. The runtime already knows which dispatch branch handled the
|
||||||
|
packet, so that answer is carried into the store.
|
||||||
|
|
||||||
|
Leaf-level retry queues also carry the same owner metadata. That matters because the
|
||||||
|
shared leaf outbox contains both rejected session-init responses and procedure
|
||||||
|
responses. Session-entry outboxes use their surrounding session key directly.
|
||||||
|
|
||||||
Time remains caller-supplied:
|
Time remains caller-supplied:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ pub fn feistel_shuffle(index: u16, seed: u32) -> u16 {
|
|||||||
.rotate_left(rot_amount)
|
.rotate_left(rot_amount)
|
||||||
.wrapping_add(round.wrapping_mul(0x9E3779B9));
|
.wrapping_add(round.wrapping_mul(0x9E3779B9));
|
||||||
|
|
||||||
// Round function F: Simple multiplicative hash mixing R and sub_key
|
// Round function F: Simple multiplicative hash mixing R and sub_key.
|
||||||
// We cast to u32 for multiplication to avoid overflow, then mask back to 8 bits
|
// Casting to u8 keeps the low byte, which is the half-block width here.
|
||||||
let r_u32 = r as u32;
|
let r_u32 = r as u32;
|
||||||
let hash_val = ((r_u32.wrapping_mul(sub_key)) ^ (r_u32 >> 4)) as u8 & 0xFF;
|
let hash_val = ((r_u32.wrapping_mul(sub_key)) ^ (r_u32 >> 4)) as u8;
|
||||||
|
|
||||||
// Feistel step: New L = Old R, New R = Old L XOR F(R, key)
|
// Feistel step: New L = Old R, New R = Old L XOR F(R, key)
|
||||||
let temp = l;
|
let temp = l;
|
||||||
|
|||||||
@@ -1,12 +1,38 @@
|
|||||||
use crate::crypto::feistel_shuffle;
|
use crate::crypto::feistel_shuffle;
|
||||||
|
|
||||||
#[cfg(feature = "counter_shuffle_none")]
|
/// Counter implementation selected by feature flags.
|
||||||
|
///
|
||||||
|
/// Cargo's `--all-features` enables every counter strategy at once, so these cfgs are
|
||||||
|
/// intentionally priority-ordered instead of mutually exclusive aliases. The strongest
|
||||||
|
/// configured shuffle wins: Feistel+LCG, then Feistel, then the linear fallback.
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "counter_shuffle_none",
|
||||||
|
not(any(
|
||||||
|
feature = "counter_shuffle_feistel",
|
||||||
|
feature = "counter_shuffle_feistel_lcg"
|
||||||
|
))
|
||||||
|
))]
|
||||||
pub type Counter = NoShuffle;
|
pub type Counter = NoShuffle;
|
||||||
#[cfg(feature = "counter_shuffle_feistel")]
|
|
||||||
|
/// Counter implementation selected when Feistel is enabled without Feistel+LCG.
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "counter_shuffle_feistel",
|
||||||
|
not(feature = "counter_shuffle_feistel_lcg")
|
||||||
|
))]
|
||||||
pub type Counter = FeistelShuffle;
|
pub type Counter = FeistelShuffle;
|
||||||
|
|
||||||
|
/// Default and strongest counter implementation.
|
||||||
#[cfg(feature = "counter_shuffle_feistel_lcg")]
|
#[cfg(feature = "counter_shuffle_feistel_lcg")]
|
||||||
pub type Counter = FeistelLCGShuffle;
|
pub type Counter = FeistelLCGShuffle;
|
||||||
|
|
||||||
|
/// Fallback used only when all counter shuffle features are disabled.
|
||||||
|
#[cfg(not(any(
|
||||||
|
feature = "counter_shuffle_none",
|
||||||
|
feature = "counter_shuffle_feistel",
|
||||||
|
feature = "counter_shuffle_feistel_lcg"
|
||||||
|
)))]
|
||||||
|
pub type Counter = NoShuffle;
|
||||||
|
|
||||||
const NONCE16_1: u16 = const_random::const_random!(u16);
|
const NONCE16_1: u16 = const_random::const_random!(u16);
|
||||||
const NONCE16_2: u16 = const_random::const_random!(u16);
|
const NONCE16_2: u16 = const_random::const_random!(u16);
|
||||||
const NONCE32: u32 = const_random::const_random!(u32);
|
const NONCE32: u32 = const_random::const_random!(u32);
|
||||||
@@ -27,12 +53,21 @@ impl NoShuffle {
|
|||||||
Self(NONCE16_1)
|
Self(NONCE16_1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is an id generator API, not an iterator: callers need a bare `u16` and no
|
||||||
|
// exhaustion state because the counter intentionally wraps through the full space.
|
||||||
|
#[allow(clippy::should_implement_trait)]
|
||||||
pub fn next(&mut self) -> u16 {
|
pub fn next(&mut self) -> u16 {
|
||||||
self.0 = self.0.wrapping_add(1);
|
self.0 = self.0.wrapping_add(1);
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for NoShuffle {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Shuffle all 16 bit numbers, an actual shuffle
|
/// Shuffle all 16 bit numbers, an actual shuffle
|
||||||
/// But this still stores local values in a linear format
|
/// But this still stores local values in a linear format
|
||||||
pub struct FeistelShuffle(u16, u32);
|
pub struct FeistelShuffle(u16, u32);
|
||||||
@@ -42,12 +77,21 @@ impl FeistelShuffle {
|
|||||||
Self(NONCE16_1, NONCE32)
|
Self(NONCE16_1, NONCE32)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is an id generator API, not an iterator: callers need a bare `u16` and no
|
||||||
|
// exhaustion state because the counter intentionally wraps through the full space.
|
||||||
|
#[allow(clippy::should_implement_trait)]
|
||||||
pub fn next(&mut self) -> u16 {
|
pub fn next(&mut self) -> u16 {
|
||||||
self.0 = self.0.wrapping_add(FEISTEL_STEP);
|
self.0 = self.0.wrapping_add(FEISTEL_STEP);
|
||||||
feistel_shuffle(self.0, self.1)
|
feistel_shuffle(self.0, self.1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for FeistelShuffle {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Linear recursive shuffle,
|
/// Linear recursive shuffle,
|
||||||
/// feeds back into itself and doesn't store the actual state.
|
/// feeds back into itself and doesn't store the actual state.
|
||||||
/// Harder to decompile
|
/// Harder to decompile
|
||||||
@@ -65,6 +109,9 @@ impl FeistelLCGShuffle {
|
|||||||
Self { state: 0, a, c }
|
Self { state: 0, a, c }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is an id generator API, not an iterator: callers need a bare `u16` and no
|
||||||
|
// exhaustion state because the counter intentionally wraps through the full space.
|
||||||
|
#[allow(clippy::should_implement_trait)]
|
||||||
pub fn next(&mut self) -> u16 {
|
pub fn next(&mut self) -> u16 {
|
||||||
// 1. Advance state using LCG (Guarantees single cycle of 65536)
|
// 1. Advance state using LCG (Guarantees single cycle of 65536)
|
||||||
self.state = self.state.wrapping_mul(self.a).wrapping_add(self.c);
|
self.state = self.state.wrapping_mul(self.a).wrapping_add(self.c);
|
||||||
@@ -73,3 +120,9 @@ impl FeistelLCGShuffle {
|
|||||||
feistel_shuffle(self.state, self.a as u32)
|
feistel_shuffle(self.state, self.a as u32)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for FeistelLCGShuffle {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ const fn compress(state: &mut [u32; 8], block: &[u8; 64]) {
|
|||||||
/// Returns the SHA-256 digest of `input` as 32 raw bytes.
|
/// Returns the SHA-256 digest of `input` as 32 raw bytes.
|
||||||
pub const fn sha256(input: &[u8]) -> [u8; 32] {
|
pub const fn sha256(input: &[u8]) -> [u8; 32] {
|
||||||
// Padded length is the next multiple of 64 that fits input + 1 (0x80) + 8 (length).
|
// Padded length is the next multiple of 64 that fits input + 1 (0x80) + 8 (length).
|
||||||
let padded_len = ((input.len() + 9 + 63) / 64) * 64;
|
let padded_len = (input.len() + 9).div_ceil(64) * 64;
|
||||||
let mut state = H;
|
let mut state = H;
|
||||||
let mut block_start = 0;
|
let mut block_start = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,12 @@
|
|||||||
mod event;
|
mod event;
|
||||||
mod key;
|
mod key;
|
||||||
mod store;
|
mod store;
|
||||||
|
mod target;
|
||||||
mod view;
|
mod view;
|
||||||
|
|
||||||
pub use event::{InterfaceEvent, InterfaceEventKind};
|
pub use event::{InterfaceEvent, InterfaceEventKind};
|
||||||
pub use key::{ProcedureKey, SessionKey};
|
pub use key::{ProcedureKey, SessionKey};
|
||||||
pub use store::InterfaceStore;
|
pub use store::InterfaceStore;
|
||||||
pub use view::{ProcedureView, SessionView, SessionViewStatus};
|
pub use view::{ProcedureView, SessionView, SessionViewStatus};
|
||||||
|
|
||||||
|
pub(crate) use target::InterfaceTarget;
|
||||||
|
|||||||
+173
-74
@@ -2,8 +2,8 @@ use alloc::{collections::BTreeMap, vec::Vec};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
interface::{
|
interface::{
|
||||||
InterfaceEvent, InterfaceEventKind, ProcedureKey, ProcedureView, SessionKey, SessionView,
|
InterfaceEvent, InterfaceEventKind, InterfaceTarget, ProcedureKey, ProcedureView,
|
||||||
SessionViewStatus,
|
SessionKey, SessionView, SessionViewStatus,
|
||||||
},
|
},
|
||||||
protocol::{EndpointError, HookID, Packet, SessionStatus},
|
protocol::{EndpointError, HookID, Packet, SessionStatus},
|
||||||
};
|
};
|
||||||
@@ -15,7 +15,6 @@ use crate::{
|
|||||||
/// itself stays with the renderer or application shell so protocol state remains
|
/// itself stays with the renderer or application shell so protocol state remains
|
||||||
/// headless and reusable.
|
/// headless and reusable.
|
||||||
pub struct InterfaceStore {
|
pub struct InterfaceStore {
|
||||||
next_sequence: u64,
|
|
||||||
now_ns: Option<u64>,
|
now_ns: Option<u64>,
|
||||||
events: Vec<InterfaceEvent>,
|
events: Vec<InterfaceEvent>,
|
||||||
sessions: BTreeMap<SessionKey, SessionView>,
|
sessions: BTreeMap<SessionKey, SessionView>,
|
||||||
@@ -26,7 +25,6 @@ impl InterfaceStore {
|
|||||||
/// Creates an empty caller-owned interface store.
|
/// Creates an empty caller-owned interface store.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
next_sequence: 0,
|
|
||||||
now_ns: None,
|
now_ns: None,
|
||||||
events: Vec::new(),
|
events: Vec::new(),
|
||||||
sessions: BTreeMap::new(),
|
sessions: BTreeMap::new(),
|
||||||
@@ -70,30 +68,32 @@ impl InterfaceStore {
|
|||||||
procedure_id: u32,
|
procedure_id: u32,
|
||||||
hook_id: HookID,
|
hook_id: HookID,
|
||||||
) -> &mut SessionView {
|
) -> &mut SessionView {
|
||||||
self.sessions
|
self.session_view_for_key_mut(SessionKey {
|
||||||
.entry(SessionKey {
|
|
||||||
leaf_id,
|
leaf_id,
|
||||||
procedure_id,
|
procedure_id,
|
||||||
hook_id,
|
hook_id,
|
||||||
})
|
})
|
||||||
.or_insert_with(SessionView::new)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns or creates the view for a one-shot procedure family.
|
/// 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 {
|
pub fn procedure_view_mut(&mut self, leaf_id: u32, procedure_id: u32) -> &mut ProcedureView {
|
||||||
self.procedures
|
self.procedure_view_for_key_mut(ProcedureKey {
|
||||||
.entry(ProcedureKey {
|
|
||||||
leaf_id,
|
leaf_id,
|
||||||
procedure_id,
|
procedure_id,
|
||||||
})
|
})
|
||||||
.or_insert_with(ProcedureView::new)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Records a packet delivered to a generated leaf.
|
/// Records a packet delivered to a generated leaf.
|
||||||
pub fn record_inbound(&mut self, leaf_id: u32, packet: &Packet) {
|
pub fn record_inbound(&mut self, leaf_id: u32, packet: &Packet) {
|
||||||
self.push_packet_event(
|
let target = InterfaceTarget::session(leaf_id, packet.procedure_id, packet.hook_id);
|
||||||
leaf_id,
|
self.record_inbound_for(target, packet);
|
||||||
packet,
|
}
|
||||||
|
|
||||||
|
/// Records a packet delivered to a target already known by generated runtime code.
|
||||||
|
pub(crate) fn record_inbound_for(&mut self, target: InterfaceTarget, packet: &Packet) {
|
||||||
|
self.record(
|
||||||
|
target,
|
||||||
|
None,
|
||||||
InterfaceEventKind::Inbound {
|
InterfaceEventKind::Inbound {
|
||||||
packet: packet.clone(),
|
packet: packet.clone(),
|
||||||
},
|
},
|
||||||
@@ -107,14 +107,25 @@ impl InterfaceStore {
|
|||||||
procedure_id: u32,
|
procedure_id: u32,
|
||||||
hook_id: HookID,
|
hook_id: HookID,
|
||||||
) {
|
) {
|
||||||
self.push_session_event(
|
self.record_session_packet_queued_for(InterfaceTarget::session(
|
||||||
leaf_id,
|
leaf_id,
|
||||||
procedure_id,
|
procedure_id,
|
||||||
hook_id,
|
hook_id,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records that a packet was queued for an existing session inbox.
|
||||||
|
pub(crate) fn record_session_packet_queued_for(&mut self, target: InterfaceTarget) {
|
||||||
|
let InterfaceTarget::Session(key) = target else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.record(
|
||||||
|
target,
|
||||||
None,
|
None,
|
||||||
InterfaceEventKind::SessionPacketQueued {
|
InterfaceEventKind::SessionPacketQueued {
|
||||||
procedure_id,
|
procedure_id: key.procedure_id,
|
||||||
hook_id,
|
hook_id: key.hook_id,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -127,14 +138,26 @@ impl InterfaceStore {
|
|||||||
hook_id: HookID,
|
hook_id: HookID,
|
||||||
started_ns: Option<u64>,
|
started_ns: Option<u64>,
|
||||||
) {
|
) {
|
||||||
self.push_session_event(
|
let target = InterfaceTarget::session(leaf_id, procedure_id, hook_id);
|
||||||
leaf_id,
|
self.record_session_created_for(target, started_ns);
|
||||||
procedure_id,
|
}
|
||||||
hook_id,
|
|
||||||
|
/// Records successful creation of a new session state for an explicit target.
|
||||||
|
pub(crate) fn record_session_created_for(
|
||||||
|
&mut self,
|
||||||
|
target: InterfaceTarget,
|
||||||
|
started_ns: Option<u64>,
|
||||||
|
) {
|
||||||
|
let InterfaceTarget::Session(key) = target else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.record(
|
||||||
|
target,
|
||||||
Some(SessionViewStatus::Running),
|
Some(SessionViewStatus::Running),
|
||||||
InterfaceEventKind::SessionCreated {
|
InterfaceEventKind::SessionCreated {
|
||||||
procedure_id,
|
procedure_id: key.procedure_id,
|
||||||
hook_id,
|
hook_id: key.hook_id,
|
||||||
started_ns,
|
started_ns,
|
||||||
finished_ns: self.now_ns,
|
finished_ns: self.now_ns,
|
||||||
},
|
},
|
||||||
@@ -149,14 +172,26 @@ impl InterfaceStore {
|
|||||||
hook_id: HookID,
|
hook_id: HookID,
|
||||||
started_ns: Option<u64>,
|
started_ns: Option<u64>,
|
||||||
) {
|
) {
|
||||||
self.push_session_event(
|
let target = InterfaceTarget::session(leaf_id, procedure_id, hook_id);
|
||||||
leaf_id,
|
self.record_session_rejected_for(target, started_ns);
|
||||||
procedure_id,
|
}
|
||||||
hook_id,
|
|
||||||
|
/// Records rejection of a packet that could not create a session.
|
||||||
|
pub(crate) fn record_session_rejected_for(
|
||||||
|
&mut self,
|
||||||
|
target: InterfaceTarget,
|
||||||
|
started_ns: Option<u64>,
|
||||||
|
) {
|
||||||
|
let InterfaceTarget::Session(key) = target else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.record(
|
||||||
|
target,
|
||||||
Some(SessionViewStatus::Rejected),
|
Some(SessionViewStatus::Rejected),
|
||||||
InterfaceEventKind::SessionRejected {
|
InterfaceEventKind::SessionRejected {
|
||||||
procedure_id,
|
procedure_id: key.procedure_id,
|
||||||
hook_id,
|
hook_id: key.hook_id,
|
||||||
started_ns,
|
started_ns,
|
||||||
finished_ns: self.now_ns,
|
finished_ns: self.now_ns,
|
||||||
},
|
},
|
||||||
@@ -172,14 +207,27 @@ impl InterfaceStore {
|
|||||||
status: SessionStatus,
|
status: SessionStatus,
|
||||||
started_ns: Option<u64>,
|
started_ns: Option<u64>,
|
||||||
) {
|
) {
|
||||||
self.push_session_event(
|
let target = InterfaceTarget::session(leaf_id, procedure_id, hook_id);
|
||||||
leaf_id,
|
self.record_session_update_for(target, status, started_ns);
|
||||||
procedure_id,
|
}
|
||||||
hook_id,
|
|
||||||
|
/// Records one session update tick for an explicit session target.
|
||||||
|
pub(crate) fn record_session_update_for(
|
||||||
|
&mut self,
|
||||||
|
target: InterfaceTarget,
|
||||||
|
status: SessionStatus,
|
||||||
|
started_ns: Option<u64>,
|
||||||
|
) {
|
||||||
|
let InterfaceTarget::Session(key) = target else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.record(
|
||||||
|
target,
|
||||||
Some(SessionViewStatus::from_session_status(status)),
|
Some(SessionViewStatus::from_session_status(status)),
|
||||||
InterfaceEventKind::SessionUpdated {
|
InterfaceEventKind::SessionUpdated {
|
||||||
procedure_id,
|
procedure_id: key.procedure_id,
|
||||||
hook_id,
|
hook_id: key.hook_id,
|
||||||
status,
|
status,
|
||||||
started_ns,
|
started_ns,
|
||||||
finished_ns: self.now_ns,
|
finished_ns: self.now_ns,
|
||||||
@@ -195,11 +243,29 @@ impl InterfaceStore {
|
|||||||
hook_id: HookID,
|
hook_id: HookID,
|
||||||
started_ns: Option<u64>,
|
started_ns: Option<u64>,
|
||||||
) {
|
) {
|
||||||
self.push_procedure_event(
|
self.record_procedure_call_for(
|
||||||
leaf_id,
|
InterfaceTarget::procedure(leaf_id, procedure_id),
|
||||||
procedure_id,
|
hook_id,
|
||||||
|
started_ns,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records one procedure call for an explicit procedure target.
|
||||||
|
pub(crate) fn record_procedure_call_for(
|
||||||
|
&mut self,
|
||||||
|
target: InterfaceTarget,
|
||||||
|
hook_id: HookID,
|
||||||
|
started_ns: Option<u64>,
|
||||||
|
) {
|
||||||
|
let InterfaceTarget::Procedure(key) = target else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.record(
|
||||||
|
target,
|
||||||
|
None,
|
||||||
InterfaceEventKind::ProcedureCalled {
|
InterfaceEventKind::ProcedureCalled {
|
||||||
procedure_id,
|
procedure_id: key.procedure_id,
|
||||||
hook_id,
|
hook_id,
|
||||||
started_ns,
|
started_ns,
|
||||||
finished_ns: self.now_ns,
|
finished_ns: self.now_ns,
|
||||||
@@ -209,9 +275,15 @@ impl InterfaceStore {
|
|||||||
|
|
||||||
/// Records a packet emitted by leaf logic before route retry handling.
|
/// Records a packet emitted by leaf logic before route retry handling.
|
||||||
pub fn record_outbound_queued(&mut self, leaf_id: u32, packet: &Packet) {
|
pub fn record_outbound_queued(&mut self, leaf_id: u32, packet: &Packet) {
|
||||||
self.push_packet_event(
|
let target = InterfaceTarget::session(leaf_id, packet.procedure_id, packet.hook_id);
|
||||||
leaf_id,
|
self.record_outbound_queued_for(target, packet);
|
||||||
packet,
|
}
|
||||||
|
|
||||||
|
/// Records a packet emitted by leaf logic before route retry handling.
|
||||||
|
pub(crate) fn record_outbound_queued_for(&mut self, target: InterfaceTarget, packet: &Packet) {
|
||||||
|
self.record(
|
||||||
|
target,
|
||||||
|
None,
|
||||||
InterfaceEventKind::OutboundQueued {
|
InterfaceEventKind::OutboundQueued {
|
||||||
packet: packet.clone(),
|
packet: packet.clone(),
|
||||||
},
|
},
|
||||||
@@ -220,9 +292,15 @@ impl InterfaceStore {
|
|||||||
|
|
||||||
/// Records a route attempt for a queued outbound packet.
|
/// Records a route attempt for a queued outbound packet.
|
||||||
pub fn record_route_attempt(&mut self, leaf_id: u32, packet: &Packet) {
|
pub fn record_route_attempt(&mut self, leaf_id: u32, packet: &Packet) {
|
||||||
self.push_packet_event(
|
let target = InterfaceTarget::session(leaf_id, packet.procedure_id, packet.hook_id);
|
||||||
leaf_id,
|
self.record_route_attempt_for(target, packet);
|
||||||
packet,
|
}
|
||||||
|
|
||||||
|
/// Records a route attempt for a queued outbound packet.
|
||||||
|
pub(crate) fn record_route_attempt_for(&mut self, target: InterfaceTarget, packet: &Packet) {
|
||||||
|
self.record(
|
||||||
|
target,
|
||||||
|
None,
|
||||||
InterfaceEventKind::RouteAttempt {
|
InterfaceEventKind::RouteAttempt {
|
||||||
packet: packet.clone(),
|
packet: packet.clone(),
|
||||||
},
|
},
|
||||||
@@ -231,9 +309,15 @@ impl InterfaceStore {
|
|||||||
|
|
||||||
/// Records a successful route attempt.
|
/// Records a successful route attempt.
|
||||||
pub fn record_route_success(&mut self, leaf_id: u32, packet: &Packet) {
|
pub fn record_route_success(&mut self, leaf_id: u32, packet: &Packet) {
|
||||||
self.push_packet_event(
|
let target = InterfaceTarget::session(leaf_id, packet.procedure_id, packet.hook_id);
|
||||||
leaf_id,
|
self.record_route_success_for(target, packet);
|
||||||
packet,
|
}
|
||||||
|
|
||||||
|
/// Records a successful route attempt.
|
||||||
|
pub(crate) fn record_route_success_for(&mut self, target: InterfaceTarget, packet: &Packet) {
|
||||||
|
self.record(
|
||||||
|
target,
|
||||||
|
None,
|
||||||
InterfaceEventKind::RouteSuccess {
|
InterfaceEventKind::RouteSuccess {
|
||||||
packet: packet.clone(),
|
packet: packet.clone(),
|
||||||
},
|
},
|
||||||
@@ -242,9 +326,20 @@ impl InterfaceStore {
|
|||||||
|
|
||||||
/// Records a failed route attempt without removing the packet from retry state.
|
/// 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) {
|
pub fn record_route_failure(&mut self, leaf_id: u32, packet: &Packet, error: EndpointError) {
|
||||||
self.push_packet_event(
|
let target = InterfaceTarget::session(leaf_id, packet.procedure_id, packet.hook_id);
|
||||||
leaf_id,
|
self.record_route_failure_for(target, packet, error);
|
||||||
packet,
|
}
|
||||||
|
|
||||||
|
/// Records a failed route attempt without removing the packet from retry state.
|
||||||
|
pub(crate) fn record_route_failure_for(
|
||||||
|
&mut self,
|
||||||
|
target: InterfaceTarget,
|
||||||
|
packet: &Packet,
|
||||||
|
error: EndpointError,
|
||||||
|
) {
|
||||||
|
self.record(
|
||||||
|
target,
|
||||||
|
None,
|
||||||
InterfaceEventKind::RouteFailure {
|
InterfaceEventKind::RouteFailure {
|
||||||
packet: packet.clone(),
|
packet: packet.clone(),
|
||||||
error,
|
error,
|
||||||
@@ -252,21 +347,25 @@ impl InterfaceStore {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_packet_event(&mut self, leaf_id: u32, packet: &Packet, kind: InterfaceEventKind) {
|
fn record(
|
||||||
let index = self.push_event(leaf_id, kind);
|
|
||||||
self.link_packet_event(leaf_id, packet, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_session_event(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
leaf_id: u32,
|
target: InterfaceTarget,
|
||||||
procedure_id: u32,
|
|
||||||
hook_id: HookID,
|
|
||||||
status: Option<SessionViewStatus>,
|
status: Option<SessionViewStatus>,
|
||||||
kind: InterfaceEventKind,
|
kind: InterfaceEventKind,
|
||||||
) {
|
) {
|
||||||
let index = self.push_event(leaf_id, kind);
|
let index = self.push_event(target.leaf_id(), kind);
|
||||||
let view = self.session_view_mut(leaf_id, procedure_id, hook_id);
|
self.link_event(target, status, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn link_event(
|
||||||
|
&mut self,
|
||||||
|
target: InterfaceTarget,
|
||||||
|
status: Option<SessionViewStatus>,
|
||||||
|
index: usize,
|
||||||
|
) {
|
||||||
|
match target {
|
||||||
|
InterfaceTarget::Session(key) => {
|
||||||
|
let view = self.session_view_for_key_mut(key);
|
||||||
|
|
||||||
if let Some(status) = status {
|
if let Some(status) = status {
|
||||||
view.status = status;
|
view.status = status;
|
||||||
@@ -274,21 +373,17 @@ impl InterfaceStore {
|
|||||||
|
|
||||||
view.events.push(index);
|
view.events.push(index);
|
||||||
}
|
}
|
||||||
|
InterfaceTarget::Procedure(key) => {
|
||||||
fn push_procedure_event(&mut self, leaf_id: u32, procedure_id: u32, kind: InterfaceEventKind) {
|
self.procedure_view_for_key_mut(key).events.push(index);
|
||||||
let index = self.push_event(leaf_id, kind);
|
}
|
||||||
self.procedure_view_mut(leaf_id, procedure_id)
|
}
|
||||||
.events
|
|
||||||
.push(index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_event(&mut self, leaf_id: u32, kind: InterfaceEventKind) -> usize {
|
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();
|
let index = self.events.len();
|
||||||
|
|
||||||
self.events.push(InterfaceEvent {
|
self.events.push(InterfaceEvent {
|
||||||
sequence,
|
sequence: index as u64,
|
||||||
time_ns: self.now_ns,
|
time_ns: self.now_ns,
|
||||||
leaf_id,
|
leaf_id,
|
||||||
kind,
|
kind,
|
||||||
@@ -297,10 +392,14 @@ impl InterfaceStore {
|
|||||||
index
|
index
|
||||||
}
|
}
|
||||||
|
|
||||||
fn link_packet_event(&mut self, leaf_id: u32, packet: &Packet, index: usize) {
|
fn session_view_for_key_mut(&mut self, key: SessionKey) -> &mut SessionView {
|
||||||
self.session_view_mut(leaf_id, packet.procedure_id, packet.hook_id)
|
self.sessions.entry(key).or_insert_with(SessionView::new)
|
||||||
.events
|
}
|
||||||
.push(index);
|
|
||||||
|
fn procedure_view_for_key_mut(&mut self, key: ProcedureKey) -> &mut ProcedureView {
|
||||||
|
self.procedures
|
||||||
|
.entry(key)
|
||||||
|
.or_insert_with(ProcedureView::new)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
use crate::{
|
||||||
|
interface::{ProcedureKey, SessionKey},
|
||||||
|
protocol::HookID,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Internal owner for one interface event.
|
||||||
|
///
|
||||||
|
/// The runtime already knows whether a packet belongs to a hook-backed session or a
|
||||||
|
/// one-shot procedure. Keeping that answer explicit avoids reconstructing ownership
|
||||||
|
/// from packet fields later, which is what made procedure packet flow look like fake
|
||||||
|
/// session activity in the previous store implementation.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum InterfaceTarget {
|
||||||
|
/// Event belongs to one hook-backed session instance.
|
||||||
|
Session(SessionKey),
|
||||||
|
|
||||||
|
/// Event belongs to one one-shot procedure family.
|
||||||
|
Procedure(ProcedureKey),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InterfaceTarget {
|
||||||
|
/// Builds a session target from the same pieces exposed by [`SessionKey`].
|
||||||
|
pub(crate) fn session(leaf_id: u32, procedure_id: u32, hook_id: HookID) -> Self {
|
||||||
|
Self::Session(SessionKey {
|
||||||
|
leaf_id,
|
||||||
|
procedure_id,
|
||||||
|
hook_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a procedure target from the same pieces exposed by [`ProcedureKey`].
|
||||||
|
pub(crate) fn procedure(leaf_id: u32, procedure_id: u32) -> Self {
|
||||||
|
Self::Procedure(ProcedureKey {
|
||||||
|
leaf_id,
|
||||||
|
procedure_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the leaf id used on the append-only event record.
|
||||||
|
pub(crate) fn leaf_id(self) -> u32 {
|
||||||
|
match self {
|
||||||
|
Self::Session(key) => key.leaf_id,
|
||||||
|
Self::Procedure(key) => key.leaf_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+140
-40
@@ -1,5 +1,7 @@
|
|||||||
|
use alloc::collections::VecDeque;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
interface::InterfaceStore,
|
interface::{InterfaceStore, InterfaceTarget},
|
||||||
protocol::{
|
protocol::{
|
||||||
Endpoint, Packet, PacketQueue, Procedure, ProcedureOut, Session, SessionCtx, SessionEntry,
|
Endpoint, Packet, PacketQueue, Procedure, ProcedureOut, Session, SessionCtx, SessionEntry,
|
||||||
SessionFamily, SessionInit, SessionInitResult, SessionStatus,
|
SessionFamily, SessionInit, SessionInitResult, SessionStatus,
|
||||||
@@ -12,25 +14,49 @@ use crate::{
|
|||||||
/// session initialization responses and one-shot procedures, both of which need the
|
/// session initialization responses and one-shot procedures, both of which need the
|
||||||
/// same retry semantics as session output without becoming separate framework types.
|
/// same retry semantics as session output without becoming separate framework types.
|
||||||
pub struct LeafOutbox {
|
pub struct LeafOutbox {
|
||||||
packets: PacketQueue,
|
packets: VecDeque<LeafOutboxEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One packet retained by a leaf-level retry queue.
|
||||||
|
///
|
||||||
|
/// Session entry outboxes have an obvious owner from their surrounding session entry.
|
||||||
|
/// Leaf-level outboxes are mixed: rejected session initialization packets and one-shot
|
||||||
|
/// procedure responses both land here. Storing the owner beside the packet keeps route
|
||||||
|
/// logging precise without exposing another public queue type.
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct LeafOutboxEntry {
|
||||||
|
packet: Packet,
|
||||||
|
target: LeafOutboxTarget,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interface owner attached to a leaf-level outbox entry.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum LeafOutboxTarget {
|
||||||
|
/// Compatibility path for packets queued through the public `push`/`extend` API.
|
||||||
|
InferFromPacket,
|
||||||
|
|
||||||
|
/// Runtime-known session or procedure target.
|
||||||
|
Explicit(InterfaceTarget),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LeafOutbox {
|
impl LeafOutbox {
|
||||||
/// Creates an empty leaf-level outbox.
|
/// Creates an empty leaf-level outbox.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
packets: PacketQueue::new(),
|
packets: VecDeque::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds one packet to the retry queue.
|
/// Adds one packet to the retry queue.
|
||||||
pub fn push(&mut self, packet: Packet) {
|
pub fn push(&mut self, packet: Packet) {
|
||||||
self.packets.push_back(packet);
|
self.push_with_target(packet, LeafOutboxTarget::InferFromPacket);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds all packets from `packets` in FIFO order.
|
/// Adds all packets from `packets` in FIFO order.
|
||||||
pub fn extend(&mut self, packets: PacketQueue) {
|
pub fn extend(&mut self, packets: PacketQueue) {
|
||||||
self.packets.extend(packets);
|
for packet in packets {
|
||||||
|
self.push(packet);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the number of queued packets.
|
/// Returns the number of queued packets.
|
||||||
@@ -42,6 +68,22 @@ impl LeafOutbox {
|
|||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.packets.is_empty()
|
self.packets.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds one packet with a runtime-known interface target.
|
||||||
|
pub(crate) fn push_for_target(&mut self, packet: Packet, target: InterfaceTarget) {
|
||||||
|
self.push_with_target(packet, LeafOutboxTarget::Explicit(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds all packets with the same runtime-known interface target.
|
||||||
|
pub(crate) fn extend_for_target(&mut self, packets: PacketQueue, target: InterfaceTarget) {
|
||||||
|
for packet in packets {
|
||||||
|
self.push_for_target(packet, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_with_target(&mut self, packet: Packet, target: LeafOutboxTarget) {
|
||||||
|
self.packets.push_back(LeafOutboxEntry { packet, target });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LeafOutbox {
|
impl Default for LeafOutbox {
|
||||||
@@ -67,9 +109,10 @@ pub fn dispatch_session<L, S>(
|
|||||||
{
|
{
|
||||||
let hook_id = packet.hook_id;
|
let hook_id = packet.hook_id;
|
||||||
let procedure_id = S::PROCEDURE_ID;
|
let procedure_id = S::PROCEDURE_ID;
|
||||||
|
let target = InterfaceTarget::session(leaf_id, procedure_id, hook_id);
|
||||||
|
|
||||||
if let Some(store) = interface.as_mut() {
|
if let Some(store) = interface.as_mut() {
|
||||||
store.record_inbound(leaf_id, &packet);
|
store.record_inbound_for(target, &packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(entry) = family
|
if let Some(entry) = family
|
||||||
@@ -80,7 +123,7 @@ pub fn dispatch_session<L, S>(
|
|||||||
entry.inbox.push_back(packet);
|
entry.inbox.push_back(packet);
|
||||||
|
|
||||||
if let Some(store) = interface.as_mut() {
|
if let Some(store) = interface.as_mut() {
|
||||||
store.record_session_packet_queued(leaf_id, procedure_id, hook_id);
|
store.record_session_packet_queued_for(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -95,21 +138,21 @@ pub fn dispatch_session<L, S>(
|
|||||||
family.entries.push(SessionEntry::new(hook_id, state));
|
family.entries.push(SessionEntry::new(hook_id, state));
|
||||||
|
|
||||||
if let Some(store) = interface.as_mut() {
|
if let Some(store) = interface.as_mut() {
|
||||||
store.record_session_created(leaf_id, procedure_id, hook_id, started_ns);
|
store.record_session_created_for(target, started_ns);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SessionInitResult::Rejected => {
|
SessionInitResult::Rejected => {
|
||||||
if let Some(store) = interface.as_mut() {
|
if let Some(store) = interface.as_mut() {
|
||||||
store.record_session_rejected(leaf_id, procedure_id, hook_id, started_ns);
|
store.record_session_rejected_for(target, started_ns);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SessionInitResult::RejectedWith(packet) => {
|
SessionInitResult::RejectedWith(packet) => {
|
||||||
if let Some(store) = interface.as_mut() {
|
if let Some(store) = interface.as_mut() {
|
||||||
store.record_session_rejected(leaf_id, procedure_id, hook_id, started_ns);
|
store.record_session_rejected_for(target, started_ns);
|
||||||
store.record_outbound_queued(leaf_id, &packet);
|
store.record_outbound_queued_for(target, &packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
outbox.push(packet);
|
outbox.push_for_target(packet, target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,23 +172,26 @@ pub fn update_session_family<L, S>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let started_ns = interface.as_ref().and_then(|store| store.now_ns());
|
let started_ns = interface.as_ref().and_then(|store| store.now_ns());
|
||||||
|
let outbox_start = entry.outbox.len();
|
||||||
let reply_path = S::reply_path(&entry.state).to_vec();
|
let reply_path = S::reply_path(&entry.state).to_vec();
|
||||||
|
let status = {
|
||||||
let mut ctx = SessionCtx::new(
|
let mut ctx = SessionCtx::new(
|
||||||
entry.hook_id,
|
entry.hook_id,
|
||||||
reply_path,
|
reply_path,
|
||||||
S::PROCEDURE_ID,
|
S::PROCEDURE_ID,
|
||||||
&mut entry.outbox,
|
&mut entry.outbox,
|
||||||
);
|
);
|
||||||
let status = S::update(leaf, &mut entry.state, &mut entry.inbox, &mut ctx);
|
|
||||||
|
S::update(leaf, &mut entry.state, &mut entry.inbox, &mut ctx)
|
||||||
|
};
|
||||||
|
let target = InterfaceTarget::session(leaf_id, S::PROCEDURE_ID, entry.hook_id);
|
||||||
|
|
||||||
if let Some(store) = interface.as_mut() {
|
if let Some(store) = interface.as_mut() {
|
||||||
store.record_session_update(
|
store.record_session_update_for(target, status, started_ns);
|
||||||
leaf_id,
|
|
||||||
S::PROCEDURE_ID,
|
for packet in entry.outbox.iter().skip(outbox_start) {
|
||||||
entry.hook_id,
|
store.record_outbound_queued_for(target, packet);
|
||||||
status,
|
}
|
||||||
started_ns,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches!(status, SessionStatus::Closed) {
|
if matches!(status, SessionStatus::Closed) {
|
||||||
@@ -166,9 +212,10 @@ pub fn dispatch_procedure<L, P>(
|
|||||||
P: Procedure<L>,
|
P: Procedure<L>,
|
||||||
{
|
{
|
||||||
let started_ns = interface.as_ref().and_then(|store| store.now_ns());
|
let started_ns = interface.as_ref().and_then(|store| store.now_ns());
|
||||||
|
let target = InterfaceTarget::procedure(leaf_id, P::PROCEDURE_ID);
|
||||||
|
|
||||||
if let Some(store) = interface.as_mut() {
|
if let Some(store) = interface.as_mut() {
|
||||||
store.record_inbound(leaf_id, &packet);
|
store.record_inbound_for(target, &packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
let hook_id = packet.hook_id;
|
let hook_id = packet.hook_id;
|
||||||
@@ -180,14 +227,14 @@ pub fn dispatch_procedure<L, P>(
|
|||||||
let packets = procedure_out.into_packets();
|
let packets = procedure_out.into_packets();
|
||||||
|
|
||||||
if let Some(store) = interface.as_mut() {
|
if let Some(store) = interface.as_mut() {
|
||||||
store.record_procedure_call(leaf_id, P::PROCEDURE_ID, hook_id, started_ns);
|
store.record_procedure_call_for(target, hook_id, started_ns);
|
||||||
|
|
||||||
for packet in &packets {
|
for packet in &packets {
|
||||||
store.record_outbound_queued(leaf_id, packet);
|
store.record_outbound_queued_for(target, packet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outbox.extend(packets);
|
outbox.extend_for_target(packets, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flushes a generated leaf-level outbox through endpoint routing.
|
/// Flushes a generated leaf-level outbox through endpoint routing.
|
||||||
@@ -197,7 +244,17 @@ pub fn flush_leaf_outbox(
|
|||||||
outbox: &mut LeafOutbox,
|
outbox: &mut LeafOutbox,
|
||||||
interface: &mut Option<&mut InterfaceStore>,
|
interface: &mut Option<&mut InterfaceStore>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
flush_packet_queue_with_interface(endpoint, leaf_id, &mut outbox.packets, interface)
|
while let Some(entry) = outbox.packets.front().cloned() {
|
||||||
|
let target = resolve_leaf_outbox_target(leaf_id, &entry);
|
||||||
|
|
||||||
|
if !flush_packet_with_target(endpoint, target, &entry.packet, interface) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
outbox.packets.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flushes and retains one generated session family.
|
/// Flushes and retains one generated session family.
|
||||||
@@ -210,7 +267,8 @@ pub fn flush_session_family<L, S>(
|
|||||||
S: Session<L>,
|
S: Session<L>,
|
||||||
{
|
{
|
||||||
for entry in &mut family.entries {
|
for entry in &mut family.entries {
|
||||||
flush_packet_queue_with_interface(endpoint, leaf_id, &mut entry.outbox, interface);
|
let target = InterfaceTarget::session(leaf_id, S::PROCEDURE_ID, entry.hook_id);
|
||||||
|
flush_packet_queue_with_target(endpoint, target, &mut entry.outbox, interface);
|
||||||
}
|
}
|
||||||
|
|
||||||
family
|
family
|
||||||
@@ -230,30 +288,72 @@ pub fn flush_packet_queue_with_interface(
|
|||||||
interface: &mut Option<&mut InterfaceStore>,
|
interface: &mut Option<&mut InterfaceStore>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
while let Some(packet) = outbox.front().cloned() {
|
while let Some(packet) = outbox.front().cloned() {
|
||||||
|
let target = InterfaceTarget::session(leaf_id, packet.procedure_id, packet.hook_id);
|
||||||
|
|
||||||
|
if !flush_packet_with_target(endpoint, target, &packet, interface) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
outbox.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flushes a packet queue whose owner is already known by the generated runtime.
|
||||||
|
fn flush_packet_queue_with_target(
|
||||||
|
endpoint: &mut Endpoint,
|
||||||
|
target: InterfaceTarget,
|
||||||
|
outbox: &mut PacketQueue,
|
||||||
|
interface: &mut Option<&mut InterfaceStore>,
|
||||||
|
) -> bool {
|
||||||
|
while let Some(packet) = outbox.front().cloned() {
|
||||||
|
if !flush_packet_with_target(endpoint, target, &packet, interface) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
outbox.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush_packet_with_target(
|
||||||
|
endpoint: &mut Endpoint,
|
||||||
|
target: InterfaceTarget,
|
||||||
|
packet: &Packet,
|
||||||
|
interface: &mut Option<&mut InterfaceStore>,
|
||||||
|
) -> bool {
|
||||||
if let Some(store) = interface.as_mut() {
|
if let Some(store) = interface.as_mut() {
|
||||||
store.record_route_attempt(leaf_id, &packet);
|
store.record_route_attempt_for(target, packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
match endpoint.add_outbound(packet.clone()) {
|
match endpoint.add_outbound(packet.clone()) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
if let Some(store) = interface.as_mut() {
|
if let Some(store) = interface.as_mut() {
|
||||||
store.record_route_success(leaf_id, &packet);
|
store.record_route_success_for(target, packet);
|
||||||
}
|
|
||||||
|
|
||||||
outbox.pop_front();
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
if let Some(store) = interface.as_mut() {
|
|
||||||
store.record_route_failure(leaf_id, &packet, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
Err(error) => {
|
||||||
|
if let Some(store) = interface.as_mut() {
|
||||||
|
store.record_route_failure_for(target, packet, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_leaf_outbox_target(leaf_id: u32, entry: &LeafOutboxEntry) -> InterfaceTarget {
|
||||||
|
match entry.target {
|
||||||
|
LeafOutboxTarget::InferFromPacket => {
|
||||||
|
InterfaceTarget::session(leaf_id, entry.packet.procedure_id, entry.packet.hook_id)
|
||||||
|
}
|
||||||
|
LeafOutboxTarget::Explicit(target) => target,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the path used by generated procedure responses.
|
/// Returns the path used by generated procedure responses.
|
||||||
fn parent_reply_path(endpoint: &Endpoint) -> alloc::vec::Vec<u32> {
|
fn parent_reply_path(endpoint: &Endpoint) -> alloc::vec::Vec<u32> {
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ pub const LEAF_FAKE_PTY: u32 = hash_32!("dev.unshell.v1.pty");
|
|||||||
/// Outer procedure id used by all fake PTY session packets.
|
/// Outer procedure id used by all fake PTY session packets.
|
||||||
pub const PROC_PTY: u32 = hash_32!("dev.unshell.v1.pty.pty");
|
pub const PROC_PTY: u32 = hash_32!("dev.unshell.v1.pty.pty");
|
||||||
|
|
||||||
|
/// One-shot procedure id used by tests to prove procedure interface ownership.
|
||||||
|
pub(crate) const PROC_PING: u32 = hash_32!("dev.unshell.v1.pty.ping");
|
||||||
|
|
||||||
/// Downward opcode that opens one PTY session.
|
/// Downward opcode that opens one PTY session.
|
||||||
pub const OP_OPEN: u8 = 0;
|
pub const OP_OPEN: u8 = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ extern crate alloc;
|
|||||||
|
|
||||||
mod codec;
|
mod codec;
|
||||||
mod constants;
|
mod constants;
|
||||||
|
mod procedure;
|
||||||
mod session;
|
mod session;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
use unshell::protocol::{Endpoint, Packet, Procedure, ProcedureOut};
|
||||||
|
|
||||||
|
use crate::{constants::PROC_PING, state::FakePtyState};
|
||||||
|
|
||||||
|
/// One-shot echo procedure used to exercise generated procedure dispatch.
|
||||||
|
///
|
||||||
|
/// The fake PTY leaf is primarily session-oriented, so this deliberately small
|
||||||
|
/// procedure gives tests a non-session packet family. That keeps interface logging
|
||||||
|
/// honest: procedure packets should populate [`unshell::interface::ProcedureView`]
|
||||||
|
/// instead of being inferred as hook-backed sessions.
|
||||||
|
pub(crate) struct PingProcedure;
|
||||||
|
|
||||||
|
impl Procedure<FakePtyState> for PingProcedure {
|
||||||
|
const PROCEDURE_ID: u32 = PROC_PING;
|
||||||
|
|
||||||
|
fn handle(_: &mut FakePtyState, _: &mut Endpoint, packet: Packet, out: &mut ProcedureOut) {
|
||||||
|
out.send_final(&packet.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use unshell::protocol::{HookID, unshell_leaf};
|
use unshell::protocol::{HookID, unshell_leaf};
|
||||||
|
|
||||||
use crate::{constants::LEAF_FAKE_PTY, session::PtySession};
|
use crate::{constants::LEAF_FAKE_PTY, procedure::PingProcedure, session::PtySession};
|
||||||
|
|
||||||
/// User-owned state for the generated fake PTY leaf.
|
/// User-owned state for the generated fake PTY leaf.
|
||||||
///
|
///
|
||||||
@@ -47,6 +47,8 @@ unshell_leaf! {
|
|||||||
sessions {
|
sessions {
|
||||||
pty: PtySession,
|
pty: PtySession,
|
||||||
}
|
}
|
||||||
procedures {}
|
procedures {
|
||||||
|
ping: PingProcedure,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
use alloc::vec;
|
||||||
|
|
||||||
|
use unshell::{
|
||||||
|
interface::{InterfaceEventKind, InterfaceStore, ProcedureKey, SessionKey, SessionViewStatus},
|
||||||
|
protocol::{Leaf, Packet},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
FakePtyLeaf, FakePtyState, OP_EXIT, OP_OPENED, OP_TERMINATE, PROC_PTY, constants::PROC_PING,
|
||||||
|
frame_opcode, pty_open_packet,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::support::{
|
||||||
|
ENDPOINT_A, ENDPOINT_B, assert_frame, drain_parent_packets, drain_parent_pty_packets,
|
||||||
|
pty_endpoints, send_downward_frame, transfer_packets,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn view_has_event<F>(interface: &InterfaceStore, event_indexes: &[usize], mut predicate: F) -> bool
|
||||||
|
where
|
||||||
|
F: FnMut(&InterfaceEventKind) -> bool,
|
||||||
|
{
|
||||||
|
event_indexes
|
||||||
|
.iter()
|
||||||
|
.any(|index| predicate(&interface.events()[*index].kind))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_downward_ping(
|
||||||
|
endpoint_a: &mut unshell::protocol::Endpoint,
|
||||||
|
endpoint_b: &mut unshell::protocol::Endpoint,
|
||||||
|
hook_id: u16,
|
||||||
|
payload: &[u8],
|
||||||
|
) {
|
||||||
|
endpoint_a
|
||||||
|
.add_outbound(Packet {
|
||||||
|
hook_id,
|
||||||
|
end_hook: false,
|
||||||
|
path: vec![ENDPOINT_A, ENDPOINT_B],
|
||||||
|
procedure_id: PROC_PING,
|
||||||
|
data: payload.to_vec(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
transfer_packets(endpoint_a, endpoint_b, ENDPOINT_B, ENDPOINT_A);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
|
||||||
|
let session_key = SessionKey {
|
||||||
|
leaf_id: leaf.get_id(),
|
||||||
|
procedure_id: PROC_PTY,
|
||||||
|
hook_id,
|
||||||
|
};
|
||||||
|
let session_view = interface.session_views().get(&session_key).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(leaf.active_session_count(), 1);
|
||||||
|
assert!(view_has_event(
|
||||||
|
&interface,
|
||||||
|
&session_view.events,
|
||||||
|
|event| matches!(
|
||||||
|
event,
|
||||||
|
InterfaceEventKind::SessionCreated { hook_id: recorded_hook, .. }
|
||||||
|
if *recorded_hook == hook_id
|
||||||
|
),
|
||||||
|
));
|
||||||
|
assert!(view_has_event(
|
||||||
|
&interface,
|
||||||
|
&session_view.events,
|
||||||
|
|event| matches!(
|
||||||
|
event,
|
||||||
|
InterfaceEventKind::OutboundQueued { packet }
|
||||||
|
if packet.hook_id == hook_id && frame_opcode(packet) == Some(OP_OPENED)
|
||||||
|
),
|
||||||
|
));
|
||||||
|
assert!(view_has_event(
|
||||||
|
&interface,
|
||||||
|
&session_view.events,
|
||||||
|
|event| matches!(
|
||||||
|
event,
|
||||||
|
InterfaceEventKind::RouteSuccess { packet }
|
||||||
|
if packet.hook_id == hook_id && frame_opcode(packet) == Some(OP_OPENED)
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn interface_update_records_failed_final_route_without_dropping_session() {
|
||||||
|
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);
|
||||||
|
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
||||||
|
drain_parent_pty_packets(&mut endpoint_a);
|
||||||
|
|
||||||
|
send_downward_frame(
|
||||||
|
&mut endpoint_a,
|
||||||
|
&mut endpoint_b,
|
||||||
|
hook_id,
|
||||||
|
OP_TERMINATE,
|
||||||
|
&[],
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
endpoint_b.connections.remove(&(ENDPOINT_A, true));
|
||||||
|
leaf.update_interface(&mut endpoint_b, &mut interface);
|
||||||
|
|
||||||
|
let session_key = SessionKey {
|
||||||
|
leaf_id: leaf.get_id(),
|
||||||
|
procedure_id: PROC_PTY,
|
||||||
|
hook_id,
|
||||||
|
};
|
||||||
|
let session_view = interface.session_views().get(&session_key).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(leaf.active_session_count(), 1);
|
||||||
|
assert_eq!(leaf.pending_packet_count(), 1);
|
||||||
|
assert_eq!(session_view.status, SessionViewStatus::Closed);
|
||||||
|
assert!(view_has_event(
|
||||||
|
&interface,
|
||||||
|
&session_view.events,
|
||||||
|
|event| matches!(
|
||||||
|
event,
|
||||||
|
InterfaceEventKind::RouteFailure { packet, .. }
|
||||||
|
if packet.hook_id == hook_id && frame_opcode(packet) == Some(OP_EXIT)
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
endpoint_b.connections.insert((ENDPOINT_A, true));
|
||||||
|
leaf.update_interface(&mut endpoint_b, &mut interface);
|
||||||
|
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
||||||
|
let packets = drain_parent_pty_packets(&mut endpoint_a);
|
||||||
|
|
||||||
|
let session_view = interface.session_views().get(&session_key).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(leaf.active_session_count(), 0);
|
||||||
|
assert_eq!(packets.len(), 1);
|
||||||
|
assert_frame(&packets[0], hook_id, OP_EXIT, true, &[0]);
|
||||||
|
assert!(view_has_event(
|
||||||
|
&interface,
|
||||||
|
&session_view.events,
|
||||||
|
|event| matches!(
|
||||||
|
event,
|
||||||
|
InterfaceEventKind::RouteSuccess { packet }
|
||||||
|
if packet.hook_id == hook_id && frame_opcode(packet) == Some(OP_EXIT)
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn interface_update_records_procedure_flow_without_session_view() {
|
||||||
|
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();
|
||||||
|
|
||||||
|
send_downward_ping(&mut endpoint_a, &mut endpoint_b, hook_id, b"ping");
|
||||||
|
leaf.update_interface(&mut endpoint_b, &mut interface);
|
||||||
|
|
||||||
|
let leaf_id = leaf.get_id();
|
||||||
|
let procedure_key = ProcedureKey {
|
||||||
|
leaf_id,
|
||||||
|
procedure_id: PROC_PING,
|
||||||
|
};
|
||||||
|
let session_key = SessionKey {
|
||||||
|
leaf_id,
|
||||||
|
procedure_id: PROC_PING,
|
||||||
|
hook_id,
|
||||||
|
};
|
||||||
|
let procedure_view = interface.procedure_views().get(&procedure_key).unwrap();
|
||||||
|
|
||||||
|
assert!(!interface.session_views().contains_key(&session_key));
|
||||||
|
assert!(view_has_event(
|
||||||
|
&interface,
|
||||||
|
&procedure_view.events,
|
||||||
|
|event| matches!(
|
||||||
|
event,
|
||||||
|
InterfaceEventKind::Inbound { packet }
|
||||||
|
if packet.hook_id == hook_id && packet.procedure_id == PROC_PING
|
||||||
|
),
|
||||||
|
));
|
||||||
|
assert!(view_has_event(
|
||||||
|
&interface,
|
||||||
|
&procedure_view.events,
|
||||||
|
|event| matches!(
|
||||||
|
event,
|
||||||
|
InterfaceEventKind::ProcedureCalled { procedure_id, hook_id: recorded_hook, .. }
|
||||||
|
if *procedure_id == PROC_PING && *recorded_hook == hook_id
|
||||||
|
),
|
||||||
|
));
|
||||||
|
assert!(view_has_event(
|
||||||
|
&interface,
|
||||||
|
&procedure_view.events,
|
||||||
|
|event| matches!(
|
||||||
|
event,
|
||||||
|
InterfaceEventKind::RouteSuccess { packet }
|
||||||
|
if packet.hook_id == hook_id && packet.procedure_id == PROC_PING
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
||||||
|
let packets = drain_parent_packets(&mut endpoint_a, PROC_PING);
|
||||||
|
|
||||||
|
assert_eq!(packets.len(), 1);
|
||||||
|
assert_eq!(packets[0].hook_id, hook_id);
|
||||||
|
assert!(packets[0].end_hook);
|
||||||
|
assert_eq!(packets[0].data, b"ping".to_vec());
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
mod session;
|
||||||
|
mod support;
|
||||||
|
|
||||||
|
#[cfg(feature = "interface")]
|
||||||
|
mod interface;
|
||||||
+10
-224
@@ -1,127 +1,17 @@
|
|||||||
use alloc::{vec, vec::Vec};
|
use alloc::{vec, vec::Vec};
|
||||||
|
|
||||||
use unshell::protocol::{Endpoint, Leaf, Packet};
|
use unshell::protocol::{Leaf, Packet};
|
||||||
|
|
||||||
#[cfg(feature = "interface")]
|
use crate::{
|
||||||
use unshell::interface::{InterfaceEventKind, InterfaceStore, SessionKey, SessionViewStatus};
|
FakePtyLeaf, FakePtyState, OP_ABORT, OP_ERROR, OP_EXIT, OP_INPUT, OP_OUTPUT, OP_STDIN_EOF,
|
||||||
|
OP_TERMINATE, pty_open_packet,
|
||||||
use super::{
|
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ENDPOINT_A: u32 = 0;
|
use super::support::{
|
||||||
const ENDPOINT_B: u32 = 1;
|
ENDPOINT_A, ENDPOINT_B, PROC_OTHER, assert_frame, assert_hook_present, assert_hook_removed,
|
||||||
const PROC_OTHER: u32 = 31;
|
assert_opened, drain_parent_pty_packets, endpoint_at, has_frame, open_pty_session,
|
||||||
|
pty_endpoints, send_downward_frame, transfer_packets,
|
||||||
/// Creates a bare endpoint at a known absolute path.
|
};
|
||||||
fn endpoint_at(id: u32, path: Vec<u32>) -> Endpoint {
|
|
||||||
let mut endpoint = Endpoint::new(id, vec![]);
|
|
||||||
endpoint.path = path;
|
|
||||||
endpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates the parent/child endpoint pair used by PTY session tests.
|
|
||||||
fn pty_endpoints() -> (Endpoint, Endpoint) {
|
|
||||||
let mut endpoint_a = endpoint_at(ENDPOINT_A, vec![ENDPOINT_A]);
|
|
||||||
let mut endpoint_b = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
|
|
||||||
|
|
||||||
endpoint_a.connections.insert((ENDPOINT_B, false));
|
|
||||||
endpoint_b.connections.insert((ENDPOINT_A, true));
|
|
||||||
|
|
||||||
(endpoint_a, endpoint_b)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transfers every queued packet for `next_hop` into `receiver` as `remote_id` traffic.
|
|
||||||
fn transfer_packets(sender: &mut Endpoint, receiver: &mut Endpoint, next_hop: u32, remote_id: u32) {
|
|
||||||
let mut packets = Vec::new();
|
|
||||||
sender.take_outbound_clear(next_hop, |packet| packets.push(packet.clone()));
|
|
||||||
|
|
||||||
for packet in packets {
|
|
||||||
receiver.add_inbound_from(remote_id, packet).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends one downward PTY frame from endpoint A to endpoint B.
|
|
||||||
fn send_downward_frame(
|
|
||||||
endpoint_a: &mut Endpoint,
|
|
||||||
endpoint_b: &mut Endpoint,
|
|
||||||
hook_id: u16,
|
|
||||||
opcode: u8,
|
|
||||||
payload: &[u8],
|
|
||||||
end_hook: bool,
|
|
||||||
) {
|
|
||||||
endpoint_a
|
|
||||||
.add_outbound(pty_packet(
|
|
||||||
vec![ENDPOINT_A, ENDPOINT_B],
|
|
||||||
hook_id,
|
|
||||||
end_hook,
|
|
||||||
opcode,
|
|
||||||
payload,
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
transfer_packets(endpoint_a, endpoint_b, ENDPOINT_B, ENDPOINT_A);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Opens a fake PTY session and delivers the `Opened` response to endpoint A.
|
|
||||||
fn open_pty_session(
|
|
||||||
endpoint_a: &mut Endpoint,
|
|
||||||
endpoint_b: &mut Endpoint,
|
|
||||||
leaf: &mut FakePtyLeaf,
|
|
||||||
) -> u16 {
|
|
||||||
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(endpoint_a, endpoint_b, ENDPOINT_B, ENDPOINT_A);
|
|
||||||
leaf.update(endpoint_b);
|
|
||||||
transfer_packets(endpoint_b, endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
|
||||||
|
|
||||||
hook_id
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Drains PTY packets delivered to endpoint A.
|
|
||||||
fn drain_parent_pty_packets(endpoint: &mut Endpoint) -> Vec<Packet> {
|
|
||||||
let mut packets = Vec::new();
|
|
||||||
endpoint.take_inbound_matching(
|
|
||||||
ENDPOINT_A,
|
|
||||||
|packet| packet.procedure_id == PROC_PTY,
|
|
||||||
|packet| packets.push(packet),
|
|
||||||
);
|
|
||||||
packets
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Asserts that local hook state still contains `hook_id`.
|
|
||||||
fn assert_hook_present(endpoint: &Endpoint, hook_id: u16) {
|
|
||||||
assert!(endpoint.has_hook(hook_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Asserts that local hook state no longer contains `hook_id`.
|
|
||||||
fn assert_hook_removed(endpoint: &Endpoint, hook_id: u16) {
|
|
||||||
assert!(!endpoint.has_hook(hook_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Asserts that `packet` carries the expected PTY frame.
|
|
||||||
fn assert_frame(packet: &Packet, hook_id: u16, opcode: u8, end_hook: bool, payload: &[u8]) {
|
|
||||||
assert_eq!(packet.hook_id, hook_id);
|
|
||||||
assert_eq!(packet.end_hook, end_hook);
|
|
||||||
assert_eq!(frame_opcode(packet), Some(opcode));
|
|
||||||
assert_eq!(frame_payload(packet), payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true when `packets` contains the requested frame.
|
|
||||||
fn has_frame(packets: &[Packet], hook_id: u16, opcode: u8, payload: &[u8]) -> bool {
|
|
||||||
packets.iter().any(|packet| {
|
|
||||||
packet.hook_id == hook_id
|
|
||||||
&& frame_opcode(packet) == Some(opcode)
|
|
||||||
&& frame_payload(packet) == payload
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn open_pty_paves_hook_and_creates_session() {
|
fn open_pty_paves_hook_and_creates_session() {
|
||||||
@@ -137,7 +27,7 @@ fn open_pty_paves_hook_and_creates_session() {
|
|||||||
assert_hook_present(&endpoint_a, hook_id);
|
assert_hook_present(&endpoint_a, hook_id);
|
||||||
assert_hook_present(&endpoint_b, hook_id);
|
assert_hook_present(&endpoint_b, hook_id);
|
||||||
assert_eq!(packets.len(), 1);
|
assert_eq!(packets.len(), 1);
|
||||||
assert_frame(&packets[0], hook_id, OP_OPENED, false, &[]);
|
assert_opened(&packets[0], hook_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -394,107 +284,3 @@ 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)
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "interface")]
|
|
||||||
#[test]
|
|
||||||
fn interface_update_records_failed_final_route_without_dropping_session() {
|
|
||||||
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);
|
|
||||||
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
|
||||||
drain_parent_pty_packets(&mut endpoint_a);
|
|
||||||
|
|
||||||
send_downward_frame(
|
|
||||||
&mut endpoint_a,
|
|
||||||
&mut endpoint_b,
|
|
||||||
hook_id,
|
|
||||||
OP_TERMINATE,
|
|
||||||
&[],
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
endpoint_b.connections.remove(&(ENDPOINT_A, true));
|
|
||||||
leaf.update_interface(&mut endpoint_b, &mut interface);
|
|
||||||
|
|
||||||
let session_key = SessionKey {
|
|
||||||
leaf_id: leaf.get_id(),
|
|
||||||
procedure_id: PROC_PTY,
|
|
||||||
hook_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(leaf.active_session_count(), 1);
|
|
||||||
assert_eq!(leaf.pending_packet_count(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
interface.session_views().get(&session_key).unwrap().status,
|
|
||||||
SessionViewStatus::Closed
|
|
||||||
);
|
|
||||||
assert!(interface.events().iter().any(|event| {
|
|
||||||
matches!(
|
|
||||||
&event.kind,
|
|
||||||
InterfaceEventKind::RouteFailure { packet, .. }
|
|
||||||
if packet.hook_id == hook_id && frame_opcode(packet) == Some(OP_EXIT)
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
|
|
||||||
endpoint_b.connections.insert((ENDPOINT_A, true));
|
|
||||||
leaf.update_interface(&mut endpoint_b, &mut interface);
|
|
||||||
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
|
||||||
let packets = drain_parent_pty_packets(&mut endpoint_a);
|
|
||||||
|
|
||||||
assert_eq!(leaf.active_session_count(), 0);
|
|
||||||
assert_eq!(packets.len(), 1);
|
|
||||||
assert_frame(&packets[0], hook_id, OP_EXIT, true, &[0]);
|
|
||||||
assert!(interface.events().iter().any(|event| {
|
|
||||||
matches!(
|
|
||||||
&event.kind,
|
|
||||||
InterfaceEventKind::RouteSuccess { packet }
|
|
||||||
if packet.hook_id == hook_id && frame_opcode(packet) == Some(OP_EXIT)
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
use alloc::{vec, vec::Vec};
|
||||||
|
|
||||||
|
use unshell::protocol::{Endpoint, Leaf, Packet};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
FakePtyLeaf, OP_OPENED, PROC_PTY, frame_opcode, frame_payload, pty_open_packet, pty_packet,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(super) const ENDPOINT_A: u32 = 0;
|
||||||
|
pub(super) const ENDPOINT_B: u32 = 1;
|
||||||
|
pub(super) const PROC_OTHER: u32 = 31;
|
||||||
|
|
||||||
|
/// Creates a bare endpoint at a known absolute path.
|
||||||
|
pub(super) fn endpoint_at(id: u32, path: Vec<u32>) -> Endpoint {
|
||||||
|
let mut endpoint = Endpoint::new(id, vec![]);
|
||||||
|
endpoint.path = path;
|
||||||
|
endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates the parent/child endpoint pair used by PTY session tests.
|
||||||
|
pub(super) fn pty_endpoints() -> (Endpoint, Endpoint) {
|
||||||
|
let mut endpoint_a = endpoint_at(ENDPOINT_A, vec![ENDPOINT_A]);
|
||||||
|
let mut endpoint_b = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
|
||||||
|
|
||||||
|
endpoint_a.connections.insert((ENDPOINT_B, false));
|
||||||
|
endpoint_b.connections.insert((ENDPOINT_A, true));
|
||||||
|
|
||||||
|
(endpoint_a, endpoint_b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transfers every queued packet for `next_hop` into `receiver` as `remote_id` traffic.
|
||||||
|
pub(super) fn transfer_packets(
|
||||||
|
sender: &mut Endpoint,
|
||||||
|
receiver: &mut Endpoint,
|
||||||
|
next_hop: u32,
|
||||||
|
remote_id: u32,
|
||||||
|
) {
|
||||||
|
let mut packets = Vec::new();
|
||||||
|
sender.take_outbound_clear(next_hop, |packet| packets.push(packet.clone()));
|
||||||
|
|
||||||
|
for packet in packets {
|
||||||
|
receiver.add_inbound_from(remote_id, packet).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends one downward PTY frame from endpoint A to endpoint B.
|
||||||
|
pub(super) fn send_downward_frame(
|
||||||
|
endpoint_a: &mut Endpoint,
|
||||||
|
endpoint_b: &mut Endpoint,
|
||||||
|
hook_id: u16,
|
||||||
|
opcode: u8,
|
||||||
|
payload: &[u8],
|
||||||
|
end_hook: bool,
|
||||||
|
) {
|
||||||
|
endpoint_a
|
||||||
|
.add_outbound(pty_packet(
|
||||||
|
vec![ENDPOINT_A, ENDPOINT_B],
|
||||||
|
hook_id,
|
||||||
|
end_hook,
|
||||||
|
opcode,
|
||||||
|
payload,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
transfer_packets(endpoint_a, endpoint_b, ENDPOINT_B, ENDPOINT_A);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens a fake PTY session and delivers the `Opened` response to endpoint A.
|
||||||
|
pub(super) fn open_pty_session(
|
||||||
|
endpoint_a: &mut Endpoint,
|
||||||
|
endpoint_b: &mut Endpoint,
|
||||||
|
leaf: &mut FakePtyLeaf,
|
||||||
|
) -> u16 {
|
||||||
|
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(endpoint_a, endpoint_b, ENDPOINT_B, ENDPOINT_A);
|
||||||
|
leaf.update(endpoint_b);
|
||||||
|
transfer_packets(endpoint_b, endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
||||||
|
|
||||||
|
hook_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drains packets for `procedure_id` delivered to endpoint A.
|
||||||
|
pub(super) fn drain_parent_packets(endpoint: &mut Endpoint, procedure_id: u32) -> Vec<Packet> {
|
||||||
|
let mut packets = Vec::new();
|
||||||
|
endpoint.take_inbound_matching(
|
||||||
|
ENDPOINT_A,
|
||||||
|
|packet| packet.procedure_id == procedure_id,
|
||||||
|
|packet| packets.push(packet),
|
||||||
|
);
|
||||||
|
packets
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drains PTY packets delivered to endpoint A.
|
||||||
|
pub(super) fn drain_parent_pty_packets(endpoint: &mut Endpoint) -> Vec<Packet> {
|
||||||
|
drain_parent_packets(endpoint, PROC_PTY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asserts that local hook state still contains `hook_id`.
|
||||||
|
pub(super) fn assert_hook_present(endpoint: &Endpoint, hook_id: u16) {
|
||||||
|
assert!(endpoint.has_hook(hook_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asserts that local hook state no longer contains `hook_id`.
|
||||||
|
pub(super) fn assert_hook_removed(endpoint: &Endpoint, hook_id: u16) {
|
||||||
|
assert!(!endpoint.has_hook(hook_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asserts that `packet` carries the expected PTY frame.
|
||||||
|
pub(super) fn assert_frame(
|
||||||
|
packet: &Packet,
|
||||||
|
hook_id: u16,
|
||||||
|
opcode: u8,
|
||||||
|
end_hook: bool,
|
||||||
|
payload: &[u8],
|
||||||
|
) {
|
||||||
|
assert_eq!(packet.hook_id, hook_id);
|
||||||
|
assert_eq!(packet.end_hook, end_hook);
|
||||||
|
assert_eq!(frame_opcode(packet), Some(opcode));
|
||||||
|
assert_eq!(frame_payload(packet), payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true when `packets` contains the requested frame.
|
||||||
|
pub(super) fn has_frame(packets: &[Packet], hook_id: u16, opcode: u8, payload: &[u8]) -> bool {
|
||||||
|
packets.iter().any(|packet| {
|
||||||
|
packet.hook_id == hook_id
|
||||||
|
&& frame_opcode(packet) == Some(opcode)
|
||||||
|
&& frame_payload(packet) == payload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asserts that a packet is the fake PTY open acknowledgement.
|
||||||
|
pub(super) fn assert_opened(packet: &Packet, hook_id: u16) {
|
||||||
|
assert_frame(packet, hook_id, OP_OPENED, false, &[]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user