Skip to main content

dk_runner/executor/
process.rs

1use std::collections::HashMap;
2use std::path::Path;
3use std::time::{Duration, Instant};
4use super::{Executor, StepOutput, StepStatus};
5
6pub struct ProcessExecutor;
7
8impl ProcessExecutor {
9    #[allow(clippy::new_without_default)]
10    pub fn new() -> Self { Self }
11}
12
13const SAFE_ENV_VARS: &[&str] = &["PATH", "HOME", "LANG", "TERM", "USER", "SHELL"];
14
15#[async_trait::async_trait]
16impl Executor for ProcessExecutor {
17    async fn run_command(
18        &self,
19        command: &str,
20        work_dir: &Path,
21        timeout: Duration,
22        env: &HashMap<String, String>,
23    ) -> StepOutput {
24        let start = Instant::now();
25        let mut cmd = tokio::process::Command::new("sh");
26        cmd.arg("-c").arg(command);
27        cmd.current_dir(work_dir);
28        cmd.env_clear();
29        for var in SAFE_ENV_VARS {
30            if let Ok(val) = std::env::var(var) {
31                cmd.env(var, val);
32            }
33        }
34        for (k, v) in env {
35            cmd.env(k, v);
36        }
37        let result = tokio::time::timeout(timeout, cmd.output()).await;
38        let duration = start.elapsed();
39        match result {
40            Ok(Ok(output)) => {
41                let stdout = String::from_utf8_lossy(&output.stdout).to_string();
42                let stderr = String::from_utf8_lossy(&output.stderr).to_string();
43                let status = if output.status.success() { StepStatus::Pass } else { StepStatus::Fail };
44                StepOutput { status, stdout, stderr, duration }
45            }
46            Ok(Err(e)) => StepOutput {
47                status: StepStatus::Fail, stdout: String::new(),
48                stderr: format!("command error: {e}"), duration,
49            },
50            Err(_) => StepOutput {
51                status: StepStatus::Timeout, stdout: String::new(),
52                stderr: format!("command timed out after {}s", timeout.as_secs()), duration,
53            },
54        }
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[tokio::test]
63    async fn test_echo_passes() {
64        let exec = ProcessExecutor::new();
65        let dir = std::env::temp_dir();
66        let out = exec.run_command("echo hello", &dir, Duration::from_secs(5), &HashMap::new()).await;
67        assert_eq!(out.status, StepStatus::Pass);
68        assert!(out.stdout.contains("hello"));
69    }
70
71    #[tokio::test]
72    async fn test_false_fails() {
73        let exec = ProcessExecutor::new();
74        let dir = std::env::temp_dir();
75        let out = exec.run_command("false", &dir, Duration::from_secs(5), &HashMap::new()).await;
76        assert_eq!(out.status, StepStatus::Fail);
77    }
78
79    #[tokio::test]
80    async fn test_timeout() {
81        let exec = ProcessExecutor::new();
82        let dir = std::env::temp_dir();
83        let out = exec.run_command("sleep 10", &dir, Duration::from_millis(100), &HashMap::new()).await;
84        assert_eq!(out.status, StepStatus::Timeout);
85    }
86
87    #[tokio::test]
88    async fn test_env_injection() {
89        let exec = ProcessExecutor::new();
90        let dir = std::env::temp_dir();
91        let mut env = HashMap::new();
92        env.insert("DKOD_TEST".to_string(), "yes".to_string());
93        let out = exec.run_command("echo $DKOD_TEST", &dir, Duration::from_secs(5), &env).await;
94        assert_eq!(out.status, StepStatus::Pass);
95        assert!(out.stdout.contains("yes"));
96    }
97}