Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 63 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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 <ADDRESS> <BODY>
Arguments:
<ADDRESS> hns address e.g. some-node.os@process:pkg:publisher.os
<BODY> json payload wrapped in single quotes, e.g. '{"foo": "bar"}'
Options:
-a, --await <SECONDS> 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`.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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),
}
```

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
53 changes: 48 additions & 5 deletions hyperprocess_macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -53,13 +54,15 @@ enum HandlerType {
Local,
Remote,
Http,
Terminal,
}

/// Grouped handlers by type
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>,
}
Expand All @@ -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();
Expand All @@ -92,6 +98,7 @@ impl<'a> HandlerGroups<'a> {
local,
remote,
http,
terminal,
local_and_remote,
}
}
Expand All @@ -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,
}

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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,
));
}
}
Expand All @@ -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.",
));
}

Expand All @@ -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();

Expand Down Expand Up @@ -550,6 +559,7 @@ fn extract_function_metadata(
is_local,
is_remote,
is_http,
is_terminal,
}
}

Expand Down Expand Up @@ -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);
Expand All @@ -684,6 +695,7 @@ fn generate_handler_dispatch(
HandlerType::Local => "local",
HandlerType::Remote => "remote",
HandlerType::Http => "http",
HandlerType::Terminal => "terminal",
};

let dispatch_arms = handlers
Expand Down Expand Up @@ -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();
}
}
}
}

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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::<HPMRequest>(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());
}
}
}
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down