More consistency.

This commit is contained in:
Michael Mikovsky
2026-02-16 13:58:49 -07:00
parent 01959ce440
commit 3954e4519e
5 changed files with 101 additions and 18 deletions
+13 -9
View File
@@ -88,19 +88,23 @@ impl std::error::Error for ModuleError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None None
} }
fn description(&self) -> &str {
"description() is deprecated; use Display"
}
fn cause(&self) -> Option<&dyn std::error::Error> {
Some(self)
}
} }
impl fmt::Display for ModuleError { impl fmt::Display for ModuleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(format!("{:?}", self).as_str()) match self {
ModuleError::NoError => write!(f, "NoError"),
ModuleError::TreeNotExist => write!(f, "Tree does not exist"),
ModuleError::TreeMessageError(msg) => write!(f, "Tree message error: {}", msg),
ModuleError::UnsupportedMethod => write!(f, "Unsupported method"),
ModuleError::InvalidType => write!(f, "Invalid type"),
ModuleError::LibLoadingError(msg) => write!(f, "Library loading error: {}", msg),
ModuleError::LinkError(msg) => write!(f, "Link error: {}", msg),
ModuleError::CryptError(msg) => write!(f, "Cryptography error: {}", msg),
ModuleError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
ModuleError::SerdeJsonError(msg) => write!(f, "JSON error: {}", msg),
ModuleError::Error(msg) => write!(f, "{}", msg),
}
} }
} }
+10
View File
@@ -103,6 +103,16 @@ impl Branch {
json!(self.branch_type) json!(self.branch_type)
} }
/// Remove a child by name, returning the removed element
pub fn remove_child(&mut self, name: &str) -> Option<Box<dyn TreeElement>> {
self.children.remove(name)
}
/// Get a reference to the children map (for iteration)
pub fn children_mut(&mut self) -> &mut HashMap<String, Box<dyn TreeElement>> {
&mut self.children
}
pub fn send_message(&mut self, target: Value, message: Value) -> Value { pub fn send_message(&mut self, target: Value, message: Value) -> Value {
self.handle_local_message(target, message) self.handle_local_message(target, message)
} }
+55 -4
View File
@@ -6,6 +6,7 @@
use serde_json::{json, Value}; use serde_json::{json, Value};
use crate::tree::{Branch, TreeElement}; use crate::tree::{Branch, TreeElement};
use crate::{error, info, warn};
/// Trait for component lifecycle management /// Trait for component lifecycle management
pub trait Component: Send + Sync { pub trait Component: Send + Sync {
@@ -96,11 +97,13 @@ impl ComponentRegistry {
let name = component.name().to_string(); let name = component.name().to_string();
if self.branch.get_child(&name).is_some() { if self.branch.get_child(&name).is_some() {
warn!("Component '{}' already registered", name);
return Err(format!("Component '{}' already registered", name)); return Err(format!("Component '{}' already registered", name));
} }
let wrapper = ComponentWrapper::new(component); let wrapper = ComponentWrapper::new(component);
self.branch.add_child(name, Box::new(wrapper)); self.branch.add_child(name.clone(), Box::new(wrapper));
info!("Component '{}' registered successfully", name);
Ok(()) Ok(())
} }
@@ -116,8 +119,16 @@ impl ComponentRegistry {
self.branch.children().contains_key(name) self.branch.children().contains_key(name)
} }
pub fn remove(&mut self, _name: &str) -> bool { /// Remove a component from the registry by name.
false /// Returns true if the component was found and removed.
pub fn remove(&mut self, name: &str) -> bool {
let removed = self.branch.remove_child(name).is_some();
if removed {
info!("Component '{}' removed successfully", name);
} else {
warn!("Component '{}' not found for removal", name);
}
removed
} }
pub fn list(&self) -> Vec<String> { pub fn list(&self) -> Vec<String> {
@@ -136,6 +147,7 @@ impl ComponentRegistry {
if let Some(component) = self.branch.get_child(component_name) { if let Some(component) = self.branch.get_child(component_name) {
component.send_message(json!(null), message) component.send_message(json!(null), message)
} else { } else {
warn!("Component '{}' not found", component_name);
let err_msg = format!("Component '{}' not found", component_name); let err_msg = format!("Component '{}' not found", component_name);
json!({"error": err_msg}) json!({"error": err_msg})
} }
@@ -159,8 +171,47 @@ impl ComponentRegistry {
.collect() .collect()
} }
/// Shutdown all registered components gracefully.
/// Returns a list of component names and their shutdown results.
pub fn shutdown_all(&mut self) -> Vec<(String, Result<(), String>)> { pub fn shutdown_all(&mut self) -> Vec<(String, Result<(), String>)> {
Vec::new() info!("Shutting down all components");
let names: Vec<String> = self.branch.children().keys().cloned().collect();
let results: Vec<(String, Result<(), String>)> = names
.into_iter()
.filter_map(|name| {
// Try to send shutdown message to each component
if let Some(component) = self.branch.get_child(&name) {
let result = component.send_message(json!(null), json!({"method": "shutdown"}));
// Check if shutdown was successful
let success = result
.get("success")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let err = if success {
info!("Component '{}' shutdown successfully", name);
Ok(())
} else {
let error_msg = result
.get("error")
.and_then(|v| v.as_str())
.unwrap_or("unknown error")
.to_string();
error!("Component '{}' shutdown failed: {}", name, error_msg);
Err(error_msg)
};
Some((name, err))
} else {
None
}
})
.collect();
info!("All components shutdown complete");
results
} }
} }
+2 -2
View File
@@ -212,10 +212,10 @@ mod tests {
#[test] #[test]
fn test_request_building() { fn test_request_building() {
let server = HttpServer::new(Default::default()); let proto = HttpProtocol::new(Default::default());
let body = r#"{"test": "data"}"#.as_bytes(); let body = r#"{"test": "data"}"#.as_bytes();
let request = server.build_request(body); let request = proto.encode(body).unwrap();
let request_str = String::from_utf8(request).unwrap(); let request_str = String::from_utf8(request).unwrap();
assert!(request_str.contains("POST / HTTP/1.1")); assert!(request_str.contains("POST / HTTP/1.1"));
+20 -2
View File
@@ -17,6 +17,7 @@ use unshell::tree::component::Component;
use unshell::tree::message::TreeMessage; use unshell::tree::message::TreeMessage;
use unshell::tree::symbols::*; use unshell::tree::symbols::*;
use unshell::tree::{Branch, TreeElement}; use unshell::tree::{Branch, TreeElement};
use unshell::{error, info};
/// TCP Client component with protocol stacking support. /// TCP Client component with protocol stacking support.
/// ///
@@ -83,6 +84,7 @@ impl TcpClient {
/// Connect to the configured address /// Connect to the configured address
pub fn connect(&mut self) -> Result<(), String> { pub fn connect(&mut self) -> Result<(), String> {
let addr = format!("{}:{}", self.config.address, self.config.port); let addr = format!("{}:{}", self.config.address, self.config.port);
info!("[{}] Connecting to {}", self.name, addr);
let stream = TcpStream::connect_timeout( let stream = TcpStream::connect_timeout(
&addr &addr
@@ -90,7 +92,10 @@ impl TcpClient {
.map_err(|e| format!("Invalid address: {}", e))?, .map_err(|e| format!("Invalid address: {}", e))?,
Duration::from_millis(self.config.timeout_ms), Duration::from_millis(self.config.timeout_ms),
) )
.map_err(|e| format!("Connection failed: {}", e))?; .map_err(|e| {
error!("[{}] Connection failed: {}", self.name, e);
format!("Connection failed: {}", e)
})?;
stream stream
.set_nonblocking(false) .set_nonblocking(false)
@@ -105,16 +110,19 @@ impl TcpClient {
.map(|a| a.to_string()) .map(|a| a.to_string())
.unwrap_or_default(); .unwrap_or_default();
self.status = ConnectionStatus::connected(remote, local); self.status = ConnectionStatus::connected(remote.clone(), local);
self.stream = Some(Arc::new(Mutex::new(stream))); self.stream = Some(Arc::new(Mutex::new(stream)));
info!("[{}] Connected to {}", self.name, remote);
Ok(()) Ok(())
} }
/// Disconnect from server /// Disconnect from server
pub fn disconnect(&mut self) -> Result<(), String> { pub fn disconnect(&mut self) -> Result<(), String> {
info!("[{}] Disconnecting", self.name);
self.stream = None; self.stream = None;
self.status = ConnectionStatus::disconnected(); self.status = ConnectionStatus::disconnected();
info!("[{}] Disconnected", self.name);
Ok(()) Ok(())
} }
@@ -305,7 +313,17 @@ impl Component for TcpClient {
self.set_protocols(p)?; self.set_protocols(p)?;
} }
// Only auto-connect if explicitly requested in config
// This follows the philosophy that init() should configure, not connect
let auto_connect = config
.get("auto_connect")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if auto_connect {
self.connect()?; self.connect()?;
}
Ok(()) Ok(())
} }