//! 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> { info!("Runner '{}': Executing hero run for job {}", self.runner_id, job.id); // Execute: hero run -s (reads from stdin) let mut cmd = Command::new("hero"); cmd.args(&["run", "-s"]); debug!("Runner '{}': Executing: hero run -s with stdin", self.runner_id); // Set environment variables from job for (key, value) in &job.env_vars { cmd.env(key, value); } // Configure stdio - pipe stdin to send heroscript content cmd.stdin(Stdio::piped()) .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 -s': {}", e))?; // Write heroscript payload to stdin if let Some(mut stdin) = child.stdin.take() { use std::io::Write; stdin.write_all(job.payload.as_bytes()) .map_err(|e| format!("Failed to write to stdin: {}", e))?; // Close stdin to signal EOF drop(stdin); } // 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> { 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 } }