From bdd0724d6b68facec86b924ab91c872b9a86df18 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 3 Jul 2025 09:23:50 -0400 Subject: [PATCH 1/7] added terminal handler --- hyperprocess_macro/src/lib.rs | 53 +++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/hyperprocess_macro/src/lib.rs b/hyperprocess_macro/src/lib.rs index e346041..87eb487 100644 --- a/hyperprocess_macro/src/lib.rs +++ b/hyperprocess_macro/src/lib.rs @@ -45,6 +45,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 } /// Enum for the different handler types @@ -53,6 +54,7 @@ enum HandlerType { Local, Remote, Http, + Terminal, } /// Grouped handlers by type @@ -60,6 +62,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>, } @@ -75,6 +78,9 @@ 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 let mut local_and_remote = local.clone(); @@ -92,6 +98,7 @@ impl<'a> HandlerGroups<'a> { local, remote, http, + terminal, local_and_remote, } } @@ -102,6 +109,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, } @@ -444,10 +452,10 @@ fn analyze_methods( let has_local = has_attribute(method, "local"); let has_remote = has_attribute(method, "remote"); let has_ws = has_attribute(method, "ws"); - + let has_terminal = has_attribute(method, "terminal"); // Handle init method if has_init { - if has_http || has_local || has_remote || has_ws { + if has_http || has_local || has_remote || has_ws || has_terminal { return Err(syn::Error::new_spanned( method, "#[init] cannot be combined with other attributes", @@ -488,10 +496,10 @@ fn analyze_methods( } // Handle request-response methods - if has_http || has_local || has_remote { + if has_http || has_local || has_remote || has_terminal { validate_request_response_function(method)?; function_metadata.push(extract_function_metadata( - method, has_local, has_remote, has_http, + method, has_local, has_remote, has_http, has_terminal, )); } } @@ -501,7 +509,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.", )); } @@ -514,6 +522,7 @@ fn extract_function_metadata( is_local: bool, is_remote: bool, is_http: bool, + is_terminal: bool, ) -> FunctionMetadata { let ident = method.sig.ident.clone(); @@ -550,6 +559,7 @@ fn extract_function_metadata( is_local, is_remote, is_http, + is_terminal, } } @@ -674,6 +684,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", }; return quote! { hyperware_process_lib::logging::warn!(#message); @@ -684,6 +695,7 @@ fn generate_handler_dispatch( HandlerType::Local => "local", HandlerType::Remote => "remote", HandlerType::Http => "http", + HandlerType::Terminal => "terminal", }; let dispatch_arms = handlers @@ -751,6 +763,14 @@ fn generate_response_handling( ); } } + 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(); + } + } } } @@ -911,6 +931,7 @@ fn generate_message_handlers( let http_request_match_arms = &handler_arms.http; let local_request_match_arms = &handler_arms.local; let remote_request_match_arms = &handler_arms.remote; + let terminal_request_match_arms = &handler_arms.terminal; // We now use the combined local_and_remote handlers for local messages let local_and_remote_request_match_arms = &handler_arms.local_and_remote; @@ -1031,6 +1052,25 @@ fn generate_message_handlers( } } } + + /// Handle terminal messages + fn handle_terminal_message(state: *mut #self_ty, message: hyperware_process_lib::Message) { + // Process the terminal request based on our handlers + match serde_json::from_slice::(message.body()) { + Ok(request) => { + unsafe { + // Match on the request variant and call the appropriate handler + #terminal_request_match_arms + + // Save state if needed + hyperware_app_common::maybe_save_state(&mut *state); + } + }, + Err(e) => { + hyperware_process_lib::logging::warn!("Failed to deserialize terminal request into HPMRequest enum: {}\nRaw request value: {:?}", e, message.body()); + } + } + } } } @@ -1190,6 +1230,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().process == "terminal:distro:sys" { + handle_terminal_message(&mut state, message); } else if message.is_local() { handle_local_message(&mut state, message); } else { @@ -1261,6 +1303,7 @@ pub fn hyperprocess(attr: TokenStream, item: TokenStream) -> TokenStream { local: generate_handler_dispatch(&handlers.local, self_ty, HandlerType::Local), remote: generate_handler_dispatch(&handlers.remote, self_ty, HandlerType::Remote), http: generate_handler_dispatch(&handlers.http, 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, From 9f35eda0b2b7ca3560fcb7792acd85bc3c4de017 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:57:24 -0400 Subject: [PATCH 2/7] added docs for terminal handler --- README.md | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index eb832eb..3075041 100644 --- a/README.md +++ b/README.md @@ -135,13 +135,14 @@ Example: ### Handler Types -Hyperware processes can handle three types of requests, specified by attributes: +Hyperware processes can handle four types of requests, specified by attributes: | Attribute | Description | |-----------|-------------| | `#[local]` | Handles local (same-node) requests | | `#[remote]` | Handles remote (cross-node) requests | | `#[http]` | Handles HTTP requests to your process endpoints | +| `#[terminal]` | Handles terminal requests from the system terminal | These attributes can be combined to make a handler respond to multiple request types: @@ -157,6 +158,47 @@ async fn increment_counter(&mut self, value: i32) -> i32 { 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 +``` + +For terminal handlers, the JSON body should match the generated request enum variant. For example, if you have a terminal handler named `handle_terminal_command` that takes a `String` parameter: + +```bash +# Send a command and await the response +m -a 5 our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "status"}' + +# Send a command without waiting for response +m our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "reset"}' ``` The function arguments and the return values _have_ to be serializable with `Serde`. @@ -265,6 +307,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 @@ -587,6 +641,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: @@ -595,11 +652,13 @@ The macro generates these enums: enum Request { GetUser(u64), UpdateSettings(Settings, bool), + ExecuteCommand(String), } enum Response { GetUser(User), UpdateSettings(bool), + ExecuteCommand(String), } ``` @@ -781,11 +840,12 @@ graph TB MsgRouter -->|"HTTP"| HttpHandler["HTTP Handlers"] MsgRouter -->|"Local"| LocalHandler["Local Handlers"] MsgRouter -->|"Remote"| RemoteHandler["Remote Handlers"] + MsgRouter -->|"Terminal"| TerminalHandler["Terminal Handlers"] MsgRouter -->|"WebSocket"| WsHandler["WebSocket Handlers"] MsgRouter -->|"Response"| RespHandler["Response Handler"] %% State management - HttpHandler & LocalHandler & RemoteHandler & WsHandler --> AppState[("Application State + HttpHandler & LocalHandler & RemoteHandler & TerminalHandler & WsHandler --> AppState[("Application State SaveOptions::EveryMessage")] %% Async handling @@ -852,7 +912,7 @@ graph TB %% Style elements class UserSrc,WitFiles,CallerUtils,EnumStructs,ReqResEnums,HandlerDisp,AsyncRuntime,MainLoop,WasmComp mainflow class MsgLoop,Executor,RespRegistry,RespHandler,AF2,AF8 accent - class MsgRouter,HttpHandler,LocalHandler,RemoteHandler,WsHandler,CallStub,AppState dataflow + class MsgRouter,HttpHandler,LocalHandler,RemoteHandler,TerminalHandler,WsHandler,CallStub,AppState dataflow class AF1,AF3,AF4,AF5,AF6,AF7,AF9 asyncflow class ExtClient1,ExtClient2,Process2,Storage,InMsg,OutMsg external class CorrelationNote annotation From 7926d881065d1ef3ecbb8b7c10d0c238c77e3d62 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 4 Sep 2025 18:09:19 -0400 Subject: [PATCH 3/7] Update lib.rs --- src/lib.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b2da87c..ec7f80c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,7 +68,6 @@ struct HandlerGroups<'a> { remote: Vec<&'a FunctionMetadata>, http: Vec<&'a FunctionMetadata>, terminal: Vec<&'a FunctionMetadata>, - eth: Vec<&'a FunctionMetadata>, // New group for combined handlers (used for local messages that can also use remote handlers) local_and_remote: Vec<&'a FunctionMetadata>, } @@ -87,9 +86,6 @@ impl<'a> HandlerGroups<'a> { // Collect terminal handlers let terminal: Vec<_> = metadata.iter().filter(|f| f.is_terminal).collect(); - // Collect ETH handlers - let eth: Vec<_> = metadata.iter().filter(|f| f.is_eth).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 let mut local_and_remote = local.clone(); @@ -108,7 +104,6 @@ impl<'a> HandlerGroups<'a> { remote, http, terminal, - eth, local_and_remote, } } @@ -120,7 +115,6 @@ struct HandlerDispatch { remote: proc_macro2::TokenStream, http: proc_macro2::TokenStream, terminal: proc_macro2::TokenStream, - eth: proc_macro2::TokenStream, local_and_remote: proc_macro2::TokenStream, } @@ -2348,7 +2342,6 @@ pub fn hyperprocess(attr: TokenStream, item: TokenStream) -> TokenStream { // 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), - eth: generate_handler_dispatch(&handlers.eth, self_ty, HandlerType::Eth), // Generate dispatch for combined local and remote handlers local_and_remote: generate_handler_dispatch( &handlers.local_and_remote, From f33dc9403f857b34f077710a081e1713b29516c3 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 4 Sep 2025 18:14:09 -0400 Subject: [PATCH 4/7] fix --- README.md | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f1a5f51..2681625 100644 --- a/README.md +++ b/README.md @@ -198,14 +198,44 @@ Example: - this one will not await the response or print it out ``` -For terminal handlers, the JSON body should match the generated request enum variant. For example, if you have a terminal handler named `handle_terminal_command` that takes a `String` parameter: +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) -> String { + match command.as_str() { + "status" => format!("Counter: {}", self.counter), + "reset" => { + self.counter = 0; + "Counter reset".to_string() + }, + "increment" => { + self.counter += 1; + format!("Counter incremented to: {}", self.counter) + }, + "help" => "Available commands: status, reset, increment, help".to_string(), + _ => "Unknown command. Type 'help' for available commands.".to_string() + } +} +``` + +**Messaging the Terminal Handler:** ```bash -# Send a command and await the response +# Get current status and await response m -a 5 our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "status"}' -# Send a command without waiting for response -m our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "reset"}' +# Reset the counter and await response +m -a 5 our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "reset"}' + +# Increment counter without waiting for response +m our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "increment"}' + +# Get help +m -a 5 our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "help"}' +``` + + ``` The function arguments and the return values _have_ to be serializable with `Serde`. @@ -1659,12 +1689,11 @@ graph TB MsgRouter -->|"HTTP"| HttpHandler["HTTP Handlers"] MsgRouter -->|"Local"| LocalHandler["Local Handlers"] MsgRouter -->|"Remote"| RemoteHandler["Remote Handlers"] - MsgRouter -->|"Terminal"| TerminalHandler["Terminal Handlers"] MsgRouter -->|"WebSocket"| WsHandler["WebSocket Handlers"] MsgRouter -->|"Response"| RespHandler["Response Handler"] %% State management - HttpHandler & LocalHandler & RemoteHandler & TerminalHandler & WsHandler --> AppState[("Application State + HttpHandler & LocalHandler & RemoteHandler & WsHandler --> AppState[("Application State SaveOptions::EveryMessage")] %% Async handling @@ -1731,7 +1760,7 @@ graph TB %% Style elements class UserSrc,WitFiles,CallerUtils,EnumStructs,ReqResEnums,HandlerDisp,AsyncRuntime,MainLoop,WasmComp mainflow class MsgLoop,Executor,RespRegistry,RespHandler,AF2,AF8 accent - class MsgRouter,HttpHandler,LocalHandler,RemoteHandler,TerminalHandler,WsHandler,CallStub,AppState dataflow + class MsgRouter,HttpHandler,LocalHandler,RemoteHandler,WsHandler,CallStub,AppState dataflow class AF1,AF3,AF4,AF5,AF6,AF7,AF9 asyncflow class ExtClient1,ExtClient2,Process2,Storage,InMsg,OutMsg external class CorrelationNote annotation From 0f01016086764906a12d5eccf1eab9e18806cbd8 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 4 Sep 2025 18:16:18 -0400 Subject: [PATCH 5/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2681625..46bebc5 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ 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 HTTP requests to your process endpoints (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 | From b7c962f1ec26f1158b40b0c999ddb8efe0dc046c Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 4 Sep 2025 19:11:41 -0400 Subject: [PATCH 6/7] fix --- README.md | 31 +++++++++++++++++-------------- src/lib.rs | 2 +- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 46bebc5..da8e1ff 100644 --- a/README.md +++ b/README.md @@ -203,42 +203,45 @@ For terminal handlers, the JSON body should match the generated request enum var **Handler Implementation:** ```rust #[terminal] -fn handle_terminal_command(&mut self, command: String) -> String { +fn handle_terminal_command(&mut self, command: String) { match command.as_str() { - "status" => format!("Counter: {}", self.counter), + "status" => kiprintln!("Counter: {}", self.counter), "reset" => { self.counter = 0; - "Counter reset".to_string() + kiprintln!("Counter reset"); }, "increment" => { self.counter += 1; - format!("Counter incremented to: {}", self.counter) + kiprintln!("Counter incremented to: {}", self.counter); }, - "help" => "Available commands: status, reset, increment, help".to_string(), - _ => "Unknown command. Type 'help' for available commands.".to_string() + "help" => kiprintln!("Available commands: status, reset, increment, help"), + _ => kiprintln!("Unknown command. Type 'help' for available commands.") } } ``` **Messaging the Terminal Handler:** ```bash -# Get current status and await response -m -a 5 our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "status"}' +# Get current status (output will be printed to terminal) +m our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "status"}' -# Reset the counter and await response -m -a 5 our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "reset"}' +# Reset the counter (output will be printed to terminal) +m our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "reset"}' -# Increment counter without waiting for response +# Increment counter (output will be printed to terminal) m our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "increment"}' -# Get help -m -a 5 our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "help"}' +# 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 and the return values _have_ to be serializable with `Serde`. +The function arguments _have_ to be serializable with `Serde`, but return values are not used. #### HTTP Method Support and Smart Routing diff --git a/src/lib.rs b/src/lib.rs index ec7f80c..d9832e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2256,7 +2256,7 @@ 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().process == "terminal:distro:sys" { + } 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); From 29e5ba585b717f654685b887cd6bf429b428a9bb Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 4 Sep 2025 19:13:56 -0400 Subject: [PATCH 7/7] preventing terminal handlers from hacing return values at compile time --- src/lib.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index d9832e6..a28e3a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -691,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 //------------------------------------------------------------------------------ @@ -818,6 +839,10 @@ fn analyze_methods( // 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, has_terminal, false);