This repository has been archived on 2025-11-14. You can view files and clone it, but cannot push or open issues or pull requests.
Files
supervisor/client/src/lib.rs

632 lines
19 KiB
Rust

//! OpenRPC client for Hero Supervisor
//!
//! This crate provides a client library for interacting with the Hero Supervisor
//! OpenRPC server. It offers a simple, async interface for managing actors and jobs.
//!
//! ## Features
//!
//! - **Native client**: Full-featured client for native Rust applications
//! - **WASM client**: Browser-compatible client using fetch APIs
//!
//! ## Usage
//!
//! ### Native Client
//! ```rust
//! use hero_supervisor_openrpc_client::SupervisorClient;
//!
//! let client = SupervisorClient::new("http://localhost:3030")?;
//! let runners = client.list_runners().await?;
//! ```
//!
//! ### WASM Client
//! ```rust
//! use hero_supervisor_openrpc_client::wasm::WasmSupervisorClient;
//!
//! let client = WasmSupervisorClient::new("http://localhost:3030".to_string());
//! let runners = client.list_runners().await?;
//! ```
use serde::{Deserialize, Serialize};
use thiserror::Error;
use serde_json;
// Import types from the main supervisor crate
// WASM-compatible client module
#[cfg(target_arch = "wasm32")]
pub mod wasm;
// Re-export WASM types for convenience
#[cfg(target_arch = "wasm32")]
pub use wasm::{WasmSupervisorClient, WasmJobType, WasmRunnerType, create_job_canonical_repr, sign_job_canonical};
// Native client dependencies
#[cfg(not(target_arch = "wasm32"))]
use jsonrpsee::{
core::client::ClientT,
http_client::{HttpClient, HttpClientBuilder},
rpc_params,
};
#[cfg(not(target_arch = "wasm32"))]
use std::path::PathBuf;
/// Client for communicating with Hero Supervisor OpenRPC server
#[cfg(not(target_arch = "wasm32"))]
pub struct SupervisorClient {
client: HttpClient,
server_url: String,
secret: Option<String>,
}
/// Error types for client operations
#[cfg(not(target_arch = "wasm32"))]
#[derive(Error, Debug)]
pub enum ClientError {
#[error("JSON-RPC error: {0}")]
JsonRpc(#[from] jsonrpsee::core::ClientError),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("HTTP client error: {0}")]
Http(String),
#[error("Server error: {message}")]
Server { message: String },
}
/// Error types for WASM client operations
#[cfg(target_arch = "wasm32")]
#[derive(Error, Debug)]
pub enum ClientError {
#[error("JSON-RPC error: {0}")]
JsonRpc(String),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("HTTP client error: {0}")]
Http(String),
#[error("Server error: {message}")]
Server { message: String },
#[error("JavaScript error: {0}")]
JavaScript(String),
#[error("Network error: {0}")]
Network(String),
}
// Implement From for jsonrpsee ClientError for WASM
#[cfg(target_arch = "wasm32")]
impl From<wasm_bindgen::JsValue> for ClientError {
fn from(js_val: wasm_bindgen::JsValue) -> Self {
ClientError::JavaScript(format!("{:?}", js_val))
}
}
/// Result type for client operations
pub type ClientResult<T> = Result<T, ClientError>;
/// Configuration for a runner
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunnerConfig {
/// Name of the runner
pub name: String,
/// Command to run the runner (full command line)
pub command: String,
/// Optional environment variables
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<std::collections::HashMap<String, String>>,
}
/// Job result response
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum JobResult {
Success { success: String },
Error { error: String },
}
/// Job status response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobStatusResponse {
pub job_id: String,
pub status: String,
pub created_at: String,
pub started_at: Option<String>,
pub completed_at: Option<String>,
}
/// Response from job.run (blocking execution)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobRunResponse {
pub job_id: String,
pub status: String,
pub result: Option<String>,
}
/// Response from job.start (non-blocking execution)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobStartResponse {
pub job_id: String,
pub status: String,
}
// Re-export Job types from hero-job crate (both native and WASM)
pub use hero_job::{Job, JobStatus, JobError, JobBuilder, JobSignature};
// Re-export Client from hero-job-client (native only, requires Redis)
#[cfg(not(target_arch = "wasm32"))]
pub use hero_job_client::{Client, ClientBuilder};
/// Process status wrapper for OpenRPC serialization (matches server response)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ProcessStatusWrapper {
Running,
Stopped,
Starting,
Stopping,
Error(String),
}
/// Log information wrapper for OpenRPC serialization (matches server response)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogInfoWrapper {
pub timestamp: String,
pub level: String,
pub message: String,
}
/// Supervisor information response containing secret counts and server details
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SupervisorInfo {
pub server_url: String,
pub admin_secrets_count: usize,
pub user_secrets_count: usize,
pub register_secrets_count: usize,
pub runners_count: usize,
}
/// Simple ProcessStatus type for native builds to avoid service manager dependency
#[cfg(not(target_arch = "wasm32"))]
pub type ProcessStatus = ProcessStatusWrapper;
/// Re-export types from supervisor crate for native builds
#[cfg(not(target_arch = "wasm32"))]
pub use hero_supervisor::RunnerStatus;
#[cfg(not(target_arch = "wasm32"))]
pub use hero_supervisor::runner::LogInfo;
/// Type aliases for WASM compatibility
#[cfg(target_arch = "wasm32")]
pub type ProcessStatus = ProcessStatusWrapper;
#[cfg(target_arch = "wasm32")]
pub type RunnerStatus = ProcessStatusWrapper;
#[cfg(target_arch = "wasm32")]
pub type LogInfo = LogInfoWrapper;
#[cfg(not(target_arch = "wasm32"))]
impl SupervisorClient {
/// Create a new supervisor client
pub fn new(server_url: impl Into<String>) -> ClientResult<Self> {
let server_url = server_url.into();
let client = HttpClientBuilder::default()
.request_timeout(std::time::Duration::from_secs(30))
.build(&server_url)
.map_err(|e| ClientError::Http(e.to_string()))?;
Ok(Self {
client,
server_url,
secret: None,
})
}
/// Create a new supervisor client with authentication secret
pub fn with_secret(server_url: impl Into<String>, secret: impl Into<String>) -> ClientResult<Self> {
let server_url = server_url.into();
let secret = secret.into();
let client = HttpClientBuilder::default()
.request_timeout(std::time::Duration::from_secs(30))
.build(&server_url)
.map_err(|e| ClientError::Http(e.to_string()))?;
Ok(Self {
client,
server_url,
secret: Some(secret),
})
}
/// Get the server URL
pub fn server_url(&self) -> &str {
&self.server_url
}
/// Test connection using OpenRPC discovery method
/// This calls the standard `rpc.discover` method that should be available on any OpenRPC server
pub async fn discover(&self) -> ClientResult<serde_json::Value> {
let result: serde_json::Value = self
.client
.request("rpc.discover", rpc_params![])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(result)
}
/// Register a new runner to the supervisor with secret authentication
/// The runner name is also used as the queue name
pub async fn register_runner(
&self,
secret: &str,
name: &str,
) -> ClientResult<()> {
let params = serde_json::json!({
"secret": secret,
"name": name
});
let _: String = self
.client
.request("register_runner", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Create a new job without queuing it to a runner
pub async fn jobs_create(
&self,
secret: &str,
job: Job,
) -> ClientResult<String> {
let params = serde_json::json!({
"secret": secret,
"job": job
});
let job_id: String = self
.client
.request("jobs.create", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(job_id)
}
/// List all jobs
pub async fn jobs_list(&self) -> ClientResult<Vec<Job>> {
let jobs: Vec<Job> = self
.client
.request("jobs.list", rpc_params![])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(jobs)
}
/// Run a job on the appropriate runner and wait for the result (blocking)
/// This method queues the job and waits for completion before returning
pub async fn job_run(
&self,
secret: &str,
job: Job,
timeout: Option<u64>,
) -> ClientResult<JobRunResponse> {
let mut params = serde_json::json!({
"secret": secret,
"job": job
});
if let Some(t) = timeout {
params["timeout"] = serde_json::json!(t);
}
let result: JobRunResponse = self
.client
.request("job.run", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(result)
}
/// Start a job without waiting for the result (non-blocking)
/// This method queues the job and returns immediately with the job_id
pub async fn job_start(
&self,
secret: &str,
job: Job,
) -> ClientResult<JobStartResponse> {
let params = serde_json::json!({
"secret": secret,
"job": job
});
let result: JobStartResponse = self
.client
.request("job.start", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(result)
}
/// Get the current status of a job
pub async fn job_status(&self, job_id: &str) -> ClientResult<JobStatusResponse> {
let status: JobStatusResponse = self
.client
.request("job.status", rpc_params![job_id])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(status)
}
/// Get the result of a completed job (blocks until result is available)
pub async fn job_result(&self, job_id: &str) -> ClientResult<JobResult> {
let result: JobResult = self
.client
.request("job.result", rpc_params![job_id])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(result)
}
/// Remove a runner from the supervisor
pub async fn remove_runner(&self, secret: &str, actor_id: &str) -> ClientResult<()> {
let params = serde_json::json!({
"secret": secret,
"actor_id": actor_id
});
let _: () = self
.client
.request("remove_runner", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// List all runner IDs
pub async fn list_runners(&self) -> ClientResult<Vec<String>> {
let runners: Vec<String> = self
.client
.request("list_runners", rpc_params![])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(runners)
}
/// Start a specific runner
pub async fn start_runner(&self, secret: &str, actor_id: &str) -> ClientResult<()> {
let params = serde_json::json!({
"secret": secret,
"actor_id": actor_id
});
let _: () = self
.client
.request("start_runner", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Stop a specific runner
pub async fn stop_runner(&self, secret: &str, actor_id: &str, force: bool) -> ClientResult<()> {
let params = serde_json::json!({
"secret": secret,
"actor_id": actor_id,
"force": force
});
let _: () = self
.client
.request("stop_runner", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Add a runner to the supervisor
pub async fn add_runner(&self, secret: &str, config: RunnerConfig) -> ClientResult<()> {
let params = serde_json::json!({
"secret": secret,
"config": config
});
let _: () = self
.client
.request("add_runner", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Get status of a specific runner
pub async fn get_runner_status(&self, secret: &str, actor_id: &str) -> ClientResult<RunnerStatus> {
let params = serde_json::json!({
"secret": secret,
"actor_id": actor_id
});
let status: RunnerStatus = self
.client
.request("get_runner_status", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(status)
}
/// Get logs for a specific runner
pub async fn get_runner_logs(
&self,
actor_id: &str,
lines: Option<usize>,
follow: bool,
) -> ClientResult<Vec<LogInfo>> {
let logs: Vec<LogInfo> = self
.client
.request("get_runner_logs", rpc_params![actor_id, lines, follow])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(logs)
}
/// Queue a job to a specific runner
pub async fn queue_job_to_runner(&self, runner: &str, job: Job) -> ClientResult<()> {
let params = serde_json::json!({
"runner": runner,
"job": job
});
let _: () = self
.client
.request("queue_job_to_runner", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Queue a job and wait for completion
pub async fn queue_and_wait(&self, runner: &str, job: Job, timeout_secs: u64) -> ClientResult<Option<String>> {
let params = serde_json::json!({
"runner": runner,
"job": job,
"timeout_secs": timeout_secs
});
let result: Option<String> = self
.client
.request("queue_and_wait", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(result)
}
/// Run a job on a specific runner
pub async fn run_job(&self, secret: &str, job: Job) -> ClientResult<JobResult> {
let params = serde_json::json!({
"secret": secret,
"job": job
});
let result: JobResult = self
.client
.request("job.run", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(result)
}
/// Get job result by job ID
pub async fn get_job_result(&self, job_id: &str) -> ClientResult<Option<String>> {
let result: Option<String> = self
.client
.request("get_job_result", rpc_params![job_id])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(result)
}
/// Get status of all runners
pub async fn get_all_runner_status(&self) -> ClientResult<Vec<(String, RunnerStatus)>> {
let statuses: Vec<(String, RunnerStatus)> = self
.client
.request("get_all_runner_status", rpc_params![])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(statuses)
}
/// Start all runners
pub async fn start_all(&self) -> ClientResult<Vec<(String, bool)>> {
let results: Vec<(String, bool)> = self
.client
.request("start_all", rpc_params![])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(results)
}
/// Stop all runners
pub async fn stop_all(&self, force: bool) -> ClientResult<Vec<(String, bool)>> {
let results: Vec<(String, bool)> = self
.client
.request("stop_all", rpc_params![force])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(results)
}
/// Get status of all runners (alternative method)
pub async fn get_all_status(&self) -> ClientResult<Vec<(String, RunnerStatus)>> {
let statuses: Vec<(String, RunnerStatus)> = self
.client
.request("get_all_status", rpc_params![])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(statuses)
}
/// Add a secret to the supervisor
pub async fn add_secret(
&self,
admin_secret: &str,
secret_type: &str,
secret_value: &str,
) -> ClientResult<()> {
let params = serde_json::json!({
"admin_secret": admin_secret,
"secret_type": secret_type,
"secret_value": secret_value
});
let _: () = self
.client
.request("add_secret", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Remove a secret from the supervisor
pub async fn remove_secret(
&self,
admin_secret: &str,
secret_type: &str,
secret_value: &str,
) -> ClientResult<()> {
let params = serde_json::json!({
"admin_secret": admin_secret,
"secret_type": secret_type,
"secret_value": secret_value
});
let _: () = self
.client
.request("remove_secret", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// List secrets (returns supervisor info including secret counts)
pub async fn list_secrets(&self, admin_secret: &str) -> ClientResult<SupervisorInfo> {
let params = serde_json::json!({
"admin_secret": admin_secret
});
let info: SupervisorInfo = self
.client
.request("list_secrets", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(info)
}
/// Stop a running job
pub async fn job_stop(&self, secret: &str, job_id: &str) -> ClientResult<()> {
let params = serde_json::json!({
"secret": secret,
"job_id": job_id
});
self.client
.request("job.stop", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))
}
/// Delete a job from the system
pub async fn job_delete(&self, secret: &str, job_id: &str) -> ClientResult<()> {
let params = serde_json::json!({
"secret": secret,
"job_id": job_id
});
self.client
.request("job.delete", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))
}
/// Get supervisor information including secret counts
pub async fn get_supervisor_info(&self, admin_secret: &str) -> ClientResult<SupervisorInfo> {
let info: SupervisorInfo = self
.client
.request("get_supervisor_info", rpc_params![admin_secret])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(info)
}
}