implement unix and ws using jsonrpsee
This commit is contained in:
472
interfaces/openrpc/client/cmd/main.rs
Normal file
472
interfaces/openrpc/client/cmd/main.rs
Normal file
@@ -0,0 +1,472 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use colored::*;
|
||||
use dialoguer::{Input, Select, Confirm, MultiSelect};
|
||||
use hero_job::ScriptType;
|
||||
use hero_openrpc_client::{
|
||||
AuthHelper, ClientTransport, HeroOpenRpcClient, JobParams,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use tracing::{error, info, Level};
|
||||
use tracing_subscriber;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "hero-openrpc-client")]
|
||||
#[command(about = "Hero OpenRPC Client - Interactive JSON-RPC client")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
|
||||
/// Private key for authentication (hex format)
|
||||
#[arg(long)]
|
||||
private_key: Option<String>,
|
||||
|
||||
/// Generate a new private key and exit
|
||||
#[arg(long)]
|
||||
generate_key: bool,
|
||||
|
||||
/// Log level
|
||||
#[arg(long, default_value = "info")]
|
||||
log_level: String,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Connect to WebSocket server
|
||||
Websocket {
|
||||
/// Server URL
|
||||
#[arg(long, default_value = "ws://127.0.0.1:9944")]
|
||||
url: String,
|
||||
},
|
||||
/// Connect to Unix socket server
|
||||
Unix {
|
||||
/// Unix socket path
|
||||
#[arg(long, default_value = "/tmp/hero-openrpc.sock")]
|
||||
socket_path: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
/// Available RPC methods with descriptions
|
||||
#[derive(Debug, Clone)]
|
||||
struct RpcMethod {
|
||||
name: &'static str,
|
||||
description: &'static str,
|
||||
requires_auth: bool,
|
||||
}
|
||||
|
||||
const RPC_METHODS: &[RpcMethod] = &[
|
||||
RpcMethod {
|
||||
name: "fetch_nonce",
|
||||
description: "Fetch a nonce for authentication",
|
||||
requires_auth: false,
|
||||
},
|
||||
RpcMethod {
|
||||
name: "authenticate",
|
||||
description: "Authenticate with public key and signature",
|
||||
requires_auth: false,
|
||||
},
|
||||
RpcMethod {
|
||||
name: "whoami",
|
||||
description: "Get authentication status and user information",
|
||||
requires_auth: true,
|
||||
},
|
||||
RpcMethod {
|
||||
name: "play",
|
||||
description: "Execute a Rhai script immediately",
|
||||
requires_auth: true,
|
||||
},
|
||||
RpcMethod {
|
||||
name: "create_job",
|
||||
description: "Create a new job without starting it",
|
||||
requires_auth: true,
|
||||
},
|
||||
RpcMethod {
|
||||
name: "start_job",
|
||||
description: "Start a previously created job",
|
||||
requires_auth: true,
|
||||
},
|
||||
RpcMethod {
|
||||
name: "run_job",
|
||||
description: "Create and run a job, returning result when complete",
|
||||
requires_auth: true,
|
||||
},
|
||||
RpcMethod {
|
||||
name: "get_job_status",
|
||||
description: "Get the current status of a job",
|
||||
requires_auth: true,
|
||||
},
|
||||
RpcMethod {
|
||||
name: "get_job_output",
|
||||
description: "Get the output of a completed job",
|
||||
requires_auth: true,
|
||||
},
|
||||
RpcMethod {
|
||||
name: "get_job_logs",
|
||||
description: "Get the logs of a job",
|
||||
requires_auth: true,
|
||||
},
|
||||
RpcMethod {
|
||||
name: "list_jobs",
|
||||
description: "List all jobs in the system",
|
||||
requires_auth: true,
|
||||
},
|
||||
RpcMethod {
|
||||
name: "stop_job",
|
||||
description: "Stop a running job",
|
||||
requires_auth: true,
|
||||
},
|
||||
RpcMethod {
|
||||
name: "delete_job",
|
||||
description: "Delete a job from the system",
|
||||
requires_auth: true,
|
||||
},
|
||||
RpcMethod {
|
||||
name: "clear_all_jobs",
|
||||
description: "Clear all jobs from the system",
|
||||
requires_auth: true,
|
||||
},
|
||||
];
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Initialize tracing
|
||||
let log_level = match cli.log_level.to_lowercase().as_str() {
|
||||
"trace" => Level::TRACE,
|
||||
"debug" => Level::DEBUG,
|
||||
"info" => Level::INFO,
|
||||
"warn" => Level::WARN,
|
||||
"error" => Level::ERROR,
|
||||
_ => Level::INFO,
|
||||
};
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(log_level)
|
||||
.init();
|
||||
|
||||
// Handle key generation
|
||||
if cli.generate_key {
|
||||
let auth_helper = AuthHelper::generate()?;
|
||||
println!("{}", "Generated new private key:".green().bold());
|
||||
println!("Private Key: {}", auth_helper.private_key_hex().yellow());
|
||||
println!("Public Key: {}", auth_helper.public_key_hex().cyan());
|
||||
println!();
|
||||
println!("{}", "Save the private key securely and use it with --private-key".bright_yellow());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let transport = match cli.command {
|
||||
Commands::Websocket { url } => {
|
||||
println!("{} {}", "Connecting to WebSocket server:".green(), url.cyan());
|
||||
ClientTransport::WebSocket(url)
|
||||
}
|
||||
Commands::Unix { socket_path } => {
|
||||
println!("{} {:?}", "Connecting to Unix socket server:".green(), socket_path);
|
||||
ClientTransport::Unix(socket_path)
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to the server
|
||||
let client = HeroOpenRpcClient::connect(transport).await?;
|
||||
println!("{}", "Connected successfully!".green().bold());
|
||||
|
||||
// Handle authentication if private key is provided
|
||||
let mut authenticated = false;
|
||||
if let Some(private_key) = cli.private_key {
|
||||
println!("{}", "Authenticating...".yellow());
|
||||
match client.authenticate_with_key(&private_key).await {
|
||||
Ok(true) => {
|
||||
println!("{}", "Authentication successful!".green().bold());
|
||||
authenticated = true;
|
||||
}
|
||||
Ok(false) => {
|
||||
println!("{}", "Authentication failed!".red().bold());
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Authentication error: {}", e);
|
||||
println!("{} {}", "Authentication error:".red().bold(), e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("{}", "No private key provided. Some methods will require authentication.".yellow());
|
||||
println!("{}", "Use --generate-key to create a new key or --private-key to use an existing one.".bright_yellow());
|
||||
}
|
||||
|
||||
println!();
|
||||
|
||||
// Interactive loop
|
||||
loop {
|
||||
// Filter methods based on authentication status
|
||||
let available_methods: Vec<&RpcMethod> = RPC_METHODS
|
||||
.iter()
|
||||
.filter(|method| !method.requires_auth || authenticated)
|
||||
.collect();
|
||||
|
||||
if available_methods.is_empty() {
|
||||
println!("{}", "No methods available. Please authenticate first.".red());
|
||||
break;
|
||||
}
|
||||
|
||||
// Display method selection
|
||||
let method_names: Vec<String> = available_methods
|
||||
.iter()
|
||||
.map(|method| {
|
||||
if method.requires_auth && !authenticated {
|
||||
format!("{} {} (requires auth)", method.name.red(), method.description)
|
||||
} else {
|
||||
format!("{} {}", method.name.green(), method.description)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let selection = Select::new()
|
||||
.with_prompt("Select an RPC method to call")
|
||||
.items(&method_names)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
let Some(selection) = selection else {
|
||||
println!("{}", "Goodbye!".cyan());
|
||||
break;
|
||||
};
|
||||
|
||||
let selected_method = available_methods[selection];
|
||||
println!();
|
||||
println!("{} {}", "Selected method:".bold(), selected_method.name.green());
|
||||
|
||||
// Handle method-specific parameter collection and execution
|
||||
match execute_method(&client, selected_method.name).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("Method execution failed: {}", e);
|
||||
println!("{} {}", "Error:".red().bold(), e);
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
|
||||
// Ask if user wants to continue
|
||||
if !Confirm::new()
|
||||
.with_prompt("Do you want to call another method?")
|
||||
.default(true)
|
||||
.interact()?
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
println!("{}", "Goodbye!".cyan().bold());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute_method(client: &HeroOpenRpcClient, method_name: &str) -> Result<()> {
|
||||
match method_name {
|
||||
"fetch_nonce" => {
|
||||
let pubkey: String = Input::new()
|
||||
.with_prompt("Public key (hex)")
|
||||
.interact_text()?;
|
||||
|
||||
let result = client.fetch_nonce(pubkey).await?;
|
||||
println!("{} {}", "Nonce:".green().bold(), result.yellow());
|
||||
}
|
||||
|
||||
"authenticate" => {
|
||||
let pubkey: String = Input::new()
|
||||
.with_prompt("Public key (hex)")
|
||||
.interact_text()?;
|
||||
|
||||
let signature: String = Input::new()
|
||||
.with_prompt("Signature (hex)")
|
||||
.interact_text()?;
|
||||
|
||||
let result = client.authenticate(pubkey, signature, nonce).await?;
|
||||
println!("{} {}", "Authentication result:".green().bold(),
|
||||
if result { "Success".green() } else { "Failed".red() });
|
||||
}
|
||||
|
||||
"whoami" => {
|
||||
let result = client.whoami().await?;
|
||||
println!("{} {}", "User info:".green().bold(),
|
||||
serde_json::to_string_pretty(&result)?.cyan());
|
||||
}
|
||||
|
||||
"play" => {
|
||||
let script: String = Input::new()
|
||||
.with_prompt("Rhai script to execute")
|
||||
.interact_text()?;
|
||||
|
||||
let result = client.play(script).await?;
|
||||
println!("{} {}", "Script output:".green().bold(), result.output.cyan());
|
||||
}
|
||||
|
||||
"create_job" => {
|
||||
let script: String = Input::new()
|
||||
.with_prompt("Script content")
|
||||
.interact_text()?;
|
||||
|
||||
let script_types = ["HeroScript", "RhaiSAL", "RhaiDSL"];
|
||||
let script_type_selection = Select::new()
|
||||
.with_prompt("Script type")
|
||||
.items(&script_types)
|
||||
.default(0)
|
||||
.interact()?;
|
||||
|
||||
let script_type = match script_type_selection {
|
||||
0 => ScriptType::HeroScript,
|
||||
1 => ScriptType::RhaiSAL,
|
||||
2 => ScriptType::RhaiDSL,
|
||||
_ => ScriptType::HeroScript,
|
||||
};
|
||||
|
||||
let add_prerequisites = Confirm::new()
|
||||
.with_prompt("Add prerequisites?")
|
||||
.default(false)
|
||||
.interact()?;
|
||||
|
||||
let prerequisites = if add_prerequisites {
|
||||
let prereq_input: String = Input::new()
|
||||
.with_prompt("Prerequisites (comma-separated job IDs)")
|
||||
.interact_text()?;
|
||||
Some(prereq_input.split(',').map(|s| s.trim().to_string()).collect())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let job_params = JobParams {
|
||||
script,
|
||||
script_type,
|
||||
prerequisites,
|
||||
};
|
||||
|
||||
let result = client.create_job(job_params).await?;
|
||||
println!("{} {}", "Created job ID:".green().bold(), result.yellow());
|
||||
}
|
||||
|
||||
"start_job" => {
|
||||
let job_id: String = Input::new()
|
||||
.with_prompt("Job ID to start")
|
||||
.interact_text()?;
|
||||
|
||||
let result = client.start_job(job_id).await?;
|
||||
println!("{} {}", "Start result:".green().bold(),
|
||||
if result.success { "Success".green() } else { "Failed".red() });
|
||||
}
|
||||
|
||||
"run_job" => {
|
||||
let script: String = Input::new()
|
||||
.with_prompt("Script content")
|
||||
.interact_text()?;
|
||||
|
||||
let script_types = ["HeroScript", "RhaiSAL", "RhaiDSL"];
|
||||
let script_type_selection = Select::new()
|
||||
.with_prompt("Script type")
|
||||
.items(&script_types)
|
||||
.default(0)
|
||||
.interact()?;
|
||||
|
||||
let script_type = match script_type_selection {
|
||||
0 => ScriptType::HeroScript,
|
||||
1 => ScriptType::RhaiSAL,
|
||||
2 => ScriptType::RhaiDSL,
|
||||
_ => ScriptType::HeroScript,
|
||||
};
|
||||
|
||||
let add_prerequisites = Confirm::new()
|
||||
.with_prompt("Add prerequisites?")
|
||||
.default(false)
|
||||
.interact()?;
|
||||
|
||||
let prerequisites = if add_prerequisites {
|
||||
let prereq_input: String = Input::new()
|
||||
.with_prompt("Prerequisites (comma-separated job IDs)")
|
||||
.interact_text()?;
|
||||
Some(prereq_input.split(',').map(|s| s.trim().to_string()).collect())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let result = client.run_job(script, script_type, prerequisites).await?;
|
||||
println!("{} {}", "Job result:".green().bold(), result.cyan());
|
||||
}
|
||||
|
||||
"get_job_status" => {
|
||||
let job_id: String = Input::new()
|
||||
.with_prompt("Job ID")
|
||||
.interact_text()?;
|
||||
|
||||
let result = client.get_job_status(job_id).await?;
|
||||
println!("{} {:?}", "Job status:".green().bold(), result);
|
||||
}
|
||||
|
||||
"get_job_output" => {
|
||||
let job_id: String = Input::new()
|
||||
.with_prompt("Job ID")
|
||||
.interact_text()?;
|
||||
|
||||
let result = client.get_job_output(job_id).await?;
|
||||
println!("{} {}", "Job output:".green().bold(), result.cyan());
|
||||
}
|
||||
|
||||
"get_job_logs" => {
|
||||
let job_id: String = Input::new()
|
||||
.with_prompt("Job ID")
|
||||
.interact_text()?;
|
||||
|
||||
let result = client.get_job_logs(job_id).await?;
|
||||
println!("{} {}", "Job logs:".green().bold(), result.logs.cyan());
|
||||
}
|
||||
|
||||
"list_jobs" => {
|
||||
let result = client.list_jobs().await?;
|
||||
println!("{}", "Jobs:".green().bold());
|
||||
for job in result {
|
||||
println!(" {} - {} ({:?})",
|
||||
job.id().yellow(),
|
||||
job.script_type(),
|
||||
job.status()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
"stop_job" => {
|
||||
let job_id: String = Input::new()
|
||||
.with_prompt("Job ID to stop")
|
||||
.interact_text()?;
|
||||
|
||||
client.stop_job(job_id.clone()).await?;
|
||||
println!("{} {}", "Stopped job:".green().bold(), job_id.yellow());
|
||||
}
|
||||
|
||||
"delete_job" => {
|
||||
let job_id: String = Input::new()
|
||||
.with_prompt("Job ID to delete")
|
||||
.interact_text()?;
|
||||
|
||||
client.delete_job(job_id.clone()).await?;
|
||||
println!("{} {}", "Deleted job:".green().bold(), job_id.yellow());
|
||||
}
|
||||
|
||||
"clear_all_jobs" => {
|
||||
let confirm = Confirm::new()
|
||||
.with_prompt("Are you sure you want to clear ALL jobs?")
|
||||
.default(false)
|
||||
.interact()?;
|
||||
|
||||
if confirm {
|
||||
client.clear_all_jobs().await?;
|
||||
println!("{}", "Cleared all jobs".green().bold());
|
||||
} else {
|
||||
println!("{}", "Operation cancelled".yellow());
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
println!("{} {}", "Unknown method:".red().bold(), method_name);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
Reference in New Issue
Block a user