mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
Make macro system and PTY test leaf
This commit is contained in:
@@ -1,20 +1,11 @@
|
||||
[package]
|
||||
name = "unshell-leaves"
|
||||
name = "leaf-pty"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description = "Application-layer UnShell leaves and client surfaces"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
leaf_endpoint = []
|
||||
leaf_tui = []
|
||||
description = "Hook-backed PTY leaf implementation for UnShell"
|
||||
|
||||
[dependencies]
|
||||
rkyv = { workspace = true }
|
||||
portable-pty = { workspace = true }
|
||||
crossbeam-channel = { workspace = true }
|
||||
unshell-macros = { workspace = true }
|
||||
unshell-protocol = { workspace = true }
|
||||
unshell = { workspace = true }
|
||||
|
||||
[lints.rust]
|
||||
elided_lifetimes_in_paths = "warn"
|
||||
@@ -27,4 +18,3 @@ unsafe_op_in_unsafe_fn = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_lifetimes = "warn"
|
||||
trivial_casts = "allow"
|
||||
missing_docs = "warn"
|
||||
@@ -0,0 +1,106 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use unshell::protocol::{HookID, Packet};
|
||||
|
||||
use crate::{OP_ERROR, OP_OPEN, PROC_PTY};
|
||||
|
||||
/// Encodes a tiny PTY frame into `Packet::data`.
|
||||
pub fn encode_frame(opcode: u8, payload: &[u8]) -> Vec<u8> {
|
||||
let mut data = Vec::with_capacity(1 + payload.len());
|
||||
data.push(opcode);
|
||||
data.extend_from_slice(payload);
|
||||
data
|
||||
}
|
||||
|
||||
/// Encodes an `Open` payload with the caller's reply path.
|
||||
pub fn encode_open(reply_path: &[u32]) -> Vec<u8> {
|
||||
let mut data = Vec::with_capacity(2 + reply_path.len() * 4);
|
||||
data.push(OP_OPEN);
|
||||
data.push(reply_path.len() as u8);
|
||||
|
||||
for segment in reply_path {
|
||||
data.extend_from_slice(&segment.to_le_bytes());
|
||||
}
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
/// Decodes the reply path embedded in an `Open` payload after the opcode byte.
|
||||
pub fn decode_open_reply_path(payload: &[u8]) -> Option<Vec<u32>> {
|
||||
let path_len = usize::from(*payload.first()?);
|
||||
let path_bytes = path_len.checked_mul(4)?;
|
||||
let expected_len = 1usize.checked_add(path_bytes)?;
|
||||
|
||||
if payload.len() != expected_len {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut path = Vec::with_capacity(path_len);
|
||||
for chunk in payload[1..].chunks_exact(4) {
|
||||
path.push(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
|
||||
}
|
||||
|
||||
Some(path)
|
||||
}
|
||||
|
||||
/// Returns the opcode byte from a PTY packet, if present.
|
||||
pub fn frame_opcode(packet: &Packet) -> Option<u8> {
|
||||
packet.data.first().copied()
|
||||
}
|
||||
|
||||
/// Returns the frame payload after the opcode byte.
|
||||
pub fn frame_payload(packet: &Packet) -> &[u8] {
|
||||
if packet.data.len() > 1 {
|
||||
&packet.data[1..]
|
||||
} else {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds an outer PTY packet for callers and tests.
|
||||
pub fn pty_packet(
|
||||
path: Vec<u32>,
|
||||
hook_id: HookID,
|
||||
end_hook: bool,
|
||||
opcode: u8,
|
||||
payload: &[u8],
|
||||
) -> Packet {
|
||||
Packet {
|
||||
hook_id,
|
||||
end_hook,
|
||||
path,
|
||||
procedure_id: PROC_PTY,
|
||||
data: encode_frame(opcode, payload),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds an outer PTY open packet with the specialized open payload shape.
|
||||
pub fn pty_open_packet(path: Vec<u32>, hook_id: HookID, reply_path: &[u32]) -> Packet {
|
||||
Packet {
|
||||
hook_id,
|
||||
end_hook: false,
|
||||
path,
|
||||
procedure_id: PROC_PTY,
|
||||
data: encode_open(reply_path),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a final error packet for session initialization failures.
|
||||
pub(crate) fn error_packet(hook_id: HookID, reply_path: Vec<u32>, payload: &[u8]) -> Packet {
|
||||
Packet {
|
||||
hook_id,
|
||||
end_hook: true,
|
||||
path: reply_path,
|
||||
procedure_id: PROC_PTY,
|
||||
data: encode_frame(OP_ERROR, payload),
|
||||
}
|
||||
}
|
||||
|
||||
/// Infers the caller reply path from a locally delivered destination path.
|
||||
pub(crate) fn reply_path_from_destination(destination: &[u32]) -> Vec<u32> {
|
||||
if destination.len() > 1 {
|
||||
destination[..destination.len() - 1].to_vec()
|
||||
} else {
|
||||
destination.to_vec()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/// Leaf id used by the generated fake PTY wrapper.
|
||||
pub const LEAF_FAKE_PTY: u32 = 300;
|
||||
|
||||
/// Outer procedure id used by all fake PTY session packets.
|
||||
pub const PROC_PTY: u32 = 30;
|
||||
|
||||
/// Downward opcode that opens one PTY session.
|
||||
pub const OP_OPEN: u8 = 0;
|
||||
|
||||
/// Upward opcode acknowledging an opened PTY session.
|
||||
pub const OP_OPENED: u8 = 1;
|
||||
|
||||
/// Downward opcode carrying PTY stdin bytes.
|
||||
pub const OP_INPUT: u8 = 2;
|
||||
|
||||
/// Downward opcode representing terminal resize.
|
||||
pub const OP_RESIZE: u8 = 3;
|
||||
|
||||
/// Downward opcode closing PTY stdin without closing the session hook.
|
||||
pub const OP_STDIN_EOF: u8 = 4;
|
||||
|
||||
/// Downward opcode asking the remote process to terminate gracefully.
|
||||
pub const OP_TERMINATE: u8 = 5;
|
||||
|
||||
/// Downward opcode aborting the session without an acknowledgement.
|
||||
pub const OP_ABORT: u8 = 6;
|
||||
|
||||
/// Upward opcode carrying PTY stdout/stderr bytes.
|
||||
pub const OP_OUTPUT: u8 = 7;
|
||||
|
||||
/// Upward final opcode carrying the process exit status.
|
||||
pub const OP_EXIT: u8 = 8;
|
||||
|
||||
/// Upward final opcode carrying a fatal PTY protocol error.
|
||||
pub const OP_ERROR: u8 = 9;
|
||||
@@ -0,0 +1,26 @@
|
||||
//! PTY leaf support for UnShell.
|
||||
//!
|
||||
//! This crate currently contains a deterministic fake PTY session used to prove the
|
||||
//! macro-generated leaf shape. The fake leaf exercises the same hook-backed protocol
|
||||
//! invariants as a real PTY worker without pulling OS-specific PTY code into
|
||||
//! `unshell-protocol`.
|
||||
|
||||
#![no_std]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
mod codec;
|
||||
mod constants;
|
||||
mod session;
|
||||
mod state;
|
||||
|
||||
pub use codec::{
|
||||
decode_open_reply_path, encode_frame, encode_open, frame_opcode, frame_payload,
|
||||
pty_open_packet, pty_packet,
|
||||
};
|
||||
pub use constants::*;
|
||||
pub use session::{PtySession, PtySessionState};
|
||||
pub use state::{FakePtyLeaf, FakePtyState};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,116 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use unshell::protocol::{
|
||||
HookID, Packet, PacketQueue, Session, SessionCtx, SessionInit, SessionInitResult, SessionStatus,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
codec::{
|
||||
decode_open_reply_path, error_packet, frame_opcode, frame_payload,
|
||||
reply_path_from_destination,
|
||||
},
|
||||
constants::{
|
||||
OP_ABORT, OP_ERROR, OP_EXIT, OP_INPUT, OP_OPEN, OP_OPENED, OP_OUTPUT, OP_STDIN_EOF,
|
||||
OP_TERMINATE, PROC_PTY,
|
||||
},
|
||||
state::FakePtyState,
|
||||
};
|
||||
|
||||
/// Session contract for one hook-backed fake PTY.
|
||||
pub struct PtySession;
|
||||
|
||||
/// Per-hook fake PTY session state.
|
||||
///
|
||||
/// A real PTY leaf will replace the pending flags with a worker handle. The reply path
|
||||
/// and hook lifecycle behavior should stay the same.
|
||||
pub struct PtySessionState {
|
||||
hook_id: HookID,
|
||||
reply_path: Vec<u32>,
|
||||
opened_pending: bool,
|
||||
stdin_closed: bool,
|
||||
}
|
||||
|
||||
impl Session<FakePtyState> for PtySession {
|
||||
const PROCEDURE_ID: u32 = PROC_PTY;
|
||||
|
||||
type State = PtySessionState;
|
||||
|
||||
fn reply_path(session: &Self::State) -> &[u32] {
|
||||
&session.reply_path
|
||||
}
|
||||
|
||||
fn init(
|
||||
leaf: &mut FakePtyState,
|
||||
packet: Packet,
|
||||
ctx: &mut SessionInit,
|
||||
) -> SessionInitResult<Self::State> {
|
||||
if frame_opcode(&packet) != Some(OP_OPEN) {
|
||||
return SessionInitResult::RejectedWith(error_packet(
|
||||
ctx.hook_id(),
|
||||
reply_path_from_destination(ctx.packet_path()),
|
||||
b"unknown-session",
|
||||
));
|
||||
}
|
||||
|
||||
let reply_path = decode_open_reply_path(frame_payload(&packet))
|
||||
.unwrap_or_else(|| reply_path_from_destination(ctx.packet_path()));
|
||||
|
||||
leaf.active_count += 1;
|
||||
leaf.total_opened += 1;
|
||||
|
||||
SessionInitResult::Created(PtySessionState {
|
||||
hook_id: ctx.hook_id(),
|
||||
reply_path,
|
||||
opened_pending: true,
|
||||
stdin_closed: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn update(
|
||||
leaf: &mut FakePtyState,
|
||||
session: &mut Self::State,
|
||||
incoming: &mut PacketQueue,
|
||||
ctx: &mut SessionCtx<'_>,
|
||||
) -> SessionStatus {
|
||||
if session.opened_pending {
|
||||
ctx.send(OP_OPENED, &[]);
|
||||
session.opened_pending = false;
|
||||
}
|
||||
|
||||
while let Some(packet) = incoming.pop_front() {
|
||||
match frame_opcode(&packet) {
|
||||
Some(OP_INPUT) => ctx.send(OP_OUTPUT, frame_payload(&packet)),
|
||||
Some(OP_STDIN_EOF) => {
|
||||
session.stdin_closed = true;
|
||||
leaf.last_stdin_eof_hook = Some(session.hook_id);
|
||||
}
|
||||
Some(OP_TERMINATE) => {
|
||||
ctx.send_final(OP_EXIT, &[0]);
|
||||
close_session(leaf);
|
||||
return SessionStatus::Closed;
|
||||
}
|
||||
Some(OP_ABORT) => {
|
||||
close_session(leaf);
|
||||
return SessionStatus::Closed;
|
||||
}
|
||||
Some(OP_OPEN) => {
|
||||
ctx.send_final(OP_ERROR, b"duplicate-open");
|
||||
close_session(leaf);
|
||||
return SessionStatus::Closed;
|
||||
}
|
||||
_ => {
|
||||
ctx.send_final(OP_ERROR, b"unknown-opcode");
|
||||
close_session(leaf);
|
||||
return SessionStatus::Closed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SessionStatus::Running
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrements the active-session counter exactly once for a terminal session path.
|
||||
fn close_session(leaf: &mut FakePtyState) {
|
||||
leaf.active_count = leaf.active_count.saturating_sub(1);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use unshell::protocol::{HookID, unshell_leaf};
|
||||
|
||||
use crate::{constants::LEAF_FAKE_PTY, session::PtySession};
|
||||
|
||||
/// User-owned state for the generated fake PTY leaf.
|
||||
///
|
||||
/// The macro-generated `FakePtyLeaf` wrapper stores sessions and retry queues around
|
||||
/// this struct. Keeping counters here makes tests and future procedures observe leaf
|
||||
/// behavior without reaching into generated session storage.
|
||||
#[unshell_leaf(leaf = FakePtyLeaf, id = LEAF_FAKE_PTY, sessions(PtySession))]
|
||||
pub struct FakePtyState {
|
||||
/// Number of sessions that application logic considers active.
|
||||
pub active_count: usize,
|
||||
|
||||
/// Total number of successfully opened sessions.
|
||||
pub total_opened: u64,
|
||||
|
||||
/// Last hook that received stdin EOF.
|
||||
pub last_stdin_eof_hook: Option<HookID>,
|
||||
}
|
||||
|
||||
impl FakePtyState {
|
||||
/// Creates a fake PTY state with no active sessions.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
active_count: 0,
|
||||
total_opened: 0,
|
||||
last_stdin_eof_hook: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FakePtyState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
use alloc::{vec, vec::Vec};
|
||||
|
||||
use unshell::protocol::{Endpoint, Leaf, 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;
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_pty_paves_hook_and_creates_session() {
|
||||
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
|
||||
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
||||
|
||||
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
|
||||
let packets = drain_parent_pty_packets(&mut endpoint_a);
|
||||
|
||||
assert_eq!(leaf.active_session_count(), 1);
|
||||
assert_eq!(leaf.state().active_count, 1);
|
||||
assert_eq!(leaf.state().total_opened, 1);
|
||||
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, &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_and_output_share_one_hook() {
|
||||
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
|
||||
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
||||
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
|
||||
drain_parent_pty_packets(&mut endpoint_a);
|
||||
|
||||
send_downward_frame(
|
||||
&mut endpoint_a,
|
||||
&mut endpoint_b,
|
||||
hook_id,
|
||||
OP_INPUT,
|
||||
b"hello",
|
||||
false,
|
||||
);
|
||||
leaf.update(&mut endpoint_b);
|
||||
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
||||
let packets = drain_parent_pty_packets(&mut endpoint_a);
|
||||
|
||||
assert_eq!(packets.len(), 1);
|
||||
assert_frame(&packets[0], hook_id, OP_OUTPUT, false, b"hello");
|
||||
assert_hook_present(&endpoint_a, hook_id);
|
||||
assert_hook_present(&endpoint_b, hook_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdin_eof_keeps_hook_until_exit() {
|
||||
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
|
||||
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
||||
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
|
||||
drain_parent_pty_packets(&mut endpoint_a);
|
||||
|
||||
send_downward_frame(
|
||||
&mut endpoint_a,
|
||||
&mut endpoint_b,
|
||||
hook_id,
|
||||
OP_STDIN_EOF,
|
||||
&[],
|
||||
false,
|
||||
);
|
||||
leaf.update(&mut endpoint_b);
|
||||
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
||||
|
||||
assert_eq!(leaf.state().last_stdin_eof_hook, Some(hook_id));
|
||||
assert!(drain_parent_pty_packets(&mut endpoint_a).is_empty());
|
||||
assert_hook_present(&endpoint_a, hook_id);
|
||||
assert_hook_present(&endpoint_b, hook_id);
|
||||
|
||||
send_downward_frame(
|
||||
&mut endpoint_a,
|
||||
&mut endpoint_b,
|
||||
hook_id,
|
||||
OP_TERMINATE,
|
||||
&[],
|
||||
false,
|
||||
);
|
||||
leaf.update(&mut endpoint_b);
|
||||
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
||||
let packets = drain_parent_pty_packets(&mut endpoint_a);
|
||||
|
||||
assert_eq!(packets.len(), 1);
|
||||
assert_frame(&packets[0], hook_id, OP_EXIT, true, &[0]);
|
||||
assert_eq!(leaf.active_session_count(), 0);
|
||||
assert_hook_removed(&endpoint_a, hook_id);
|
||||
assert_hook_removed(&endpoint_b, hook_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_end_hook_cleans_route_and_session() {
|
||||
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
|
||||
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
||||
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
|
||||
drain_parent_pty_packets(&mut endpoint_a);
|
||||
|
||||
send_downward_frame(
|
||||
&mut endpoint_a,
|
||||
&mut endpoint_b,
|
||||
hook_id,
|
||||
OP_TERMINATE,
|
||||
&[],
|
||||
false,
|
||||
);
|
||||
leaf.update(&mut endpoint_b);
|
||||
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
||||
let packets = drain_parent_pty_packets(&mut endpoint_a);
|
||||
|
||||
assert_eq!(packets.len(), 1);
|
||||
assert_frame(&packets[0], hook_id, OP_EXIT, true, &[0]);
|
||||
assert_eq!(leaf.active_session_count(), 0);
|
||||
assert_hook_removed(&endpoint_a, hook_id);
|
||||
assert_hook_removed(&endpoint_b, hook_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_final_exit_route_retries_without_losing_session() {
|
||||
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
|
||||
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
||||
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
|
||||
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(&mut endpoint_b);
|
||||
|
||||
assert_eq!(leaf.active_session_count(), 1);
|
||||
assert_eq!(leaf.pending_packet_count(), 1);
|
||||
assert_hook_present(&endpoint_b, hook_id);
|
||||
|
||||
endpoint_b.connections.insert((ENDPOINT_A, true));
|
||||
leaf.update(&mut endpoint_b);
|
||||
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
||||
let packets = drain_parent_pty_packets(&mut endpoint_a);
|
||||
|
||||
assert_eq!(packets.len(), 1);
|
||||
assert_frame(&packets[0], hook_id, OP_EXIT, true, &[0]);
|
||||
assert_eq!(leaf.active_session_count(), 0);
|
||||
assert_hook_removed(&endpoint_a, hook_id);
|
||||
assert_hook_removed(&endpoint_b, hook_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abort_downward_end_hook_closes_without_ack() {
|
||||
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
|
||||
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
||||
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
|
||||
drain_parent_pty_packets(&mut endpoint_a);
|
||||
|
||||
send_downward_frame(
|
||||
&mut endpoint_a,
|
||||
&mut endpoint_b,
|
||||
hook_id,
|
||||
OP_ABORT,
|
||||
&[],
|
||||
true,
|
||||
);
|
||||
leaf.update(&mut endpoint_b);
|
||||
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
||||
|
||||
assert_eq!(leaf.active_session_count(), 0);
|
||||
assert!(drain_parent_pty_packets(&mut endpoint_a).is_empty());
|
||||
assert_hook_removed(&endpoint_a, hook_id);
|
||||
assert_hook_removed(&endpoint_b, hook_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_session_input_returns_error_end_hook() {
|
||||
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
|
||||
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
||||
let hook_id = endpoint_a.get_hook_id();
|
||||
|
||||
send_downward_frame(
|
||||
&mut endpoint_a,
|
||||
&mut endpoint_b,
|
||||
hook_id,
|
||||
OP_INPUT,
|
||||
b"orphan",
|
||||
false,
|
||||
);
|
||||
leaf.update(&mut endpoint_b);
|
||||
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
||||
let packets = drain_parent_pty_packets(&mut endpoint_a);
|
||||
|
||||
assert_eq!(packets.len(), 1);
|
||||
assert_frame(&packets[0], hook_id, OP_ERROR, true, b"unknown-session");
|
||||
assert_eq!(leaf.active_session_count(), 0);
|
||||
assert_hook_removed(&endpoint_a, hook_id);
|
||||
assert_hook_removed(&endpoint_b, hook_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_pty_sessions_interleave_without_crossing_hooks() {
|
||||
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
|
||||
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
||||
|
||||
let first_hook = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
|
||||
let second_hook = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
|
||||
drain_parent_pty_packets(&mut endpoint_a);
|
||||
|
||||
send_downward_frame(
|
||||
&mut endpoint_a,
|
||||
&mut endpoint_b,
|
||||
second_hook,
|
||||
OP_INPUT,
|
||||
b"second",
|
||||
false,
|
||||
);
|
||||
send_downward_frame(
|
||||
&mut endpoint_a,
|
||||
&mut endpoint_b,
|
||||
first_hook,
|
||||
OP_INPUT,
|
||||
b"first",
|
||||
false,
|
||||
);
|
||||
leaf.update(&mut endpoint_b);
|
||||
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(), 2);
|
||||
assert_eq!(packets.len(), 2);
|
||||
assert!(has_frame(&packets, first_hook, OP_OUTPUT, b"first"));
|
||||
assert!(has_frame(&packets, second_hook, OP_OUTPUT, b"second"));
|
||||
assert_hook_present(&endpoint_a, first_hook);
|
||||
assert_hook_present(&endpoint_a, second_hook);
|
||||
assert_hook_present(&endpoint_b, first_hook);
|
||||
assert_hook_present(&endpoint_b, second_hook);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pty_leaf_does_not_consume_other_leaf_packets() {
|
||||
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
|
||||
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
||||
endpoint.connections.insert((ENDPOINT_A, true));
|
||||
|
||||
endpoint
|
||||
.add_inbound_from(
|
||||
ENDPOINT_A,
|
||||
pty_open_packet(vec![ENDPOINT_A, ENDPOINT_B], 7, &[ENDPOINT_A]),
|
||||
)
|
||||
.unwrap();
|
||||
endpoint
|
||||
.add_inbound_from(
|
||||
ENDPOINT_A,
|
||||
Packet {
|
||||
hook_id: 8,
|
||||
end_hook: false,
|
||||
path: vec![ENDPOINT_A, ENDPOINT_B],
|
||||
procedure_id: PROC_OTHER,
|
||||
data: b"leave-me".to_vec(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
leaf.update(&mut endpoint);
|
||||
|
||||
let mut other_packets = Vec::new();
|
||||
endpoint.take_inbound_matching(
|
||||
ENDPOINT_B,
|
||||
|packet| packet.procedure_id == PROC_OTHER,
|
||||
|packet| other_packets.push(packet),
|
||||
);
|
||||
|
||||
assert_eq!(leaf.active_session_count(), 1);
|
||||
assert_eq!(other_packets.len(), 1);
|
||||
assert_eq!(other_packets[0].procedure_id, PROC_OTHER);
|
||||
assert_eq!(other_packets[0].data, b"leave-me".to_vec());
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
Reference in New Issue
Block a user