dk_runner/executor/
process.rs1use 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}