Redesign interface event ownership.

This commit is contained in:
Michael Mikovsky
2026-06-01 09:54:37 -06:00
parent 5597ca2fef
commit aa1e9be696
16 changed files with 882 additions and 368 deletions
+3
View File
@@ -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.
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.
pub const OP_OPEN: u8 = 0;
+1
View File
@@ -11,6 +11,7 @@ extern crate alloc;
mod codec;
mod constants;
mod procedure;
mod session;
mod state;
+19
View File
@@ -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);
}
}
+4 -2
View File
@@ -1,6 +1,6 @@
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.
///
@@ -47,6 +47,8 @@ unshell_leaf! {
sessions {
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());
}
+5
View File
@@ -0,0 +1,5 @@
mod session;
mod support;
#[cfg(feature = "interface")]
mod interface;
@@ -1,127 +1,17 @@
use alloc::{vec, vec::Vec};
use unshell::protocol::{Endpoint, Leaf, Packet};
use unshell::protocol::{Leaf, Packet};
#[cfg(feature = "interface")]
use unshell::interface::{InterfaceEventKind, InterfaceStore, SessionKey, SessionViewStatus};
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,
use crate::{
FakePtyLeaf, FakePtyState, OP_ABORT, OP_ERROR, OP_EXIT, OP_INPUT, OP_OUTPUT, OP_STDIN_EOF,
OP_TERMINATE, pty_open_packet,
};
const ENDPOINT_A: u32 = 0;
const ENDPOINT_B: u32 = 1;
const PROC_OTHER: u32 = 31;
/// 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
})
}
use super::support::{
ENDPOINT_A, ENDPOINT_B, PROC_OTHER, assert_frame, assert_hook_present, assert_hook_removed,
assert_opened, drain_parent_pty_packets, endpoint_at, has_frame, open_pty_session,
pty_endpoints, send_downward_frame, transfer_packets,
};
#[test]
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_b, hook_id);
assert_eq!(packets.len(), 1);
assert_frame(&packets[0], hook_id, OP_OPENED, false, &[]);
assert_opened(&packets[0], hook_id);
}
#[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].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, &[]);
}