diff --git a/README.md b/README.md index 5cdd45d..da8e1ff 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,8 @@ Hyperware processes can handle four types of requests, specified by attributes: |-----------|-------------| | `#[local]` | Handles local (same-node) requests | | `#[remote]` | Handles remote (cross-node) requests | -| `#[http]` | Handles ALL HTTP requests (GET, POST, PUT, DELETE, etc.) | +| `#[http]` | Handles ALL HTTP requests (GET, POST, PUT, DELETE, etc.) | +| `#[terminal]` | Handles terminal requests from the system terminal | | `#[eth]` | Handles Ethereum subscription updates from your RPC provider | These attributes can be combined to make a handler respond to multiple request types: @@ -164,9 +165,83 @@ fn handle_any_http(&mut self) -> String { fn get_status(&mut self) -> String { format!("Status: {}", self.counter) } + +#[terminal] +fn handle_terminal_command(&mut self, command: String) -> String { + match command.as_str() { + "status" => format!("Counter: {}", self.counter), + "reset" => { + self.counter = 0; + "Counter reset".to_string() + } + _ => "Unknown command".to_string() + } +} +``` + +#### Messaging Terminal Handlers + +To send messages to terminal handlers from the Hyperdrive terminal, use the `m` command: + +```bash +m - message a process +Usage: m
+Arguments: +
hns address e.g. some-node.os@process:pkg:publisher.os + json payload wrapped in single quotes, e.g. '{"foo": "bar"}' +Options: + -a, --await await the response, timing out after SECONDS +Example: + m -a 5 our@foo:bar:baz '{"some payload": "value"}' + - this will await the response and print it out + m our@foo:bar:baz '{"some payload": "value"}' + - this one will not await the response or print it out ``` -The function arguments and the return values _have_ to be serializable with `Serde`. +For terminal handlers, the JSON body should match the generated request enum variant. Here's a complete example: + +**Handler Implementation:** +```rust +#[terminal] +fn handle_terminal_command(&mut self, command: String) { + match command.as_str() { + "status" => kiprintln!("Counter: {}", self.counter), + "reset" => { + self.counter = 0; + kiprintln!("Counter reset"); + }, + "increment" => { + self.counter += 1; + kiprintln!("Counter incremented to: {}", self.counter); + }, + "help" => kiprintln!("Available commands: status, reset, increment, help"), + _ => kiprintln!("Unknown command. Type 'help' for available commands.") + } +} +``` + +**Messaging the Terminal Handler:** +```bash +# Get current status (output will be printed to terminal) +m our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "status"}' + +# Reset the counter (output will be printed to terminal) +m our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "reset"}' + +# Increment counter (output will be printed to terminal) +m our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "increment"}' + +# Get help (output will be printed to terminal) +m our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "help"}' +``` + +**Important Notes:** +- Terminal handlers should **not** have a return value +- All output should be handled with `kiprintln!()` or logging functions + +``` + +The function arguments _have_ to be serializable with `Serde`, but return values are not used. #### HTTP Method Support and Smart Routing @@ -600,6 +675,18 @@ impl AsyncRequesterState { self.request_count } + #[terminal] + fn handle_terminal(&mut self, command: String) -> String { + match command.as_str() { + "stats" => format!("Requests processed: {}", self.request_count), + "clear" => { + self.request_count = 0; + "Request count cleared".to_string() + } + _ => "Available commands: stats, clear".to_string() + } + } + #[ws] fn websocket(&mut self, channel_id: u32, message_type: WsMessageType, blob: LazyLoadBlob) { // Process WebSocket messages @@ -1406,6 +1493,9 @@ async fn get_user(&mut self, id: u64) -> User { ... } #[local] #[remote] fn update_settings(&mut self, settings: Settings, apply_now: bool) -> bool { ... } + +#[terminal] +fn execute_command(&mut self, cmd: String) -> String { ... } ``` The macro generates these enums: @@ -1414,11 +1504,13 @@ The macro generates these enums: enum Request { GetUser(u64), UpdateSettings(Settings, bool), + ExecuteCommand(String), } enum Response { GetUser(User), UpdateSettings(bool), + ExecuteCommand(String), } ``` diff --git a/src/lib.rs b/src/lib.rs index 166528e..a28e3a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,6 +46,7 @@ struct FunctionMetadata { is_local: bool, // Has #[local] attribute is_remote: bool, // Has #[remote] attribute is_http: bool, // Has #[http] attribute + is_terminal: bool, // Has #[terminal] attribute is_eth: bool, // Has #[eth] attribute http_methods: Vec, // HTTP methods this handler accepts (GET, POST, etc.) http_path: Option, // Specific path this handler is bound to (optional) @@ -57,6 +58,7 @@ enum HandlerType { Local, Remote, Http, + Terminal, Eth, } @@ -65,6 +67,7 @@ struct HandlerGroups<'a> { local: Vec<&'a FunctionMetadata>, remote: Vec<&'a FunctionMetadata>, http: Vec<&'a FunctionMetadata>, + terminal: Vec<&'a FunctionMetadata>, // New group for combined handlers (used for local messages that can also use remote handlers) local_and_remote: Vec<&'a FunctionMetadata>, } @@ -80,6 +83,8 @@ impl<'a> HandlerGroups<'a> { // Collect HTTP handlers let http: Vec<_> = metadata.iter().filter(|f| f.is_http).collect(); + // Collect terminal handlers + let terminal: Vec<_> = metadata.iter().filter(|f| f.is_terminal).collect(); // Create a combined list of local and remote handlers for local messages // We first include all local handlers, then add remote handlers that aren't already covered @@ -98,6 +103,7 @@ impl<'a> HandlerGroups<'a> { local, remote, http, + terminal, local_and_remote, } } @@ -108,6 +114,7 @@ struct HandlerDispatch { local: proc_macro2::TokenStream, remote: proc_macro2::TokenStream, http: proc_macro2::TokenStream, + terminal: proc_macro2::TokenStream, local_and_remote: proc_macro2::TokenStream, } @@ -370,6 +377,7 @@ fn clean_impl_block(impl_block: &ItemImpl) -> ItemImpl { && !attr.path().is_ident("local") && !attr.path().is_ident("remote") && !attr.path().is_ident("eth") + && !attr.path().is_ident("terminal") && !attr.path().is_ident("ws") && !attr.path().is_ident("ws_client") }); @@ -683,6 +691,27 @@ fn validate_eth_handler(method: &syn::ImplItemFn) -> syn::Result<()> { Ok(()) } +/// Validate the terminal handler signature +fn validate_terminal_handler(method: &syn::ImplItemFn) -> syn::Result<()> { + // Ensure first param is &mut self + if !has_valid_self_receiver(method) { + return Err(syn::Error::new_spanned( + &method.sig, + "Terminal handler must take &mut self as their first parameter", + )); + } + + // Validate return type (must be unit) + if !matches!(method.sig.output, ReturnType::Default) { + return Err(syn::Error::new_spanned( + &method.sig.output, + "Terminal handlers must not return a value. Use kiprintln!() for output instead of return values.", + )); + } + + Ok(()) +} + //------------------------------------------------------------------------------ // Method Analysis Functions //------------------------------------------------------------------------------ @@ -716,11 +745,12 @@ fn analyze_methods( let has_remote = has_attribute(method, "remote"); let has_eth = has_attribute(method, "eth"); let has_ws = has_attribute(method, "ws"); + let has_terminal = has_attribute(method, "terminal"); let has_ws_client = has_attribute(method, "ws_client"); // Handle init method if has_init { - if has_http || has_local || has_remote || has_eth || has_ws || has_ws_client { + if has_http || has_local || has_remote || has_eth || has_ws || has_ws_client || has_terminal { return Err(syn::Error::new_spanned( method, "#[init] cannot be combined with other attributes", @@ -743,7 +773,7 @@ fn analyze_methods( // Handle WebSocket method if has_ws { - if has_http || has_local || has_remote || has_eth || has_init || has_ws_client { + if has_http || has_local || has_remote || has_eth || has_init || has_ws_client || has_terminal { return Err(syn::Error::new_spanned( method, "#[ws] cannot be combined with other attributes", @@ -765,7 +795,7 @@ fn analyze_methods( // Handle WebSocket client method if has_ws_client { - if has_http || has_local || has_remote || has_eth || has_init || has_ws { + if has_http || has_local || has_remote || has_eth || has_init || has_ws || has_terminal { return Err(syn::Error::new_spanned( method, "#[ws_client] cannot be combined with other attributes", @@ -787,7 +817,7 @@ fn analyze_methods( // Handle ETH method if has_eth { - if has_http || has_local || has_remote || has_init || has_ws || has_ws_client { + if has_http || has_local || has_remote || has_init || has_ws || has_ws_client || has_terminal { return Err(syn::Error::new_spanned( method, "#[eth] cannot be combined with other attributes", @@ -807,10 +837,14 @@ fn analyze_methods( // Continue with regular processing for function metadata } - // Handle request-response methods (local, remote, http - NOT eth) - if has_http || has_local || has_remote { + // Handle request-response methods (local, remote, http, terminal - NOT eth) + if has_http || has_local || has_remote || has_terminal { + // Validate terminal handlers specifically + if has_terminal { + validate_terminal_handler(method)?; + } validate_request_response_function(method)?; - let metadata = extract_function_metadata(method, has_local, has_remote, has_http, false); + let metadata = extract_function_metadata(method, has_local, has_remote, has_http, has_terminal, false); // Parameter-less HTTP handlers can optionally specify a path, but it's not required // They can use get_path() and get_method() to handle requests dynamically @@ -824,7 +858,7 @@ fn analyze_methods( if function_metadata.is_empty() { return Err(syn::Error::new( proc_macro2::Span::call_site(), - "You must specify at least one handler with #[remote], #[local] or #[http] attribute. Without any handlers, this hyperprocess wouldn't respond to any requests.", + "You must specify at least one handler with #[remote], #[local], #[terminal] or #[http] attribute. Without any handlers, this hyperprocess wouldn't respond to any requests.", )); } @@ -878,6 +912,7 @@ fn extract_function_metadata( is_local: bool, is_remote: bool, is_http: bool, + is_terminal: bool, is_eth: bool, ) -> FunctionMetadata { let ident = method.sig.ident.clone(); @@ -922,6 +957,7 @@ fn extract_function_metadata( is_local, is_remote, is_http, + is_terminal, is_eth, http_methods, http_path, @@ -1053,6 +1089,7 @@ fn generate_handler_dispatch( HandlerType::Local => "No local handlers defined but received a local request", HandlerType::Remote => "No remote handlers defined but received a remote request", HandlerType::Http => "No HTTP handlers defined but received an HTTP request", + HandlerType::Terminal => "No terminal handlers defined but received a terminal request", HandlerType::Eth => "No ETH handlers defined but received an ETH request", }; return quote! { @@ -1064,6 +1101,7 @@ fn generate_handler_dispatch( HandlerType::Local => "local", HandlerType::Remote => "remote", HandlerType::Http => "http", + HandlerType::Terminal => "terminal", HandlerType::Eth => "eth", }; @@ -1121,6 +1159,14 @@ fn generate_response_handling( resp.send().unwrap(); } } + HandlerType::Terminal => { + quote! { + // Instead of wrapping in HPMResponse enum, directly serialize the result + let resp = hyperware_process_lib::Response::new() + .body(serde_json::to_vec(&result).unwrap()); + resp.send().unwrap(); + } + } HandlerType::Eth => { quote! { // Instead of wrapping in HPMResponse enum, directly serialize the result @@ -1947,6 +1993,8 @@ fn generate_message_handlers( generate_local_message_handler(self_ty, local_and_remote_request_match_arms); let remote_message_handler = generate_remote_message_handler(self_ty, remote_request_match_arms); + let terminal_message_handler = + generate_terminal_message_handler(self_ty, &handler_arms.terminal); let eth_message_handler = generate_eth_message_handler(self_ty, eth_method_call); @@ -1987,10 +2035,44 @@ fn generate_message_handlers( #local_message_handler #remote_message_handler + #terminal_message_handler #eth_message_handler } } +/// Generate terminal message handler +fn generate_terminal_message_handler( + self_ty: &Box, + match_arms: &proc_macro2::TokenStream, +) -> proc_macro2::TokenStream { + quote! { + /// Handle terminal messages + fn handle_terminal_message(state: *mut #self_ty, message: hyperware_process_lib::Message) { + hyperware_process_lib::logging::debug!("Processing terminal message from: {:?}", message.source()); + match serde_json::from_slice::(message.body()) { + Ok(request) => { + unsafe { + #match_arms + hyperware_process_lib::hyperapp::maybe_save_state(&mut *state); + } + }, + Err(e) => { + let raw_body = String::from_utf8_lossy(message.body()); + hyperware_process_lib::logging::error!( + "Failed to deserialize terminal request into HPMRequest enum.\n\ + Error: {}\n\ + Source: {:?}\n\ + Body: {}\n\ + \n\ + 💡 This usually means the message format doesn't match any of your #[terminal] handlers.", + e, message.source(), raw_body + ); + } + } + } + } +} + /// Generate ETH message handler fn generate_eth_message_handler( self_ty: &Box, @@ -2199,6 +2281,8 @@ fn generate_component_impl( hyperware_process_lib::Message::Request { .. } => { if message.is_local() && message.source().process == "http-server:distro:sys" { handle_http_server_message(&mut state, message); + } else if message.is_local() && message.source().package_id().to_string() == "terminal:sys" { + handle_terminal_message(&mut state, message); } else if message.is_local() && message.source().process == "http-client:distro:sys" { handle_websocket_client_message(&mut state, message); } else if message.is_local() && message.source().process == "eth:distro:sys" { @@ -2282,6 +2366,7 @@ pub fn hyperprocess(attr: TokenStream, item: TokenStream) -> TokenStream { remote: generate_handler_dispatch(&handlers.remote, self_ty, HandlerType::Remote), // HTTP dispatch arms are only generated for handlers with parameters. http: generate_handler_dispatch(&http_handlers_with_params, self_ty, HandlerType::Http), + terminal: generate_handler_dispatch(&handlers.terminal, self_ty, HandlerType::Terminal), // Generate dispatch for combined local and remote handlers local_and_remote: generate_handler_dispatch( &handlers.local_and_remote,