use crate::errors::{AtentoError, Result};
use crate::interpreter;
#[cfg(unix)]
use std::fs::Permissions;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
const TEMP_FILENAME: &str = "atento_temp_file_";
const STDERR_FILTER_PATTERNS: &[&str] = &["[Perftrack", "NamedPipeIPC"];
const DEFAULT_RUNNER_TIMEOUT_SECS: u64 = 86400;
struct TempRemover(PathBuf);
impl Drop for TempRemover {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
pub struct RunnerResult {
pub exit_code: i32,
pub duration_ms: u128,
pub stdout: Option<String>,
pub stderr: Option<String>,
}
pub fn run(
script: &str,
interpreter: &interpreter::Interpreter,
timeout_secs: u64,
) -> Result<RunnerResult> {
if script.is_empty() {
return Err(AtentoError::Runner("Script cannot be empty".to_string()));
}
if !interpreter.is_valid() {
return Err(AtentoError::Runner(
"Interpreter has invalid configuration".to_string(),
));
}
let mut path = std::env::temp_dir();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let filename = format!("{TEMP_FILENAME}{nanos}{}", interpreter.extension);
path.push(filename);
std::fs::write(&path, format!("{script}\n"))
.map_err(|e| AtentoError::Runner(format!("Failed to write temp script file: {e}")))?;
#[cfg(unix)]
{
let perm = Permissions::from_mode(0o700);
std::fs::set_permissions(&path, perm)
.map_err(|e| AtentoError::Runner(format!("Failed to set permissions: {e}")))?;
}
let _remover = TempRemover(path.clone());
let mut cmd = Command::new(interpreter.command.as_str());
if !interpreter.args.is_empty() {
cmd.args(&interpreter.args);
}
if interpreter.extension == ".ps1" {
cmd.env("POWERSHELL_TELEMETRY_OPTOUT", "1");
}
let mut child = cmd
.arg(&path)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| AtentoError::Runner(format!("Failed to start command: {e}")))?;
let timeout = if timeout_secs > 0 {
Duration::from_secs(timeout_secs)
} else {
Duration::from_secs(DEFAULT_RUNNER_TIMEOUT_SECS)
};
let start = Instant::now();
loop {
if let Some(_status) = child
.try_wait()
.map_err(|e| AtentoError::Execution(format!("Failed to check process: {e}")))?
{
let output = child.wait_with_output().map_err(|e| {
AtentoError::Execution(format!("Failed to wait for process output: {e}"))
})?;
return Ok(process_result(&start, &output));
}
if start.elapsed() >= timeout {
let _ = child
.kill()
.map_err(|e| AtentoError::Execution(format!("Failed to kill process: {e}")));
return Err(AtentoError::Timeout {
context: "Step execution timed out".to_string(),
timeout_secs,
});
}
std::thread::sleep(Duration::from_millis(100)); }
}
fn process_result(start: &Instant, output: &std::process::Output) -> RunnerResult {
let elapsed = start.elapsed();
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let exit_code = output.status.code().unwrap_or(-1);
let stderr = {
let raw = String::from_utf8_lossy(&output.stderr);
raw.lines()
.filter(|line| !STDERR_FILTER_PATTERNS.iter().any(|pat| line.contains(pat)))
.collect::<Vec<_>>()
.join("\n")
};
RunnerResult {
exit_code,
stdout: Some(stdout.trim().to_string()).filter(|s| !s.is_empty()),
stderr: Some(stderr.trim().to_string()).filter(|s| !s.is_empty()),
duration_ms: elapsed.as_millis(),
}
}