mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
Shrink endpoint runtime footprint
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -0,0 +1,3 @@
|
||||
mod shell;
|
||||
|
||||
pub use shell::{ShellLeaf, ShellState};
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user