Production deployment with zinit config
This commit is contained in:
3368
Cargo.lock
generated
3368
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,8 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Shared job crate
|
# Runner crate with integrated job module
|
||||||
hero-job = { git = "https://git.ourworld.tf/herocode/job" }
|
runner_rust = { path = "../runner_rust" }
|
||||||
# Async runtime
|
# Async runtime
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
|
||||||
|
|||||||
3421
clients/admin-ui/Cargo.lock
generated
3421
clients/admin-ui/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
3640
clients/openrpc/Cargo.lock
generated
3640
clients/openrpc/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,15 +8,18 @@ license = "MIT OR Apache-2.0"
|
|||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Common dependencies for both native and WASM
|
# Common dependencies for both native and WASM
|
||||||
hero-supervisor = { path = "../../" }
|
|
||||||
hero-job = { path = "../../../job" }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||||
|
# Collections (common)
|
||||||
|
indexmap = "2.0"
|
||||||
|
|
||||||
# Native JSON-RPC client (not WASM compatible)
|
# Native JSON-RPC client (not WASM compatible)
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
@@ -65,19 +68,9 @@ features = ["serde", "wasmbind"]
|
|||||||
version = "1.0"
|
version = "1.0"
|
||||||
features = ["v4", "serde", "js"]
|
features = ["v4", "serde", "js"]
|
||||||
|
|
||||||
# Collections
|
# Collections (common)
|
||||||
indexmap = "2.0"
|
indexmap = "2.0"
|
||||||
|
|
||||||
# Interactive CLI
|
|
||||||
crossterm = "0.27"
|
|
||||||
ratatui = "0.28"
|
|
||||||
|
|
||||||
# Command line parsing
|
|
||||||
clap = { version = "4.0", features = ["derive"] }
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "openrpc-cli"
|
|
||||||
path = "cmd/main.rs"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# Testing utilities
|
# Testing utilities
|
||||||
|
|||||||
@@ -1,872 +0,0 @@
|
|||||||
//! Interactive CLI for Hero Supervisor OpenRPC Client
|
|
||||||
//!
|
|
||||||
//! This CLI provides an interactive interface to explore and test OpenRPC methods
|
|
||||||
//! with arrow key navigation, parameter input, and response display.
|
|
||||||
|
|
||||||
use clap::Parser;
|
|
||||||
use crossterm::{
|
|
||||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
};
|
|
||||||
use ratatui::{
|
|
||||||
backend::CrosstermBackend,
|
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
|
|
||||||
style::{Color, Modifier, Style},
|
|
||||||
text::{Line, Span, Text},
|
|
||||||
widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
|
|
||||||
Frame, Terminal,
|
|
||||||
};
|
|
||||||
use serde_json::json;
|
|
||||||
use std::io;
|
|
||||||
use chrono;
|
|
||||||
|
|
||||||
use hero_supervisor_openrpc_client::{SupervisorClient, RunnerConfig, RunnerType, ProcessManagerType};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(name = "openrpc-cli")]
|
|
||||||
#[command(about = "Interactive CLI for Hero Supervisor OpenRPC")]
|
|
||||||
struct Cli {
|
|
||||||
/// OpenRPC server URL
|
|
||||||
#[arg(short, long, default_value = "http://127.0.0.1:3030")]
|
|
||||||
url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct RpcMethod {
|
|
||||||
name: String,
|
|
||||||
description: String,
|
|
||||||
params: Vec<RpcParam>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct RpcParam {
|
|
||||||
name: String,
|
|
||||||
param_type: String,
|
|
||||||
required: bool,
|
|
||||||
description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct App {
|
|
||||||
client: SupervisorClient,
|
|
||||||
methods: Vec<RpcMethod>,
|
|
||||||
list_state: ListState,
|
|
||||||
current_screen: Screen,
|
|
||||||
selected_method: Option<RpcMethod>,
|
|
||||||
param_inputs: Vec<String>,
|
|
||||||
current_param_index: usize,
|
|
||||||
response: Option<String>,
|
|
||||||
error_message: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
enum Screen {
|
|
||||||
MethodList,
|
|
||||||
ParamInput,
|
|
||||||
Response,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
|
||||||
async fn new(url: String) -> Result<Self, Box<dyn std::error::Error>> {
|
|
||||||
let client = SupervisorClient::new(&url)?;
|
|
||||||
|
|
||||||
// Test connection to OpenRPC server using the standard rpc.discover method
|
|
||||||
// This is the proper OpenRPC way to test server connectivity and discover available methods
|
|
||||||
let discovery_result = client.discover().await;
|
|
||||||
match discovery_result {
|
|
||||||
Ok(discovery_info) => {
|
|
||||||
println!("✓ Connected to OpenRPC server at {}", url);
|
|
||||||
if let Some(info) = discovery_info.get("info") {
|
|
||||||
if let Some(title) = info.get("title").and_then(|t| t.as_str()) {
|
|
||||||
println!(" Server: {}", title);
|
|
||||||
}
|
|
||||||
if let Some(version) = info.get("version").and_then(|v| v.as_str()) {
|
|
||||||
println!(" Version: {}", version);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Err(format!("Failed to connect to OpenRPC server at {}: {}\nMake sure the supervisor is running with OpenRPC enabled.", url, e).into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let methods = vec![
|
|
||||||
RpcMethod {
|
|
||||||
name: "list_runners".to_string(),
|
|
||||||
description: "List all registered runners".to_string(),
|
|
||||||
params: vec![],
|
|
||||||
},
|
|
||||||
RpcMethod {
|
|
||||||
name: "register_runner".to_string(),
|
|
||||||
description: "Register a new runner to the supervisor with secret authentication".to_string(),
|
|
||||||
params: vec![
|
|
||||||
RpcParam {
|
|
||||||
name: "secret".to_string(),
|
|
||||||
param_type: "String".to_string(),
|
|
||||||
required: true,
|
|
||||||
description: "Secret required for runner registration".to_string(),
|
|
||||||
},
|
|
||||||
RpcParam {
|
|
||||||
name: "name".to_string(),
|
|
||||||
param_type: "String".to_string(),
|
|
||||||
required: true,
|
|
||||||
description: "Name of the runner".to_string(),
|
|
||||||
},
|
|
||||||
RpcParam {
|
|
||||||
name: "queue".to_string(),
|
|
||||||
param_type: "String".to_string(),
|
|
||||||
required: true,
|
|
||||||
description: "Queue name for the runner to listen to".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
RpcMethod {
|
|
||||||
name: "run_job".to_string(),
|
|
||||||
description: "Run a job on the appropriate runner".to_string(),
|
|
||||||
params: vec![
|
|
||||||
RpcParam {
|
|
||||||
name: "secret".to_string(),
|
|
||||||
param_type: "String".to_string(),
|
|
||||||
required: true,
|
|
||||||
description: "Secret required for job execution".to_string(),
|
|
||||||
},
|
|
||||||
RpcParam {
|
|
||||||
name: "job_id".to_string(),
|
|
||||||
param_type: "String".to_string(),
|
|
||||||
required: true,
|
|
||||||
description: "Job ID".to_string(),
|
|
||||||
},
|
|
||||||
RpcParam {
|
|
||||||
name: "runner".to_string(),
|
|
||||||
param_type: "String".to_string(),
|
|
||||||
required: true,
|
|
||||||
description: "Name of the runner to execute the job".to_string(),
|
|
||||||
},
|
|
||||||
RpcParam {
|
|
||||||
name: "payload".to_string(),
|
|
||||||
param_type: "String".to_string(),
|
|
||||||
required: true,
|
|
||||||
description: "Job payload/script content".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
RpcMethod {
|
|
||||||
name: "remove_runner".to_string(),
|
|
||||||
description: "Remove a runner from the supervisor".to_string(),
|
|
||||||
params: vec![
|
|
||||||
RpcParam {
|
|
||||||
name: "actor_id".to_string(),
|
|
||||||
param_type: "String".to_string(),
|
|
||||||
required: true,
|
|
||||||
description: "ID of the runner to remove".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
RpcMethod {
|
|
||||||
name: "start_runner".to_string(),
|
|
||||||
description: "Start a specific runner".to_string(),
|
|
||||||
params: vec![
|
|
||||||
RpcParam {
|
|
||||||
name: "actor_id".to_string(),
|
|
||||||
param_type: "String".to_string(),
|
|
||||||
required: true,
|
|
||||||
description: "ID of the runner to start".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
RpcMethod {
|
|
||||||
name: "stop_runner".to_string(),
|
|
||||||
description: "Stop a specific runner".to_string(),
|
|
||||||
params: vec![
|
|
||||||
RpcParam {
|
|
||||||
name: "actor_id".to_string(),
|
|
||||||
param_type: "String".to_string(),
|
|
||||||
required: true,
|
|
||||||
description: "ID of the runner to stop".to_string(),
|
|
||||||
},
|
|
||||||
RpcParam {
|
|
||||||
name: "force".to_string(),
|
|
||||||
param_type: "bool".to_string(),
|
|
||||||
required: true,
|
|
||||||
description: "Whether to force stop the runner".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
RpcMethod {
|
|
||||||
name: "get_runner_status".to_string(),
|
|
||||||
description: "Get the status of a specific runner".to_string(),
|
|
||||||
params: vec![
|
|
||||||
RpcParam {
|
|
||||||
name: "actor_id".to_string(),
|
|
||||||
param_type: "String".to_string(),
|
|
||||||
required: true,
|
|
||||||
description: "ID of the runner".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
RpcMethod {
|
|
||||||
name: "get_all_runner_status".to_string(),
|
|
||||||
description: "Get status of all runners".to_string(),
|
|
||||||
params: vec![],
|
|
||||||
},
|
|
||||||
RpcMethod {
|
|
||||||
name: "start_all".to_string(),
|
|
||||||
description: "Start all runners".to_string(),
|
|
||||||
params: vec![],
|
|
||||||
},
|
|
||||||
RpcMethod {
|
|
||||||
name: "stop_all".to_string(),
|
|
||||||
description: "Stop all runners".to_string(),
|
|
||||||
params: vec![
|
|
||||||
RpcParam {
|
|
||||||
name: "force".to_string(),
|
|
||||||
param_type: "bool".to_string(),
|
|
||||||
required: true,
|
|
||||||
description: "Whether to force stop all runners".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
RpcMethod {
|
|
||||||
name: "get_all_status".to_string(),
|
|
||||||
description: "Get status of all components".to_string(),
|
|
||||||
params: vec![],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut list_state = ListState::default();
|
|
||||||
list_state.select(Some(0));
|
|
||||||
|
|
||||||
Ok(App {
|
|
||||||
client,
|
|
||||||
methods,
|
|
||||||
list_state,
|
|
||||||
current_screen: Screen::MethodList,
|
|
||||||
selected_method: None,
|
|
||||||
param_inputs: vec![],
|
|
||||||
current_param_index: 0,
|
|
||||||
response: None,
|
|
||||||
error_message: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_method(&mut self) {
|
|
||||||
let i = match self.list_state.selected() {
|
|
||||||
Some(i) => {
|
|
||||||
if i >= self.methods.len() - 1 {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
self.list_state.select(Some(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn previous_method(&mut self) {
|
|
||||||
let i = match self.list_state.selected() {
|
|
||||||
Some(i) => {
|
|
||||||
if i == 0 {
|
|
||||||
self.methods.len() - 1
|
|
||||||
} else {
|
|
||||||
i - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
self.list_state.select(Some(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn select_method(&mut self) {
|
|
||||||
if let Some(i) = self.list_state.selected() {
|
|
||||||
let method = self.methods[i].clone();
|
|
||||||
if method.params.is_empty() {
|
|
||||||
// No parameters needed, call directly
|
|
||||||
self.selected_method = Some(method);
|
|
||||||
self.current_screen = Screen::Response;
|
|
||||||
} else {
|
|
||||||
// Parameters needed, go to input screen
|
|
||||||
self.selected_method = Some(method.clone());
|
|
||||||
self.param_inputs = vec!["".to_string(); method.params.len()];
|
|
||||||
self.current_param_index = 0;
|
|
||||||
self.current_screen = Screen::ParamInput;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_param(&mut self) {
|
|
||||||
if let Some(method) = &self.selected_method {
|
|
||||||
if self.current_param_index < method.params.len() - 1 {
|
|
||||||
self.current_param_index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn previous_param(&mut self) {
|
|
||||||
if self.current_param_index > 0 {
|
|
||||||
self.current_param_index -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_char_to_current_param(&mut self, c: char) {
|
|
||||||
if self.current_param_index < self.param_inputs.len() {
|
|
||||||
self.param_inputs[self.current_param_index].push(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_char_from_current_param(&mut self) {
|
|
||||||
if self.current_param_index < self.param_inputs.len() {
|
|
||||||
self.param_inputs[self.current_param_index].pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute_method(&mut self) {
|
|
||||||
if let Some(method) = &self.selected_method {
|
|
||||||
self.error_message = None;
|
|
||||||
self.response = None;
|
|
||||||
|
|
||||||
// Build parameters
|
|
||||||
let mut params = json!({});
|
|
||||||
|
|
||||||
if !method.params.is_empty() {
|
|
||||||
for (i, param) in method.params.iter().enumerate() {
|
|
||||||
let input = &self.param_inputs[i];
|
|
||||||
if input.is_empty() && param.required {
|
|
||||||
self.error_message = Some(format!("Required parameter '{}' is empty", param.name));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !input.is_empty() {
|
|
||||||
let value = match param.param_type.as_str() {
|
|
||||||
"bool" => {
|
|
||||||
match input.to_lowercase().as_str() {
|
|
||||||
"true" | "1" | "yes" => json!(true),
|
|
||||||
"false" | "0" | "no" => json!(false),
|
|
||||||
_ => {
|
|
||||||
self.error_message = Some(format!("Invalid boolean value for '{}': {}", param.name, input));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"i32" | "i64" | "u32" | "u64" => {
|
|
||||||
match input.parse::<i64>() {
|
|
||||||
Ok(n) => json!(n),
|
|
||||||
Err(_) => {
|
|
||||||
self.error_message = Some(format!("Invalid number for '{}': {}", param.name, input));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => json!(input),
|
|
||||||
};
|
|
||||||
|
|
||||||
if method.name == "register_runner" {
|
|
||||||
// Special handling for register_runner method
|
|
||||||
match param.name.as_str() {
|
|
||||||
"secret" => params["secret"] = value,
|
|
||||||
"name" => params["name"] = value,
|
|
||||||
"queue" => params["queue"] = value,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
} else if method.name == "run_job" {
|
|
||||||
// Special handling for run_job method
|
|
||||||
match param.name.as_str() {
|
|
||||||
"secret" => params["secret"] = value,
|
|
||||||
"job_id" => params["job_id"] = value,
|
|
||||||
"runner" => params["runner"] = value,
|
|
||||||
"payload" => params["payload"] = value,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
params[¶m.name] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the method
|
|
||||||
let result: Result<serde_json::Value, hero_supervisor_openrpc_client::ClientError> = match method.name.as_str() {
|
|
||||||
"list_runners" => {
|
|
||||||
match self.client.list_runners().await {
|
|
||||||
Ok(response) => {
|
|
||||||
match serde_json::to_value(response) {
|
|
||||||
Ok(value) => Ok(value),
|
|
||||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"get_all_runner_status" => {
|
|
||||||
match self.client.get_all_runner_status().await {
|
|
||||||
Ok(response) => {
|
|
||||||
match serde_json::to_value(response) {
|
|
||||||
Ok(value) => Ok(value),
|
|
||||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"start_all" => {
|
|
||||||
match self.client.start_all().await {
|
|
||||||
Ok(response) => {
|
|
||||||
match serde_json::to_value(response) {
|
|
||||||
Ok(value) => Ok(value),
|
|
||||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"get_all_status" => {
|
|
||||||
match self.client.get_all_status().await {
|
|
||||||
Ok(response) => {
|
|
||||||
match serde_json::to_value(response) {
|
|
||||||
Ok(value) => Ok(value),
|
|
||||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"stop_all" => {
|
|
||||||
let force = params.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
|
|
||||||
match self.client.stop_all(force).await {
|
|
||||||
Ok(response) => {
|
|
||||||
match serde_json::to_value(response) {
|
|
||||||
Ok(value) => Ok(value),
|
|
||||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"start_runner" => {
|
|
||||||
if let Some(actor_id) = params.get("actor_id").and_then(|v| v.as_str()) {
|
|
||||||
match self.client.start_runner(actor_id).await {
|
|
||||||
Ok(response) => {
|
|
||||||
match serde_json::to_value(response) {
|
|
||||||
Ok(value) => Ok(value),
|
|
||||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(hero_supervisor_openrpc_client::ClientError::from(
|
|
||||||
serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing actor_id parameter"))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"stop_runner" => {
|
|
||||||
if let (Some(actor_id), Some(force)) = (
|
|
||||||
params.get("actor_id").and_then(|v| v.as_str()),
|
|
||||||
params.get("force").and_then(|v| v.as_bool())
|
|
||||||
) {
|
|
||||||
match self.client.stop_runner(actor_id, force).await {
|
|
||||||
Ok(response) => {
|
|
||||||
match serde_json::to_value(response) {
|
|
||||||
Ok(value) => Ok(value),
|
|
||||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(hero_supervisor_openrpc_client::ClientError::from(
|
|
||||||
serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing parameters"))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"remove_runner" => {
|
|
||||||
if let Some(actor_id) = params.get("actor_id").and_then(|v| v.as_str()) {
|
|
||||||
match self.client.remove_runner(actor_id).await {
|
|
||||||
Ok(response) => {
|
|
||||||
match serde_json::to_value(response) {
|
|
||||||
Ok(value) => Ok(value),
|
|
||||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(hero_supervisor_openrpc_client::ClientError::from(
|
|
||||||
serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing actor_id parameter"))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"get_runner_status" => {
|
|
||||||
if let Some(actor_id) = params.get("actor_id").and_then(|v| v.as_str()) {
|
|
||||||
match self.client.get_runner_status(actor_id).await {
|
|
||||||
Ok(response) => {
|
|
||||||
match serde_json::to_value(response) {
|
|
||||||
Ok(value) => Ok(value),
|
|
||||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(hero_supervisor_openrpc_client::ClientError::from(
|
|
||||||
serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing actor_id parameter"))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"register_runner" => {
|
|
||||||
if let (Some(secret), Some(name), Some(queue)) = (
|
|
||||||
params.get("secret").and_then(|v| v.as_str()),
|
|
||||||
params.get("name").and_then(|v| v.as_str()),
|
|
||||||
params.get("queue").and_then(|v| v.as_str())
|
|
||||||
) {
|
|
||||||
match self.client.register_runner(secret, name, queue).await {
|
|
||||||
Ok(response) => {
|
|
||||||
match serde_json::to_value(response) {
|
|
||||||
Ok(value) => Ok(value),
|
|
||||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(hero_supervisor_openrpc_client::ClientError::from(
|
|
||||||
serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing required parameters: secret, name, queue"))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"run_job" => {
|
|
||||||
if let (Some(secret), Some(job_id), Some(runner), Some(payload)) = (
|
|
||||||
params.get("secret").and_then(|v| v.as_str()),
|
|
||||||
params.get("job_id").and_then(|v| v.as_str()),
|
|
||||||
params.get("runner").and_then(|v| v.as_str()),
|
|
||||||
params.get("payload").and_then(|v| v.as_str())
|
|
||||||
) {
|
|
||||||
// Create a job object
|
|
||||||
let job = serde_json::json!({
|
|
||||||
"id": job_id,
|
|
||||||
"caller_id": "cli_user",
|
|
||||||
"context_id": "cli_context",
|
|
||||||
"payload": payload,
|
|
||||||
"job_type": "SAL",
|
|
||||||
"runner": runner,
|
|
||||||
"timeout": 30000000000u64, // 30 seconds in nanoseconds
|
|
||||||
"env_vars": {},
|
|
||||||
"created_at": chrono::Utc::now().to_rfc3339(),
|
|
||||||
"updated_at": chrono::Utc::now().to_rfc3339()
|
|
||||||
});
|
|
||||||
|
|
||||||
match self.client.run_job(secret, job).await {
|
|
||||||
Ok(response) => {
|
|
||||||
match serde_json::to_value(response) {
|
|
||||||
Ok(value) => Ok(value),
|
|
||||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(hero_supervisor_openrpc_client::ClientError::from(
|
|
||||||
serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing required parameters: secret, job_id, runner, payload"))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(hero_supervisor_openrpc_client::ClientError::from(
|
|
||||||
serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Method not implemented in CLI"))
|
|
||||||
)),
|
|
||||||
};
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(response) => {
|
|
||||||
self.response = Some(format!("{:#}", response));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
self.error_message = Some(format!("Error: {}", e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.current_screen = Screen::Response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn back_to_methods(&mut self) {
|
|
||||||
self.current_screen = Screen::MethodList;
|
|
||||||
self.selected_method = None;
|
|
||||||
self.param_inputs.clear();
|
|
||||||
self.current_param_index = 0;
|
|
||||||
self.response = None;
|
|
||||||
self.error_message = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ui(f: &mut Frame, app: &mut App) {
|
|
||||||
match app.current_screen {
|
|
||||||
Screen::MethodList => draw_method_list(f, app),
|
|
||||||
Screen::ParamInput => draw_param_input(f, app),
|
|
||||||
Screen::Response => draw_response(f, app),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_method_list(f: &mut Frame, app: &mut App) {
|
|
||||||
let chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.margin(1)
|
|
||||||
.constraints([Constraint::Min(0)].as_ref())
|
|
||||||
.split(f.area());
|
|
||||||
|
|
||||||
let items: Vec<ListItem> = app
|
|
||||||
.methods
|
|
||||||
.iter()
|
|
||||||
.map(|method| {
|
|
||||||
let content = vec![Line::from(vec![
|
|
||||||
Span::styled(&method.name, Style::default().fg(Color::Yellow)),
|
|
||||||
Span::raw(" - "),
|
|
||||||
Span::raw(&method.description),
|
|
||||||
])];
|
|
||||||
ListItem::new(content)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let items = List::new(items)
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.title("OpenRPC Methods (↑↓ to navigate, Enter to select, q to quit)"),
|
|
||||||
)
|
|
||||||
.highlight_style(
|
|
||||||
Style::default()
|
|
||||||
.bg(Color::LightGreen)
|
|
||||||
.fg(Color::Black)
|
|
||||||
.add_modifier(Modifier::BOLD),
|
|
||||||
)
|
|
||||||
.highlight_symbol(">> ");
|
|
||||||
|
|
||||||
f.render_stateful_widget(items, chunks[0], &mut app.list_state);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_param_input(f: &mut Frame, app: &mut App) {
|
|
||||||
if let Some(method) = &app.selected_method {
|
|
||||||
let chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.margin(1)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Length(3),
|
|
||||||
Constraint::Min(0),
|
|
||||||
Constraint::Length(3),
|
|
||||||
])
|
|
||||||
.split(f.area());
|
|
||||||
|
|
||||||
// Title
|
|
||||||
let title = Paragraph::new(format!("Parameters for: {}", method.name))
|
|
||||||
.block(Block::default().borders(Borders::ALL).title("Method"));
|
|
||||||
f.render_widget(title, chunks[0]);
|
|
||||||
|
|
||||||
// Parameters - create proper form layout with separate label and input areas
|
|
||||||
let param_chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints(vec![Constraint::Length(5); method.params.len()])
|
|
||||||
.split(chunks[1]);
|
|
||||||
|
|
||||||
for (i, param) in method.params.iter().enumerate() {
|
|
||||||
let is_current = i == app.current_param_index;
|
|
||||||
|
|
||||||
// Split each parameter into label and input areas
|
|
||||||
let param_layout = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([Constraint::Length(2), Constraint::Length(3)])
|
|
||||||
.split(param_chunks[i]);
|
|
||||||
|
|
||||||
// Parameter label and description
|
|
||||||
let label_style = if is_current {
|
|
||||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
|
||||||
} else {
|
|
||||||
Style::default().fg(Color::White)
|
|
||||||
};
|
|
||||||
|
|
||||||
let label_text = vec![
|
|
||||||
Line::from(vec![
|
|
||||||
Span::styled(¶m.name, label_style),
|
|
||||||
Span::raw(if param.required { " (required)" } else { " (optional)" }),
|
|
||||||
Span::raw(format!(" [{}]", param.param_type)),
|
|
||||||
]),
|
|
||||||
Line::from(Span::styled(¶m.description, Style::default().fg(Color::Gray))),
|
|
||||||
];
|
|
||||||
|
|
||||||
let label_widget = Paragraph::new(label_text)
|
|
||||||
.block(Block::default().borders(Borders::NONE));
|
|
||||||
f.render_widget(label_widget, param_layout[0]);
|
|
||||||
|
|
||||||
// Input field
|
|
||||||
let empty_string = String::new();
|
|
||||||
let input_value = app.param_inputs.get(i).unwrap_or(&empty_string);
|
|
||||||
|
|
||||||
let input_display = if is_current {
|
|
||||||
if input_value.is_empty() {
|
|
||||||
"█".to_string() // Show cursor when active and empty
|
|
||||||
} else {
|
|
||||||
format!("{}█", input_value) // Show cursor at end when active
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if input_value.is_empty() {
|
|
||||||
" ".to_string() // Empty space for inactive empty fields
|
|
||||||
} else {
|
|
||||||
input_value.clone()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let input_style = if is_current {
|
|
||||||
Style::default().fg(Color::Black).bg(Color::Cyan)
|
|
||||||
} else {
|
|
||||||
Style::default().fg(Color::White).bg(Color::DarkGray)
|
|
||||||
};
|
|
||||||
|
|
||||||
let border_style = if is_current {
|
|
||||||
Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)
|
|
||||||
} else {
|
|
||||||
Style::default().fg(Color::Gray)
|
|
||||||
};
|
|
||||||
|
|
||||||
let input_widget = Paragraph::new(Line::from(Span::styled(input_display, input_style)))
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(border_style)
|
|
||||||
.title(if is_current { " INPUT " } else { "" }),
|
|
||||||
);
|
|
||||||
|
|
||||||
f.render_widget(input_widget, param_layout[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instructions
|
|
||||||
let instructions = Paragraph::new("↑↓ to navigate params, type to edit, Enter to execute, Esc to go back")
|
|
||||||
.block(Block::default().borders(Borders::ALL).title("Instructions"));
|
|
||||||
f.render_widget(instructions, chunks[2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_response(f: &mut Frame, app: &mut App) {
|
|
||||||
let chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.margin(1)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Length(3),
|
|
||||||
Constraint::Min(0),
|
|
||||||
Constraint::Length(3),
|
|
||||||
])
|
|
||||||
.split(f.area());
|
|
||||||
|
|
||||||
// Title
|
|
||||||
let method_name = app.selected_method.as_ref().map(|m| m.name.as_str()).unwrap_or("Unknown");
|
|
||||||
let title = Paragraph::new(format!("Response for: {}", method_name))
|
|
||||||
.block(Block::default().borders(Borders::ALL).title("Response"));
|
|
||||||
f.render_widget(title, chunks[0]);
|
|
||||||
|
|
||||||
// Response content
|
|
||||||
let content = if let Some(error) = &app.error_message {
|
|
||||||
Text::from(error.clone()).style(Style::default().fg(Color::Red))
|
|
||||||
} else if let Some(response) = &app.response {
|
|
||||||
Text::from(response.clone()).style(Style::default().fg(Color::Green))
|
|
||||||
} else {
|
|
||||||
Text::from("Executing...").style(Style::default().fg(Color::Yellow))
|
|
||||||
};
|
|
||||||
|
|
||||||
let response_widget = Paragraph::new(content)
|
|
||||||
.block(Block::default().borders(Borders::ALL))
|
|
||||||
.wrap(Wrap { trim: true });
|
|
||||||
f.render_widget(response_widget, chunks[1]);
|
|
||||||
|
|
||||||
// Instructions
|
|
||||||
let instructions = Paragraph::new("Esc to go back to methods")
|
|
||||||
.block(Block::default().borders(Borders::ALL).title("Instructions"));
|
|
||||||
f.render_widget(instructions, chunks[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let cli = Cli::parse();
|
|
||||||
|
|
||||||
// Setup terminal
|
|
||||||
enable_raw_mode()?;
|
|
||||||
let mut stdout = io::stdout();
|
|
||||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
|
||||||
let backend = CrosstermBackend::new(stdout);
|
|
||||||
let mut terminal = Terminal::new(backend)?;
|
|
||||||
|
|
||||||
// Create app
|
|
||||||
let mut app = match App::new(cli.url).await {
|
|
||||||
Ok(app) => app,
|
|
||||||
Err(e) => {
|
|
||||||
// Cleanup terminal before showing error
|
|
||||||
disable_raw_mode()?;
|
|
||||||
execute!(
|
|
||||||
terminal.backend_mut(),
|
|
||||||
LeaveAlternateScreen,
|
|
||||||
DisableMouseCapture
|
|
||||||
)?;
|
|
||||||
terminal.show_cursor()?;
|
|
||||||
|
|
||||||
eprintln!("Failed to connect to OpenRPC server: {}", e);
|
|
||||||
eprintln!("Make sure the supervisor is running with OpenRPC enabled.");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Main loop
|
|
||||||
loop {
|
|
||||||
terminal.draw(|f| ui(f, &mut app))?;
|
|
||||||
|
|
||||||
if let Event::Key(key) = event::read()? {
|
|
||||||
if key.kind == KeyEventKind::Press {
|
|
||||||
match app.current_screen {
|
|
||||||
Screen::MethodList => {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Char('q') => break,
|
|
||||||
KeyCode::Down => app.next_method(),
|
|
||||||
KeyCode::Up => app.previous_method(),
|
|
||||||
KeyCode::Enter => {
|
|
||||||
app.select_method();
|
|
||||||
// If the selected method has no parameters, execute it immediately
|
|
||||||
if let Some(method) = &app.selected_method {
|
|
||||||
if method.params.is_empty() {
|
|
||||||
app.execute_method().await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Screen::ParamInput => {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Esc => app.back_to_methods(),
|
|
||||||
KeyCode::Up => app.previous_param(),
|
|
||||||
KeyCode::Down => app.next_param(),
|
|
||||||
KeyCode::Enter => {
|
|
||||||
app.execute_method().await;
|
|
||||||
}
|
|
||||||
KeyCode::Backspace => app.remove_char_from_current_param(),
|
|
||||||
KeyCode::Char(c) => app.add_char_to_current_param(c),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Screen::Response => {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Esc => app.back_to_methods(),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore terminal
|
|
||||||
disable_raw_mode()?;
|
|
||||||
execute!(
|
|
||||||
terminal.backend_mut(),
|
|
||||||
LeaveAlternateScreen,
|
|
||||||
DisableMouseCapture
|
|
||||||
)?;
|
|
||||||
terminal.show_cursor()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -171,8 +171,13 @@ pub struct JobStatusResponse {
|
|||||||
pub completed_at: Option<String>,
|
pub completed_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-export Job types from shared crate
|
// Re-export Job types from runner_rust crate (native only)
|
||||||
pub use hero_job::{Job, JobStatus, JobError, JobBuilder};
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub use runner_rust::{Job, JobStatus, JobError, JobBuilder, Client, ClientBuilder};
|
||||||
|
|
||||||
|
// WASM-compatible Job types (simplified versions)
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub use crate::wasm::{Job, JobStatus, JobError, JobBuilder};
|
||||||
|
|
||||||
/// Process status wrapper for OpenRPC serialization (matches server response)
|
/// Process status wrapper for OpenRPC serialization (matches server response)
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ use wasm_bindgen::prelude::*;
|
|||||||
use wasm_bindgen_futures::JsFuture;
|
use wasm_bindgen_futures::JsFuture;
|
||||||
use web_sys::{Request, RequestInit, RequestMode, Response, Headers};
|
use web_sys::{Request, RequestInit, RequestMode, Response, Headers};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
// use std::collections::HashMap; // Unused
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
// use js_sys::Promise; // Unused
|
|
||||||
|
|
||||||
/// WASM-compatible client for communicating with Hero Supervisor OpenRPC server
|
/// WASM-compatible client for communicating with Hero Supervisor OpenRPC server
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@@ -87,6 +85,102 @@ pub enum WasmJobType {
|
|||||||
V,
|
V,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Job status enumeration
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum JobStatus {
|
||||||
|
Pending,
|
||||||
|
Running,
|
||||||
|
Finished,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Job error type
|
||||||
|
#[derive(Debug, Clone, thiserror::Error)]
|
||||||
|
pub enum JobError {
|
||||||
|
#[error("Validation error: {0}")]
|
||||||
|
Validation(String),
|
||||||
|
#[error("Execution error: {0}")]
|
||||||
|
Execution(String),
|
||||||
|
#[error("Timeout error")]
|
||||||
|
Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Job builder for WASM
|
||||||
|
pub struct JobBuilder {
|
||||||
|
id: Option<String>,
|
||||||
|
caller_id: Option<String>,
|
||||||
|
context_id: Option<String>,
|
||||||
|
payload: Option<String>,
|
||||||
|
runner: Option<String>,
|
||||||
|
executor: Option<String>,
|
||||||
|
timeout_secs: Option<u64>,
|
||||||
|
env_vars: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobBuilder {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: None,
|
||||||
|
caller_id: None,
|
||||||
|
context_id: None,
|
||||||
|
payload: None,
|
||||||
|
runner: None,
|
||||||
|
executor: None,
|
||||||
|
timeout_secs: None,
|
||||||
|
env_vars: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn caller_id(mut self, caller_id: &str) -> Self {
|
||||||
|
self.caller_id = Some(caller_id.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn context_id(mut self, context_id: &str) -> Self {
|
||||||
|
self.context_id = Some(context_id.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn payload(mut self, payload: &str) -> Self {
|
||||||
|
self.payload = Some(payload.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runner(mut self, runner: &str) -> Self {
|
||||||
|
self.runner = Some(runner.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn executor(mut self, executor: &str) -> Self {
|
||||||
|
self.executor = Some(executor.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn timeout(mut self, timeout_secs: u64) -> Self {
|
||||||
|
self.timeout_secs = Some(timeout_secs);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> Result<Job, JobError> {
|
||||||
|
let now = chrono::Utc::now().to_rfc3339();
|
||||||
|
Ok(Job {
|
||||||
|
id: self.id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
|
||||||
|
caller_id: self.caller_id.ok_or_else(|| JobError::Validation("caller_id is required".to_string()))?,
|
||||||
|
context_id: self.context_id.ok_or_else(|| JobError::Validation("context_id is required".to_string()))?,
|
||||||
|
payload: self.payload.ok_or_else(|| JobError::Validation("payload is required".to_string()))?,
|
||||||
|
runner: self.runner.ok_or_else(|| JobError::Validation("runner is required".to_string()))?,
|
||||||
|
executor: self.executor.ok_or_else(|| JobError::Validation("executor is required".to_string()))?,
|
||||||
|
timeout_secs: self.timeout_secs.unwrap_or(30),
|
||||||
|
env_vars: self.env_vars.unwrap_or_else(|| "{}".to_string()),
|
||||||
|
created_at: now.clone(),
|
||||||
|
updated_at: now,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Job structure for creating and managing jobs (alias for WasmJob)
|
||||||
|
pub type Job = WasmJob;
|
||||||
|
|
||||||
/// Job structure for creating and managing jobs
|
/// Job structure for creating and managing jobs
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
use hero_supervisor_openrpc_client::{
|
use hero_supervisor_openrpc_client::{
|
||||||
SupervisorClient, RunnerConfig, RunnerType, ProcessManagerType,
|
SupervisorClient, RunnerConfig, RunnerType, ProcessManagerType,
|
||||||
JobBuilder, JobType
|
JobBuilder
|
||||||
};
|
};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use escargot::CargoBuild;
|
use escargot::CargoBuild;
|
||||||
@@ -135,8 +135,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.caller_id("comprehensive_client")
|
.caller_id("comprehensive_client")
|
||||||
.context_id("demo")
|
.context_id("demo")
|
||||||
.payload(payload)
|
.payload(payload)
|
||||||
.job_type(JobType::OSIS)
|
|
||||||
.runner("basic_example_actor")
|
.runner("basic_example_actor")
|
||||||
|
.executor("rhai")
|
||||||
.timeout(30)
|
.timeout(30)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
@@ -163,8 +163,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.caller_id("sync_client")
|
.caller_id("sync_client")
|
||||||
.context_id("sync_demo")
|
.context_id("sync_demo")
|
||||||
.payload(payload)
|
.payload(payload)
|
||||||
.job_type(JobType::OSIS)
|
|
||||||
.runner("basic_example_actor")
|
.runner("basic_example_actor")
|
||||||
|
.executor("rhai")
|
||||||
.timeout(30)
|
.timeout(30)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
|
|||||||
@@ -188,3 +188,9 @@ async fn test_nonexistent_job_operations() -> Result<(), Box<dyn std::error::Err
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("Integration test example - this would contain test logic");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -213,13 +213,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
println!("-------------------------------");
|
println!("-------------------------------");
|
||||||
|
|
||||||
match client.jobs_list().await {
|
match client.jobs_list().await {
|
||||||
Ok(job_ids) => {
|
Ok(jobs) => {
|
||||||
println!("✅ Found {} jobs in the system:", job_ids.len());
|
println!("✅ Found {} jobs in the system:", jobs.len());
|
||||||
for (i, job_id) in job_ids.iter().take(10).enumerate() {
|
for (i, job) in jobs.iter().take(10).enumerate() {
|
||||||
println!(" {}. {}", i + 1, job_id);
|
println!(" {}. {}", i + 1, job.id);
|
||||||
}
|
}
|
||||||
if job_ids.len() > 10 {
|
if jobs.len() > 10 {
|
||||||
println!(" ... and {} more", job_ids.len() - 10);
|
println!(" ... and {} more", jobs.len() - 10);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use std::time::Duration;
|
|||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use redis::AsyncCommands;
|
use redis::AsyncCommands;
|
||||||
use hero_supervisor::{
|
use hero_supervisor::{
|
||||||
Job, JobStatus, JobError, client::{Client, ClientBuilder}
|
Job, JobStatus, JobError, Client, ClientBuilder
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ impl SupervisorApp {
|
|||||||
|
|
||||||
/// Start the Mycelium integration
|
/// Start the Mycelium integration
|
||||||
async fn start_mycelium_integration(&self) -> Result<(), Box<dyn std::error::Error>> {
|
async fn start_mycelium_integration(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Skip Mycelium if URL is empty
|
||||||
|
if self.mycelium_url.is_empty() {
|
||||||
|
info!("Mycelium integration disabled (no URL provided)");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
info!("Starting Mycelium integration...");
|
info!("Starting Mycelium integration...");
|
||||||
|
|
||||||
let supervisor_for_mycelium = Arc::new(Mutex::new(self.supervisor.clone()));
|
let supervisor_for_mycelium = Arc::new(Mutex::new(self.supervisor.clone()));
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
// Re-export job types from the separate job crate
|
// Re-export job types from the runner_rust crate
|
||||||
pub use hero_job::*;
|
pub use runner_rust::{Job, JobBuilder, JobStatus, JobError, Client, ClientBuilder};
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ pub mod mycelium;
|
|||||||
pub use runner::{Runner, RunnerConfig, RunnerResult, RunnerStatus};
|
pub use runner::{Runner, RunnerConfig, RunnerResult, RunnerStatus};
|
||||||
// pub use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager};
|
// pub use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager};
|
||||||
pub use supervisor::{Supervisor, SupervisorBuilder, ProcessManagerType};
|
pub use supervisor::{Supervisor, SupervisorBuilder, ProcessManagerType};
|
||||||
pub use hero_job::{Job, JobBuilder, JobStatus, JobError};
|
pub use runner_rust::{Job, JobBuilder, JobStatus, JobError, Client, ClientBuilder};
|
||||||
pub use hero_job::Client;
|
|
||||||
pub use app::SupervisorApp;
|
pub use app::SupervisorApp;
|
||||||
pub use mycelium::{MyceliumIntegration, MyceliumServer};
|
pub use mycelium::{MyceliumIntegration, MyceliumServer};
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ pub struct Runner {
|
|||||||
pub command: PathBuf, // Command to run runner by, used only if supervisor is used to run runners
|
pub command: PathBuf, // Command to run runner by, used only if supervisor is used to run runners
|
||||||
/// Redis URL for job queue
|
/// Redis URL for job queue
|
||||||
pub redis_url: String,
|
pub redis_url: String,
|
||||||
|
/// Additional command-line arguments
|
||||||
|
pub extra_args: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Runner {
|
impl Runner {
|
||||||
@@ -79,6 +81,7 @@ impl Runner {
|
|||||||
namespace: config.namespace,
|
namespace: config.namespace,
|
||||||
command: config.command,
|
command: config.command,
|
||||||
redis_url: config.redis_url,
|
redis_url: config.redis_url,
|
||||||
|
extra_args: config.extra_args,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +99,26 @@ impl Runner {
|
|||||||
namespace,
|
namespace,
|
||||||
command,
|
command,
|
||||||
redis_url,
|
redis_url,
|
||||||
|
extra_args: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new runner with extra arguments
|
||||||
|
pub fn with_args(
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
namespace: String,
|
||||||
|
command: PathBuf,
|
||||||
|
redis_url: String,
|
||||||
|
extra_args: Vec<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
namespace,
|
||||||
|
command,
|
||||||
|
redis_url,
|
||||||
|
extra_args,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +186,7 @@ pub enum RunnerError {
|
|||||||
#[error("Job error: {source}")]
|
#[error("Job error: {source}")]
|
||||||
JobError {
|
JobError {
|
||||||
#[from]
|
#[from]
|
||||||
source: hero_job::JobError,
|
source: runner_rust::JobError,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("Job '{job_id}' not found")]
|
#[error("Job '{job_id}' not found")]
|
||||||
@@ -178,13 +201,15 @@ pub type RunnerConfig = Runner;
|
|||||||
|
|
||||||
/// Convert Runner to ProcessConfig
|
/// Convert Runner to ProcessConfig
|
||||||
pub fn runner_to_process_config(config: &Runner) -> ProcessConfig {
|
pub fn runner_to_process_config(config: &Runner) -> ProcessConfig {
|
||||||
let args = vec![
|
let mut args = vec![
|
||||||
"--id".to_string(),
|
config.id.clone(), // First positional argument is the runner ID
|
||||||
config.id.clone(),
|
|
||||||
"--redis-url".to_string(),
|
"--redis-url".to_string(),
|
||||||
config.redis_url.clone(),
|
config.redis_url.clone(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Add extra arguments (e.g., context configurations)
|
||||||
|
args.extend(config.extra_args.clone());
|
||||||
|
|
||||||
ProcessConfig::new(
|
ProcessConfig::new(
|
||||||
config.command.to_string_lossy().to_string(),
|
config.command.to_string_lossy().to_string(),
|
||||||
args,
|
args,
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ use tokio::sync::Mutex;
|
|||||||
// use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager};
|
// use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager};
|
||||||
|
|
||||||
use crate::{job::JobStatus, runner::{LogInfo, Runner, RunnerConfig, RunnerError, RunnerResult, RunnerStatus}};
|
use crate::{job::JobStatus, runner::{LogInfo, Runner, RunnerConfig, RunnerError, RunnerResult, RunnerStatus}};
|
||||||
use hero_job::{Client, client::ClientBuilder};
|
use runner_rust::{Client, ClientBuilder};
|
||||||
|
|
||||||
|
|
||||||
/// Process manager type for a runner
|
/// Process manager type for a runner
|
||||||
@@ -280,6 +280,7 @@ impl Supervisor {
|
|||||||
namespace: self.namespace.clone(),
|
namespace: self.namespace.clone(),
|
||||||
command: PathBuf::from("/tmp/mock_runner"), // Default path
|
command: PathBuf::from("/tmp/mock_runner"), // Default path
|
||||||
redis_url: "redis://localhost:6379".to_string(),
|
redis_url: "redis://localhost:6379".to_string(),
|
||||||
|
extra_args: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the runner using existing logic
|
// Add the runner using existing logic
|
||||||
|
|||||||
Reference in New Issue
Block a user