add hero runner and clean improve runner lib
This commit is contained in:
26
bin/runners/hero/Cargo.toml
Normal file
26
bin/runners/hero/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "runner-hero"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description = "Hero Runner - Command execution runner for Hero jobs"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[[bin]]
|
||||
name = "herorunner"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# Runner library
|
||||
hero-runner = { path = "../../../lib/runner" }
|
||||
hero-job = { path = "../../../lib/models/job" }
|
||||
|
||||
# Core dependencies
|
||||
anyhow.workspace = true
|
||||
tokio.workspace = true
|
||||
log.workspace = true
|
||||
env_logger.workspace = true
|
||||
clap.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
# Process execution
|
||||
168
bin/runners/hero/README.md
Normal file
168
bin/runners/hero/README.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Hero Runner
|
||||
|
||||
A specialized runner for the Hero ecosystem that executes heroscripts using the `hero` CLI tool.
|
||||
|
||||
## Overview
|
||||
|
||||
The Hero runner executes heroscripts by calling `hero run -h <payload>` for each job. This makes it ideal for:
|
||||
|
||||
- Running heroscripts from job payloads
|
||||
- Executing Hero automation tasks
|
||||
- Integrating with the Hero CLI ecosystem
|
||||
- Running scripted workflows
|
||||
|
||||
## Features
|
||||
|
||||
- **Heroscript Execution**: Executes `hero run -h <payload>` for each job
|
||||
- **Environment Variables**: Passes job environment variables to the hero command
|
||||
- **Timeout Support**: Respects job timeout settings
|
||||
- **Signature Verification**: Verifies job signatures before execution
|
||||
- **Simple Integration**: No complex payload parsing - just pass the heroscript content
|
||||
|
||||
## Usage
|
||||
|
||||
### Starting the Runner
|
||||
|
||||
```bash
|
||||
# Basic usage
|
||||
herorunner my-hero-runner
|
||||
|
||||
# With custom Redis URL
|
||||
herorunner my-hero-runner --redis-url redis://localhost:6379
|
||||
```
|
||||
|
||||
### Command-line Options
|
||||
|
||||
- `runner_id`: Runner identifier (required, positional)
|
||||
- `-r, --redis-url`: Redis URL (default: `redis://localhost:6379`)
|
||||
|
||||
## Job Payload Format
|
||||
|
||||
The job payload should contain the heroscript content that will be passed to `hero run -h`.
|
||||
|
||||
### Example Payload
|
||||
|
||||
```
|
||||
print("Hello from heroscript!")
|
||||
```
|
||||
|
||||
The runner will execute: `hero run -h 'print("Hello from heroscript!")'`
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Simple Heroscript
|
||||
|
||||
Job payload:
|
||||
```
|
||||
print("Processing job...")
|
||||
```
|
||||
|
||||
Executed as: `hero run -h 'print("Processing job...")'`
|
||||
|
||||
### Example 2: Multi-line Heroscript
|
||||
|
||||
Job payload:
|
||||
```
|
||||
print("Starting task...")
|
||||
// Your heroscript logic here
|
||||
print("Task completed!")
|
||||
```
|
||||
|
||||
### Example 3: With Environment Variables
|
||||
|
||||
Job with env_vars:
|
||||
```json
|
||||
{
|
||||
"payload": "print(env.MY_VAR)",
|
||||
"env_vars": {
|
||||
"MY_VAR": "Hello from Hero Runner"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The Hero runner implements the `Runner` trait from `hero-runner` library:
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ HeroExecutor │
|
||||
│ │
|
||||
│ - execute_command()│
|
||||
│ - process_job() │
|
||||
└─────────────────────┘
|
||||
│
|
||||
│ implements
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Runner Trait │
|
||||
│ │
|
||||
│ - spawn() │
|
||||
│ - process_job() │
|
||||
│ - runner_type() │
|
||||
└─────────────────────┘
|
||||
│
|
||||
│ executes
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ hero run -h │
|
||||
│ <heroscript> │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Heroscript Execution**: The runner executes heroscripts via the `hero` CLI. Ensure job payloads are from trusted sources.
|
||||
2. **Signature Verification**: Always verify job signatures before execution.
|
||||
3. **Environment Variables**: Be cautious with sensitive data in environment variables.
|
||||
4. **Hero CLI Access**: Ensure the `hero` command is available in the system PATH.
|
||||
|
||||
## Error Handling
|
||||
|
||||
The runner handles various error scenarios:
|
||||
|
||||
- **Hero CLI Not Found**: Returns error if the `hero` command is not available
|
||||
- **Timeout**: Kills the process if it exceeds the job timeout
|
||||
- **Non-zero Exit**: Returns error if `hero run -h` exits with non-zero status
|
||||
- **Heroscript Errors**: Returns error output from the hero CLI
|
||||
|
||||
## Logging
|
||||
|
||||
The runner logs to stdout/stderr with the following log levels:
|
||||
|
||||
- `INFO`: Job start/completion, runner lifecycle
|
||||
- `DEBUG`: Command details, parsing information
|
||||
- `ERROR`: Execution failures, timeout errors
|
||||
|
||||
## Integration with Supervisor
|
||||
|
||||
The Vlang runner integrates with the Hero Supervisor:
|
||||
|
||||
1. Register the runner with the supervisor
|
||||
2. Supervisor queues jobs to the runner's Redis queue
|
||||
3. Runner polls the queue and executes commands
|
||||
4. Results are stored back in Redis
|
||||
|
||||
## Development
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
cargo build -p runner-hero
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
cargo test -p runner-hero
|
||||
```
|
||||
|
||||
### Running Locally
|
||||
|
||||
```bash
|
||||
cargo run -p runner-hero -- test-runner
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT OR Apache-2.0
|
||||
121
bin/runners/hero/src/executor.rs
Normal file
121
bin/runners/hero/src/executor.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
//! Hero Command Executor
|
||||
//!
|
||||
//! This module implements command execution for Hero jobs.
|
||||
//! It executes commands from job payloads and returns the output.
|
||||
|
||||
use hero_runner::{Runner, Job};
|
||||
use log::{debug, error, info};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Hero command executor
|
||||
pub struct HeroExecutor {
|
||||
runner_id: String,
|
||||
redis_url: String,
|
||||
}
|
||||
|
||||
impl HeroExecutor {
|
||||
/// Create a new Hero executor
|
||||
pub fn new(runner_id: String, redis_url: String) -> Self {
|
||||
Self {
|
||||
runner_id,
|
||||
redis_url,
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a command from the job payload
|
||||
fn execute_command(&self, job: &Job) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
info!("Runner '{}': Executing hero run -h for job {}", self.runner_id, job.id);
|
||||
|
||||
// Always execute: hero run -h <payload>
|
||||
let mut cmd = Command::new("hero");
|
||||
cmd.args(&["run", "-h", &job.payload]);
|
||||
|
||||
debug!("Runner '{}': Executing: hero run -h {}", self.runner_id, job.payload);
|
||||
|
||||
// Set environment variables from job
|
||||
for (key, value) in &job.env_vars {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
|
||||
// Configure stdio
|
||||
cmd.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
// Execute command with timeout
|
||||
let timeout = Duration::from_secs(job.timeout);
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
info!("Runner '{}': Starting command execution for job {}", self.runner_id, job.id);
|
||||
|
||||
let mut child = cmd.spawn()
|
||||
.map_err(|e| format!("Failed to spawn 'hero run -h': {}", e))?;
|
||||
|
||||
// Wait for command with timeout
|
||||
let output = loop {
|
||||
if start.elapsed() > timeout {
|
||||
// Kill the process if it times out
|
||||
let _ = child.kill();
|
||||
return Err(format!("Command execution timed out after {} seconds", job.timeout).into());
|
||||
}
|
||||
|
||||
match child.try_wait() {
|
||||
Ok(Some(_status)) => {
|
||||
// Process has exited
|
||||
let output = child.wait_with_output()
|
||||
.map_err(|e| format!("Failed to get command output: {}", e))?;
|
||||
|
||||
break output;
|
||||
}
|
||||
Ok(None) => {
|
||||
// Process still running, sleep briefly
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(format!("Error waiting for command: {}", e).into());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check exit status
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
error!("Runner '{}': Command failed for job {}: {}", self.runner_id, job.id, stderr);
|
||||
return Err(format!("Command failed with exit code {:?}: {}", output.status.code(), stderr).into());
|
||||
}
|
||||
|
||||
// Return stdout
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
info!("Runner '{}': Command completed successfully for job {}", self.runner_id, job.id);
|
||||
|
||||
Ok(stdout)
|
||||
}
|
||||
}
|
||||
|
||||
impl Runner for HeroExecutor {
|
||||
fn process_job(&self, job: Job) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
info!("Runner '{}': Processing job {}", self.runner_id, job.id);
|
||||
|
||||
// Execute the command
|
||||
let result = self.execute_command(&job);
|
||||
|
||||
match result {
|
||||
Ok(output) => {
|
||||
info!("Runner '{}': Job {} completed successfully", self.runner_id, job.id);
|
||||
Ok(output)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Runner '{}': Job {} failed: {}", self.runner_id, job.id, e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn runner_id(&self) -> &str {
|
||||
&self.runner_id
|
||||
}
|
||||
|
||||
fn redis_url(&self) -> &str {
|
||||
&self.redis_url
|
||||
}
|
||||
}
|
||||
66
bin/runners/hero/src/main.rs
Normal file
66
bin/runners/hero/src/main.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
//! Hero Runner - Command Execution Runner
|
||||
//!
|
||||
//! This runner executes commands from job payloads.
|
||||
//! Unlike script-based runners, it directly executes commands from the job payload.
|
||||
|
||||
use hero_runner::runner_trait::spawn_runner;
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
use tokio::sync::mpsc;
|
||||
use std::sync::Arc;
|
||||
|
||||
mod executor;
|
||||
use executor::HeroExecutor;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about = "Hero Runner - Command execution runner", long_about = None)]
|
||||
struct Args {
|
||||
/// Runner ID
|
||||
runner_id: String,
|
||||
|
||||
/// Redis URL
|
||||
#[arg(short = 'r', long, default_value = "redis://localhost:6379")]
|
||||
redis_url: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Initialize logging
|
||||
env_logger::init();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
info!("Starting Hero Command Runner with ID: {}", args.runner_id);
|
||||
info!("Redis URL: {}", args.redis_url);
|
||||
|
||||
// Create shutdown channel
|
||||
let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>(1);
|
||||
|
||||
// Setup signal handling for graceful shutdown
|
||||
let shutdown_tx_clone = shutdown_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::signal::ctrl_c().await.expect("Failed to listen for ctrl+c");
|
||||
info!("Received Ctrl+C, initiating shutdown...");
|
||||
let _ = shutdown_tx_clone.send(()).await;
|
||||
});
|
||||
|
||||
// Create executor
|
||||
let executor = HeroExecutor::new(
|
||||
args.runner_id.clone(),
|
||||
args.redis_url.clone(),
|
||||
);
|
||||
|
||||
// Wrap in Arc for the runner trait
|
||||
let executor = Arc::new(executor);
|
||||
|
||||
// Spawn the runner using the trait method
|
||||
let runner_handle = spawn_runner(executor, shutdown_rx);
|
||||
|
||||
info!("Hero runner '{}' is now running", args.runner_id);
|
||||
|
||||
// Wait for runner to finish (shutdown is handled by the runner itself)
|
||||
runner_handle.await??;
|
||||
|
||||
info!("Hero runner '{}' shutdown complete", args.runner_id);
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user