mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
Improve Rust code clarity across the workspace
Document public APIs and non-obvious control flow so the protocol, simulator, and macro crates are easier to follow. Tighten a few helper paths and feature gates while preserving behavior and keeping the workspace warning-free.
This commit is contained in:
@@ -13,40 +13,56 @@ use crate::model::EndpointProcedureSpec;
|
||||
/// Root inspector mode.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum InspectorMode {
|
||||
/// Render the full scenario definition, including information the root has
|
||||
/// not yet learned through traffic or introspection.
|
||||
GroundTruth,
|
||||
/// Render only the subset of state the root host could plausibly know.
|
||||
Realistic,
|
||||
}
|
||||
|
||||
/// Learned procedure metadata stored by the root host.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LearnedProcedure {
|
||||
/// Stable protocol identifier for the learned procedure.
|
||||
pub procedure_id: String,
|
||||
/// Optional human-readable description learned from config or introspection.
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// Learned leaf metadata stored by the root host.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LearnedLeaf {
|
||||
/// Leaf name relative to the endpoint path.
|
||||
pub leaf_name: String,
|
||||
/// Optional human-readable description for the leaf.
|
||||
pub description: Option<String>,
|
||||
/// Procedures currently known on the leaf.
|
||||
pub procedures: Vec<LearnedProcedure>,
|
||||
}
|
||||
|
||||
/// Learned endpoint metadata stored by the root host.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LearnedNode {
|
||||
/// Absolute node path from the root.
|
||||
pub path: Vec<String>,
|
||||
/// Optional display title shown in the inspector.
|
||||
pub title: Option<String>,
|
||||
/// Optional endpoint description shown in the inspector.
|
||||
pub description: Option<String>,
|
||||
/// Whether the node is a direct child of the root.
|
||||
pub direct_child: bool,
|
||||
/// Endpoint-level procedures known on the node itself.
|
||||
pub endpoint_procedures: Vec<LearnedProcedure>,
|
||||
/// Leaf metadata currently known for the node.
|
||||
pub leaves: Vec<LearnedLeaf>,
|
||||
/// Whether endpoint introspection definitely ran against this node.
|
||||
pub endpoint_introspected: bool,
|
||||
}
|
||||
|
||||
/// Root-host knowledge accumulated from local configuration and observed traffic.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RootKnowledge {
|
||||
/// Learned nodes keyed by their absolute path.
|
||||
pub nodes: BTreeMap<Vec<String>, LearnedNode>,
|
||||
}
|
||||
|
||||
@@ -58,49 +74,9 @@ impl RootKnowledge {
|
||||
};
|
||||
for node in &tree.nodes {
|
||||
if node.path.is_empty() || node.path.len() == 1 {
|
||||
// Realistic mode intentionally starts with root plus direct children,
|
||||
// not the full transitive tree.
|
||||
let direct_child = node.path.len() == 1;
|
||||
let mut learned = LearnedNode {
|
||||
path: node.path.clone(),
|
||||
title: Some(node.title.clone()),
|
||||
description: Some(node.description.clone()),
|
||||
direct_child,
|
||||
endpoint_procedures: Vec::new(),
|
||||
leaves: Vec::new(),
|
||||
endpoint_introspected: node.path.is_empty(),
|
||||
};
|
||||
|
||||
if node.path.is_empty() {
|
||||
// The root always knows its own procedures and leaves because
|
||||
// those are locally configured, not discovered remotely.
|
||||
learned.endpoint_procedures = node
|
||||
.endpoint_procedures
|
||||
.iter()
|
||||
.map(|procedure| LearnedProcedure {
|
||||
procedure_id: procedure.procedure_id.clone(),
|
||||
description: Some(procedure.description.clone()),
|
||||
})
|
||||
.collect();
|
||||
learned.leaves = node
|
||||
.leaves
|
||||
.iter()
|
||||
.map(|leaf| LearnedLeaf {
|
||||
leaf_name: leaf.name.clone(),
|
||||
description: Some(leaf.description.clone()),
|
||||
procedures: leaf
|
||||
.procedures
|
||||
.iter()
|
||||
.map(|procedure_id| LearnedProcedure {
|
||||
procedure_id: procedure_id.clone(),
|
||||
description: Some(leaf.description.clone()),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
knowledge.nodes.insert(node.path.clone(), learned);
|
||||
knowledge
|
||||
.nodes
|
||||
.insert(node.path.clone(), initial_learned_node(node));
|
||||
}
|
||||
}
|
||||
knowledge
|
||||
@@ -223,12 +199,56 @@ impl RootKnowledge {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the root's initial record for one statically known node.
|
||||
fn initial_learned_node(node: &crate::model::DemoNode) -> LearnedNode {
|
||||
let mut learned = LearnedNode {
|
||||
path: node.path.clone(),
|
||||
title: Some(node.title.clone()),
|
||||
description: Some(node.description.clone()),
|
||||
direct_child: node.path.len() == 1,
|
||||
endpoint_procedures: Vec::new(),
|
||||
leaves: Vec::new(),
|
||||
endpoint_introspected: node.path.is_empty(),
|
||||
};
|
||||
|
||||
if node.path.is_empty() {
|
||||
// The root always knows its own procedures and leaves because those are
|
||||
// locally configured, not discovered remotely.
|
||||
learned.endpoint_procedures = node
|
||||
.endpoint_procedures
|
||||
.iter()
|
||||
.map(|procedure| LearnedProcedure {
|
||||
procedure_id: procedure.procedure_id.clone(),
|
||||
description: Some(procedure.description.clone()),
|
||||
})
|
||||
.collect();
|
||||
learned.leaves = node
|
||||
.leaves
|
||||
.iter()
|
||||
.map(|leaf| LearnedLeaf {
|
||||
leaf_name: leaf.name.clone(),
|
||||
description: Some(leaf.description.clone()),
|
||||
procedures: leaf
|
||||
.procedures
|
||||
.iter()
|
||||
.map(|procedure_id| LearnedProcedure {
|
||||
procedure_id: procedure_id.clone(),
|
||||
description: Some(leaf.description.clone()),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
learned
|
||||
}
|
||||
|
||||
/// Returns one learned leaf entry, creating it if necessary.
|
||||
fn ensure_leaf<'a>(
|
||||
leaves: &'a mut Vec<LearnedLeaf>,
|
||||
fn ensure_leaf(
|
||||
leaves: &mut Vec<LearnedLeaf>,
|
||||
leaf_name: String,
|
||||
description: Option<String>,
|
||||
) -> &'a mut LearnedLeaf {
|
||||
) -> &mut LearnedLeaf {
|
||||
if let Some(index) = leaves.iter().position(|leaf| leaf.leaf_name == leaf_name) {
|
||||
if leaves[index].description.is_none() {
|
||||
leaves[index].description = description;
|
||||
|
||||
@@ -32,16 +32,14 @@ impl Simulation {
|
||||
match procedure.kind {
|
||||
EndpointProcedureKind::Ping => {
|
||||
let reply = format!("pong from {}", self.node(node_id).display_path());
|
||||
let frame = self.nodes[node_id.0]
|
||||
.endpoint
|
||||
.make_data(
|
||||
hook.return_path.clone(),
|
||||
hook.hook_id,
|
||||
procedure.procedure_id.clone(),
|
||||
reply.clone().into_bytes(),
|
||||
true,
|
||||
)
|
||||
.map_err(|error| SimError::Protocol(error.to_string()))?;
|
||||
let frame = self.make_endpoint_data_frame(
|
||||
node_id,
|
||||
hook.return_path.clone(),
|
||||
hook.hook_id,
|
||||
procedure.procedure_id.clone(),
|
||||
reply.clone().into_bytes(),
|
||||
true,
|
||||
)?;
|
||||
self.record_trace(node_id, format!("endpoint sent ping reply: {reply}"));
|
||||
self.process_local_frame(node_id, frame)?;
|
||||
}
|
||||
@@ -54,16 +52,14 @@ impl Simulation {
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let frame = self.nodes[node_id.0]
|
||||
.endpoint
|
||||
.make_data(
|
||||
hook.return_path.clone(),
|
||||
hook.hook_id,
|
||||
procedure.procedure_id.clone(),
|
||||
text.as_bytes().to_vec(),
|
||||
index == 2,
|
||||
)
|
||||
.map_err(|error| SimError::Protocol(error.to_string()))?;
|
||||
let frame = self.make_endpoint_data_frame(
|
||||
node_id,
|
||||
hook.return_path.clone(),
|
||||
hook.hook_id,
|
||||
procedure.procedure_id.clone(),
|
||||
text.as_bytes().to_vec(),
|
||||
index == 2,
|
||||
)?;
|
||||
self.record_trace(node_id, format!("endpoint sent chunk {}", index + 1));
|
||||
self.process_local_frame(node_id, frame)?;
|
||||
}
|
||||
@@ -80,16 +76,14 @@ impl Simulation {
|
||||
procedure_id: procedure.procedure_id.clone(),
|
||||
},
|
||||
);
|
||||
let frame = self.nodes[node_id.0]
|
||||
.endpoint
|
||||
.make_data(
|
||||
hook.return_path.clone(),
|
||||
hook.hook_id,
|
||||
procedure.procedure_id.clone(),
|
||||
b"chat ready".to_vec(),
|
||||
false,
|
||||
)
|
||||
.map_err(|error| SimError::Protocol(error.to_string()))?;
|
||||
let frame = self.make_endpoint_data_frame(
|
||||
node_id,
|
||||
hook.return_path.clone(),
|
||||
hook.hook_id,
|
||||
procedure.procedure_id.clone(),
|
||||
b"chat ready".to_vec(),
|
||||
false,
|
||||
)?;
|
||||
self.record_trace(node_id, "chat handler opened session".to_owned());
|
||||
self.process_local_frame(node_id, frame)?;
|
||||
}
|
||||
@@ -97,6 +91,23 @@ impl Simulation {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds one endpoint-originated data frame after application logic decides
|
||||
/// what to send back on an already-validated hook.
|
||||
fn make_endpoint_data_frame(
|
||||
&mut self,
|
||||
node_id: NodeId,
|
||||
return_path: Vec<String>,
|
||||
hook_id: u64,
|
||||
procedure_id: String,
|
||||
data: Vec<u8>,
|
||||
end_hook: bool,
|
||||
) -> Result<unshell::protocol::FrameBytes, SimError> {
|
||||
self.nodes[node_id.0]
|
||||
.endpoint
|
||||
.make_data(return_path, hook_id, procedure_id, data, end_hook)
|
||||
.map_err(|error| SimError::Protocol(error.to_string()))
|
||||
}
|
||||
|
||||
/// Resolves one endpoint procedure from the ground-truth node metadata.
|
||||
pub(super) fn lookup_endpoint_procedure(
|
||||
&self,
|
||||
|
||||
@@ -20,51 +20,24 @@ impl Simulation {
|
||||
match event {
|
||||
LocalEvent::Data { header, message } => {
|
||||
let text = String::from_utf8_lossy(&message.data).to_string();
|
||||
self.record_trace(
|
||||
node_id,
|
||||
format!(
|
||||
"local Data on {}: {text}",
|
||||
format_hook_ref(
|
||||
self.node(node_id).path.as_slice(),
|
||||
header.hook_id.unwrap_or(0)
|
||||
)
|
||||
),
|
||||
let hook_ref = format_hook_ref(
|
||||
self.node(node_id).path.as_slice(),
|
||||
header.hook_id.unwrap_or(0),
|
||||
);
|
||||
self.record_trace(node_id, format!("local Data on {hook_ref}: {text}"));
|
||||
|
||||
if let Some(hook_id) = header.hook_id {
|
||||
if let Some(snapshot) = self.hooks.get_mut(&hook_id) {
|
||||
// Keep the most recent human-readable payload in the UI.
|
||||
snapshot.last_message = if text.is_empty() {
|
||||
format!("binary payload ({} bytes)", message.data.len())
|
||||
} else {
|
||||
text.clone()
|
||||
};
|
||||
if message.end_hook {
|
||||
snapshot.closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
self.update_hook_snapshot(hook_id, &text, message.data.len(), message.end_hook);
|
||||
if node_id == self.root_id {
|
||||
self.learn_from_root_data(hook_id, &message);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(session) = self
|
||||
.chat_sessions
|
||||
.get(&header.hook_id.unwrap_or(0))
|
||||
.cloned()
|
||||
.filter(|session| session.node_id == node_id)
|
||||
{
|
||||
if let Some(session) = self.chat_session_for_event(node_id, header.hook_id) {
|
||||
// Rationale: chat responses are implemented here instead of in the
|
||||
// core endpoint so the protocol crate stays generic. The simulator
|
||||
// acts as the application layer sitting above validated hook traffic.
|
||||
let reply = if text.eq_ignore_ascii_case("bye") {
|
||||
Some(("chat session closed".to_owned(), true))
|
||||
} else if !text.is_empty() {
|
||||
Some((format!("chat ack: {}", text.to_uppercase()), false))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let reply = chat_reply_for_text(&text);
|
||||
|
||||
if let Some((reply, end_hook)) = reply {
|
||||
let frame = self.nodes[session.node_id.0]
|
||||
@@ -92,16 +65,13 @@ impl Simulation {
|
||||
});
|
||||
}
|
||||
LocalEvent::Fault { header, message } => {
|
||||
let hook_ref = format_hook_ref(
|
||||
self.node(node_id).path.as_slice(),
|
||||
header.hook_id.unwrap_or(0),
|
||||
);
|
||||
self.record_trace(
|
||||
node_id,
|
||||
format!(
|
||||
"local Fault on {}: 0x{:02X}",
|
||||
format_hook_ref(
|
||||
self.node(node_id).path.as_slice(),
|
||||
header.hook_id.unwrap_or(0)
|
||||
),
|
||||
message.fault.0
|
||||
),
|
||||
format!("local Fault on {hook_ref}: 0x{:02X}", message.fault.0),
|
||||
);
|
||||
if let Some(hook_id) = header.hook_id {
|
||||
if let Some(snapshot) = self.hooks.get_mut(&hook_id) {
|
||||
@@ -140,4 +110,45 @@ impl Simulation {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_hook_snapshot(
|
||||
&mut self,
|
||||
hook_id: u64,
|
||||
text: &str,
|
||||
payload_len: usize,
|
||||
end_hook: bool,
|
||||
) {
|
||||
if let Some(snapshot) = self.hooks.get_mut(&hook_id) {
|
||||
// Keep the most recent human-readable payload in the UI.
|
||||
snapshot.last_message = if text.is_empty() {
|
||||
format!("binary payload ({payload_len} bytes)")
|
||||
} else {
|
||||
text.to_owned()
|
||||
};
|
||||
if end_hook {
|
||||
snapshot.closed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn chat_session_for_event(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
hook_id: Option<u64>,
|
||||
) -> Option<super::super::super::types::ChatSession> {
|
||||
self.chat_sessions
|
||||
.get(&hook_id.unwrap_or(0))
|
||||
.cloned()
|
||||
.filter(|session| session.node_id == node_id)
|
||||
}
|
||||
}
|
||||
|
||||
fn chat_reply_for_text(text: &str) -> Option<(String, bool)> {
|
||||
if text.eq_ignore_ascii_case("bye") {
|
||||
return Some(("chat session closed".to_owned(), true));
|
||||
}
|
||||
if text.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some((format!("chat ack: {}", text.to_uppercase()), false))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
//! Root-side knowledge learning from returned data.
|
||||
//!
|
||||
//! The simulator learns only from data that arrives back at the root on a known
|
||||
//! hook. This keeps the realistic inspector aligned with what the UI-triggered
|
||||
//! action actually observed.
|
||||
|
||||
use unshell::protocol::{
|
||||
DataMessage, EndpointIntrospection, LeafIntrospection, deserialize_archived_bytes,
|
||||
@@ -17,23 +21,7 @@ impl Simulation {
|
||||
let demo_node = self.node(node_id).clone();
|
||||
|
||||
if snapshot.procedure_id.is_empty() {
|
||||
if snapshot.target_leaf.is_some() {
|
||||
if let Ok(introspection) = deserialize_archived_bytes::<
|
||||
unshell::protocol::introspection::ArchivedLeafIntrospection,
|
||||
LeafIntrospection,
|
||||
>(&message.data)
|
||||
{
|
||||
self.root_knowledge
|
||||
.remember_leaf_introspection(&demo_node, &introspection);
|
||||
}
|
||||
} else if let Ok(introspection) = deserialize_archived_bytes::<
|
||||
unshell::protocol::introspection::ArchivedEndpointIntrospection,
|
||||
EndpointIntrospection,
|
||||
>(&message.data)
|
||||
{
|
||||
self.root_knowledge
|
||||
.remember_endpoint_introspection(&demo_node, &introspection);
|
||||
}
|
||||
self.learn_from_root_introspection(&snapshot, &demo_node, message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -54,3 +42,33 @@ impl Simulation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Simulation {
|
||||
fn learn_from_root_introspection(
|
||||
&mut self,
|
||||
snapshot: &super::super::types::HookSnapshot,
|
||||
demo_node: &crate::model::DemoNode,
|
||||
message: &DataMessage,
|
||||
) {
|
||||
if snapshot.target_leaf.is_some() {
|
||||
if let Ok(introspection) = deserialize_archived_bytes::<
|
||||
unshell::protocol::introspection::ArchivedLeafIntrospection,
|
||||
LeafIntrospection,
|
||||
>(&message.data)
|
||||
{
|
||||
self.root_knowledge
|
||||
.remember_leaf_introspection(demo_node, &introspection);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(introspection) = deserialize_archived_bytes::<
|
||||
unshell::protocol::introspection::ArchivedEndpointIntrospection,
|
||||
EndpointIntrospection,
|
||||
>(&message.data)
|
||||
{
|
||||
self.root_knowledge
|
||||
.remember_endpoint_introspection(demo_node, &introspection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user