Shrink endpoint runtime footprint

This commit is contained in:
Michael Mikovsky
2026-06-01 13:08:26 -06:00
parent 4cd496ed2b
commit 7749f62629
25 changed files with 1245 additions and 489 deletions
@@ -107,7 +107,7 @@ fn interface_update_records_failed_direct_route_without_retry() {
&[],
false,
);
endpoint_b.connections.remove(&(ENDPOINT_A, true));
endpoint_b.remove_connection(ENDPOINT_A, true);
leaf.update_interface(&mut endpoint_b, &mut interface);
let session_key = SessionKey {
@@ -121,7 +121,7 @@ fn interface_update_records_failed_direct_route_without_retry() {
assert_eq!(leaf.pending_packet_count(), 0);
assert_eq!(session_view.status, SessionViewStatus::Closed);
endpoint_b.connections.insert((ENDPOINT_A, true));
endpoint_b.add_connection(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);
+3 -3
View File
@@ -138,14 +138,14 @@ fn failed_final_exit_route_closes_session_without_retry() {
&[],
false,
);
endpoint_b.connections.remove(&(ENDPOINT_A, true));
endpoint_b.remove_connection(ENDPOINT_A, true);
leaf.update(&mut endpoint_b);
assert_eq!(leaf.active_session_count(), 0);
assert_eq!(leaf.pending_packet_count(), 0);
assert_hook_removed(&endpoint_b, hook_id);
endpoint_b.connections.insert((ENDPOINT_A, true));
endpoint_b.add_connection(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);
@@ -248,7 +248,7 @@ fn two_pty_sessions_interleave_without_crossing_hooks() {
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_connection(ENDPOINT_A, true);
endpoint
.add_inbound_from(ENDPOINT_A, pty_open_packet(vec![ENDPOINT_A, ENDPOINT_B], 7))
+3 -3
View File
@@ -12,7 +12,7 @@ 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![]);
let mut endpoint = Endpoint::new(id);
endpoint.path = path;
endpoint
}
@@ -22,8 +22,8 @@ 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.add_connection(ENDPOINT_B, false);
endpoint_b.add_connection(ENDPOINT_A, true);
(endpoint_a, endpoint_b)
}
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "leaf-shell"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
include.workspace = true
[dependencies]
unshell = { workspace = true }
[features]
default = []
interface = ["unshell/interface"]
interface_ratatui = ["interface", "unshell/interface_ratatui"]
[lints.rust]
elided_lifetimes_in_paths = "warn"
future_incompatible = { level = "warn", priority = -1 }
nonstandard_style = { level = "warn", priority = -1 }
rust_2018_idioms = { level = "warn", priority = -1 }
rust_2021_prelude_collisions = "warn"
semicolon_in_expressions_from_macros = "warn"
unsafe_op_in_unsafe_fn = "warn"
unused_import_braces = "warn"
unused_lifetimes = "warn"
trivial_casts = "allow"
+3
View File
@@ -0,0 +1,3 @@
mod shell;
pub use shell::{ShellLeaf, ShellState};
+143
View File
@@ -0,0 +1,143 @@
use std::{
io::Write,
process::{Child, Command, Stdio},
};
use unshell::{
crypto::hash_str_32,
protocol::{Endpoint, HookID, Packet, PacketQueue, Session, SessionInitError, SessionStatus},
unshell_leaf,
};
macro_rules! version {
() => {
env!("CARGO_PKG_VERSION")
};
}
pub const IDENTIFIER: &str = concat!("dev.unshell.", version!(), ".shell");
pub const SESSION_ID: &str = concat!("dev.unshell.", version!(), ".shell.session");
pub const IDENTIFIER_HASH: u32 = hash_str_32(IDENTIFIER);
pub const SESSION_ID_HASH: u32 = hash_str_32(SESSION_ID);
unshell_leaf! {
pub leaf ShellLeaf for ShellState {
id: IDENTIFIER_HASH,
meta: unshell::protocol::LeafMeta {
name: "Shell",
identifier: IDENTIFIER,
version: version!(),
authors: vec!["ASTATIN3"],
},
sessions {
shell: ShellSession,
}
procedures {
// ping: PingProcedure,
}
}
}
/// Runtime state for the native shell leaf.
///
/// The process state lives in per-hook [`ShellSessionState`] values because every
/// routed hook owns one child shell. The leaf-level state is intentionally empty for
/// now, but keeping a named type gives callers a stable constructor as the real shell
/// leaf grows environment and policy configuration.
#[derive(Debug, Default)]
pub struct ShellState;
impl ShellState {
/// Creates a shell leaf state with default local process settings.
pub fn new() -> Self {
Self
}
}
/// Per-hook native child process state.
///
/// Hook routing is retained by the generated runtime. This state only owns the child
/// process and stream lifecycle so dropping a session cannot leave a shell orphaned.
struct ShellSession {
_hook_id: HookID,
child: Child,
stdin_closed: bool,
}
impl ShellSession {
/// Starts the user's interactive shell for one routed session.
///
/// `/bin/bash` matches the original shell leaf sketch. This should eventually be
/// made configurable at `ShellState`, but hard-coding it here keeps the current
/// migration focused on the session API instead of broadening shell policy.
fn spawn(hook_id: HookID) -> Result<Self, SessionInitError> {
let child = Command::new("/bin/bash")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.map_err(|_| SessionInitError::rejected())?;
Ok(Self {
_hook_id: hook_id,
child,
stdin_closed: false,
})
}
/// Closes the child's stdin once callers finish writing to the session.
fn close_stdin(&mut self) {
self.stdin_closed = true;
let _ = self.child.stdin.take();
}
}
impl Drop for ShellSession {
fn drop(&mut self) {
if matches!(self.child.try_wait(), Ok(Some(_))) {
return;
}
let _ = self.child.kill();
let _ = self.child.wait();
}
}
impl Session<ShellState> for ShellSession {
const PROCEDURE_ID: u32 = SESSION_ID_HASH;
fn init(_leaf: &mut ShellState, packet: Packet) -> Result<Self, SessionInitError> {
Self::spawn(packet.hook_id)
}
fn update(
_leaf: &mut ShellState,
session: &mut Self,
incoming: &mut PacketQueue,
_endpoint: &mut Endpoint,
) -> SessionStatus {
while let Some(packet) = incoming.pop_front() {
if packet.end_hook {
session.close_stdin();
}
if packet.data.is_empty() || session.stdin_closed {
continue;
}
let Some(stdin) = session.child.stdin.as_mut() else {
session.close_stdin();
continue;
};
if stdin.write_all(&packet.data).is_err() {
session.close_stdin();
}
}
match session.child.try_wait() {
Ok(Some(_)) | Err(_) => SessionStatus::Closed,
Ok(None) => SessionStatus::Running,
}
}
}