refactor wip

This commit is contained in:
Timur Gordon
2025-08-05 12:19:38 +02:00
parent 8ed40ce99c
commit 7a652c9c3c
51 changed files with 6183 additions and 840 deletions

View File

@@ -1,157 +1,66 @@
# Rhai Client Binary
# Supervisor CLI
A command-line client for executing Rhai scripts on remote workers via Redis.
A command-line interface for the Hero Supervisor.
## Binary: `client`
## Binary: `hive-supervisor`
### Installation
Build the binary:
```bash
cargo build --bin client --release
cargo build --bin hive-supervisor --release
```
### Usage
```bash
# Basic usage - requires caller and circle keys
client --caller-key <CALLER_KEY> --circle-key <CIRCLE_KEY>
# Execute inline script
client -c <CALLER_KEY> -k <CIRCLE_KEY> --script "print('Hello World!')"
# Execute script from file
client -c <CALLER_KEY> -k <CIRCLE_KEY> --file script.rhai
# Use specific worker (defaults to circle key)
client -c <CALLER_KEY> -k <CIRCLE_KEY> -w <WORKER_KEY> --script "2 + 2"
# Custom Redis and timeout
client -c <CALLER_KEY> -k <CIRCLE_KEY> --redis-url redis://localhost:6379/1 --timeout 60
# Remove timestamps from logs
client -c <CALLER_KEY> -k <CIRCLE_KEY> --no-timestamp
# Increase verbosity
client -c <CALLER_KEY> -k <CIRCLE_KEY> -v --script "debug_info()"
```
### Command-Line Options
| Option | Short | Default | Description |
|--------|-------|---------|-------------|
| `--caller-key` | `-c` | **Required** | Caller public key (your identity) |
| `--circle-key` | `-k` | **Required** | Circle public key (execution context) |
| `--worker-key` | `-w` | `circle-key` | Worker public key (target worker) |
| `--redis-url` | `-r` | `redis://localhost:6379` | Redis connection URL |
| `--script` | `-s` | | Rhai script to execute |
| `--file` | `-f` | | Path to Rhai script file |
| `--timeout` | `-t` | `30` | Timeout for script execution (seconds) |
| `--no-timestamp` | | `false` | Remove timestamps from log output |
| `--verbose` | `-v` | | Increase verbosity (stackable) |
### Execution Modes
#### Inline Script Execution
```bash
# Execute a simple calculation
client -c caller_123 -k circle_456 -s "let result = 2 + 2; print(result);"
# Execute with specific worker
client -c caller_123 -k circle_456 -w worker_789 -s "get_user_data()"
```
#### Script File Execution
```bash
# Execute script from file
client -c caller_123 -k circle_456 -f examples/data_processing.rhai
# Execute with custom timeout
client -c caller_123 -k circle_456 -f long_running_script.rhai -t 120
```
#### Interactive Mode
```bash
# Enter interactive REPL mode (when no script or file provided)
client -c caller_123 -k circle_456
# Interactive mode with verbose logging
client -c caller_123 -k circle_456 -v --no-timestamp
```
### Interactive Mode
When no script (`-s`) or file (`-f`) is provided, the client enters interactive mode:
# Basic usage
hive-supervisor --config <CONFIG_PATH>
```
🔗 Starting Rhai Client
📋 Configuration:
Caller Key: caller_123
Circle Key: circle_456
Worker Key: circle_456
Redis URL: redis://localhost:6379
Timeout: 30s
✅ Connected to Redis at redis://localhost:6379
🎮 Entering interactive mode
Type Rhai scripts and press Enter to execute. Type 'exit' or 'quit' to close.
rhai> let x = 42; print(x);
Status: completed
Output: 42
rhai> exit
👋 Goodbye!
Where config is toml file with the following structure:
```toml
[global]
redis_url = "redis://localhost:6379"
[osis_worker]
binary_path = "/path/to/osis_worker"
env_vars = { "VAR1" = "value1", "VAR2" = "value2" }
[sal_worker]
binary_path = "/path/to/sal_worker"
env_vars = { "VAR1" = "value1", "VAR2" = "value2" }
[v_worker]
binary_path = "/path/to/v_worker"
env_vars = { "VAR1" = "value1", "VAR2" = "value2" }
[python_worker]
binary_path = "/path/to/python_worker"
env_vars = { "VAR1" = "value1", "VAR2" = "value2" }
```
### Configuration Examples
#### Development Usage
```bash
# Simple development client
client -c dev_user -k dev_circle
Lets have verbosity settings etc.
CLI Offers a few commands:
# Development with clean logs
client -c dev_user -k dev_circle --no-timestamp -v
```
workers:
start
stop
restart
status
logs
list
#### Production Usage
```bash
# Production client with specific worker
client \
--caller-key prod_user_123 \
--circle-key prod_circle_456 \
--worker-key prod_worker_789 \
--redis-url redis://redis-cluster:6379/0 \
--timeout 300 \
--file production_script.rhai
```
#### Batch Processing
```bash
# Process multiple scripts
for script in scripts/*.rhai; do
client -c batch_user -k batch_circle -f "$script" --no-timestamp
done
```
### Key Concepts
- **Caller Key**: Your identity - used for authentication and tracking
- **Circle Key**: Execution context - defines the environment/permissions
- **Worker Key**: Target worker - which worker should execute the script (defaults to circle key)
### Error Handling
The client provides clear error messages for:
- Missing required keys
- Redis connection failures
- Script execution timeouts
- Worker unavailability
- Script syntax errors
### Dependencies
- `rhai_supervisor`: Core client library for Redis-based script execution
- `redis`: Redis client for task queue communication
- `clap`: Command-line argument parsing
- `env_logger`: Logging infrastructure
- `tokio`: Async runtime
jobs:
create
start
stop
restart
status
logs
list
repl: you can enter interactive mode to run scripts, however predefine caller_id, context_id and worker type so supervisor dispathces jobs accordingly

View File

@@ -0,0 +1,365 @@
use anyhow::Result;
use clap::Parser;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use hero_supervisor::{Supervisor, SupervisorBuilder};
use zinit_client::ZinitClient;
use log::{error, info};
use ratatui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::Line,
widgets::{
Block, Borders, List, ListItem, Paragraph, Tabs, Wrap,
},
Frame, Terminal,
};
use std::{
io,
path::PathBuf,
sync::Arc,
time::{Duration, Instant},
};
use tokio::time::sleep;
use toml;
use serde::Deserialize;
#[derive(Parser)]
#[command(name = "hive-supervisor-tui")]
#[command(about = "Hero Supervisor Terminal User Interface")]
struct Args {
#[arg(short, long, help = "Configuration file path")]
config: PathBuf,
#[arg(short, long, help = "Enable verbose logging")]
verbose: bool,
}
#[derive(Debug, Deserialize)]
struct Config {
global: GlobalConfig,
#[serde(flatten)]
workers: std::collections::HashMap<String, WorkerConfigToml>,
}
#[derive(Debug, Deserialize)]
struct GlobalConfig {
redis_url: String,
}
#[derive(Debug, Deserialize)]
struct WorkerConfigToml {
binary_path: String,
env_vars: Option<std::collections::HashMap<String, String>>,
}
#[derive(Debug, Clone, PartialEq)]
enum TabId {
Dashboard,
Workers,
Jobs,
Logs,
}
impl TabId {
fn all() -> Vec<TabId> {
vec![TabId::Dashboard, TabId::Workers, TabId::Jobs, TabId::Logs]
}
fn title(&self) -> &str {
match self {
TabId::Dashboard => "Dashboard",
TabId::Workers => "Workers",
TabId::Jobs => "Jobs",
TabId::Logs => "Logs",
}
}
}
struct App {
supervisor: Arc<Supervisor>,
current_tab: TabId,
should_quit: bool,
logs: Vec<String>,
last_update: Instant,
}
impl App {
fn new(supervisor: Arc<Supervisor>) -> Self {
Self {
supervisor,
current_tab: TabId::Dashboard,
should_quit: false,
logs: vec!["TUI started successfully".to_string()],
last_update: Instant::now(),
}
}
fn next_tab(&mut self) {
let tabs = TabId::all();
let current_index = tabs.iter().position(|t| *t == self.current_tab).unwrap_or(0);
let next_index = (current_index + 1) % tabs.len();
self.current_tab = tabs[next_index].clone();
}
fn prev_tab(&mut self) {
let tabs = TabId::all();
let current_index = tabs.iter().position(|t| *t == self.current_tab).unwrap_or(0);
let prev_index = if current_index == 0 { tabs.len() - 1 } else { current_index - 1 };
self.current_tab = tabs[prev_index].clone();
}
fn add_log(&mut self, message: String) {
self.logs.push(format!("[{}] {}",
chrono::Utc::now().format("%H:%M:%S"),
message
));
if self.logs.len() > 100 {
self.logs.remove(0);
}
}
fn handle_key(&mut self, key: KeyCode) -> bool {
match key {
KeyCode::Char('q') => {
self.should_quit = true;
true
}
KeyCode::Tab => {
self.next_tab();
false
}
KeyCode::BackTab => {
self.prev_tab();
false
}
_ => false
}
}
}
fn render_ui(f: &mut Frame, app: &mut App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(f.area());
// Render tabs
let tabs_list = TabId::all();
let tab_titles: Vec<Line> = tabs_list
.iter()
.map(|t| Line::from(t.title()))
.collect();
let selected_tab = TabId::all().iter().position(|t| *t == app.current_tab).unwrap_or(0);
let tabs = Tabs::new(tab_titles)
.block(Block::default().borders(Borders::ALL).title("Hero Supervisor TUI"))
.select(selected_tab)
.style(Style::default().fg(Color::Cyan))
.highlight_style(Style::default().add_modifier(Modifier::BOLD).bg(Color::Black));
f.render_widget(tabs, chunks[0]);
// Render content based on selected tab
match app.current_tab {
TabId::Dashboard => render_dashboard(f, chunks[1], app),
TabId::Workers => render_workers(f, chunks[1], app),
TabId::Jobs => render_jobs(f, chunks[1], app),
TabId::Logs => render_logs(f, chunks[1], app),
}
}
fn render_dashboard(f: &mut Frame, area: Rect, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(7), Constraint::Min(0)].as_ref())
.split(area);
// Status overview - supervisor is already running if we get here
let status_text = "Status: ✓ Running\nWorkers: Started successfully\nJobs: Ready for processing\n\nPress 'q' to quit, Tab to navigate";
let status_paragraph = Paragraph::new(status_text)
.block(Block::default().borders(Borders::ALL).title("System Status"))
.wrap(Wrap { trim: true });
f.render_widget(status_paragraph, chunks[0]);
// Recent logs
let log_items: Vec<ListItem> = app.logs
.iter()
.rev()
.take(10)
.map(|log| ListItem::new(log.as_str()))
.collect();
let logs_list = List::new(log_items)
.block(Block::default().borders(Borders::ALL).title("Recent Activity"));
f.render_widget(logs_list, chunks[1]);
}
fn render_workers(f: &mut Frame, area: Rect, _app: &App) {
let paragraph = Paragraph::new("Workers tab - Status checking not implemented yet to avoid system issues")
.block(Block::default().borders(Borders::ALL).title("Workers"))
.wrap(Wrap { trim: true });
f.render_widget(paragraph, area);
}
fn render_jobs(f: &mut Frame, area: Rect, _app: &App) {
let paragraph = Paragraph::new("Jobs tab - Job monitoring not implemented yet to avoid system issues")
.block(Block::default().borders(Borders::ALL).title("Jobs"))
.wrap(Wrap { trim: true });
f.render_widget(paragraph, area);
}
fn render_logs(f: &mut Frame, area: Rect, app: &App) {
let items: Vec<ListItem> = app.logs
.iter()
.map(|log| ListItem::new(log.as_str()))
.collect();
let logs_list = List::new(items)
.block(Block::default().borders(Borders::ALL).title("System Logs"));
f.render_widget(logs_list, area);
}
async fn run_app(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
app: &mut App,
) -> Result<()> {
loop {
terminal.draw(|f| render_ui(f, app))?;
// Simple, safe event handling
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
if app.handle_key(key.code) {
break;
}
}
}
}
if app.should_quit {
break;
}
// Small delay to prevent excessive CPU usage
sleep(Duration::from_millis(50)).await;
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
// Initialize logging
if args.verbose {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init();
} else {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
}
info!("Hero Supervisor TUI - Fail-fast initialization");
// Step 1: Load and parse configuration
info!("Step 1/4: Loading configuration from {:?}", args.config);
let config_content = std::fs::read_to_string(&args.config)
.map_err(|e| anyhow::anyhow!("Failed to read config file: {}", e))?;
let config: Config = toml::from_str(&config_content)
.map_err(|e| anyhow::anyhow!("Failed to parse config file: {}", e))?;
info!("✓ Configuration loaded successfully");
// Step 2: Check if Zinit is running
info!("Step 2/4: Checking if Zinit is running...");
let zinit_client = ZinitClient::new("/tmp/zinit.sock");
match zinit_client.status("_test_connectivity").await {
Ok(_) => {
info!("✓ Zinit is running and accessible");
}
Err(e) => {
let error_msg = e.to_string();
if error_msg.contains("Connection refused") || error_msg.contains("No such file") {
eprintln!("Error: Zinit process manager is not running.");
eprintln!("Please start Zinit before running the supervisor TUI.");
eprintln!("Expected Zinit socket at: /tmp/zinit.sock");
std::process::exit(1);
} else {
info!("✓ Zinit is running (service not found is expected)");
}
}
}
// Step 3: Build supervisor
info!("Step 3/4: Building supervisor...");
let mut builder = SupervisorBuilder::new()
.redis_url(&config.global.redis_url);
for (worker_name, worker_config) in &config.workers {
match worker_name.as_str() {
"osis_worker" => builder = builder.osis_worker(&worker_config.binary_path),
"sal_worker" => builder = builder.sal_worker(&worker_config.binary_path),
"v_worker" => builder = builder.v_worker(&worker_config.binary_path),
"python_worker" => builder = builder.python_worker(&worker_config.binary_path),
_ => log::warn!("Unknown worker type: {}", worker_name),
}
if let Some(env_vars) = &worker_config.env_vars {
for (key, value) in env_vars {
builder = builder.worker_env_var(key, value);
}
}
}
let supervisor = Arc::new(builder.build()
.map_err(|e| anyhow::anyhow!("Failed to build supervisor: {}", e))?);
info!("✓ Supervisor built successfully");
// Step 4: Start supervisor and workers
info!("Step 4/4: Starting supervisor and workers...");
supervisor.start_workers().await
.map_err(|e| anyhow::anyhow!("Failed to start workers: {}", e))?;
info!("✓ All workers started successfully");
// All initialization successful - now start TUI
info!("Initialization complete - starting TUI...");
let mut app = App::new(Arc::clone(&supervisor));
// 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)?;
// Run the app
let result = run_app(&mut terminal, &mut app).await;
// Cleanup
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
// Cleanup supervisor
if let Err(e) = supervisor.cleanup_and_shutdown().await {
error!("Error during cleanup: {}", e);
}
info!("Hero Supervisor TUI shutdown complete");
result
}