132 lines
4.3 KiB
Rust
132 lines
4.3 KiB
Rust
//! 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 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<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
|
|
}
|
|
}
|