add hero runner and clean improve runner lib

This commit is contained in:
Timur Gordon
2025-11-14 02:18:48 +01:00
parent d2ff7835e2
commit 75e62f4730
9 changed files with 694 additions and 148 deletions

View 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
}
}

View 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(())
}