Skip to main content

apollo/
process_cmd.rs

1//! Spawn processes from a command line without `sh -c` (no shell metacharacter interpretation).
2
3use std::process::Stdio;
4use std::time::Duration;
5
6use tokio::process::Command;
7
8/// Run `command` as argv[0..] via `shlex` (no shell). Returns (stdout+stderr text, success).
9pub async fn run_argv_command(command: &str, timeout_secs: u64) -> anyhow::Result<(String, bool)> {
10    let trimmed = command.trim();
11    if trimmed.is_empty() {
12        anyhow::bail!("empty command");
13    }
14
15    let parts = shlex::split(trimmed).ok_or_else(|| anyhow::anyhow!("invalid command quoting"))?;
16    if parts.is_empty() {
17        anyhow::bail!("empty command");
18    }
19
20    let program = &parts[0];
21    let args = &parts[1..];
22
23    let child = Command::new(program)
24        .args(args)
25        .stdout(Stdio::piped())
26        .stderr(Stdio::piped())
27        .spawn()?;
28
29    let output =
30        match tokio::time::timeout(Duration::from_secs(timeout_secs), child.wait_with_output())
31            .await
32        {
33            Ok(result) => result?,
34            Err(_) => anyhow::bail!("command timed out after {}s", timeout_secs),
35        };
36
37    let stdout = String::from_utf8_lossy(&output.stdout);
38    let stderr = String::from_utf8_lossy(&output.stderr);
39    let combined = if stderr.is_empty() {
40        stdout.into_owned()
41    } else if stdout.is_empty() {
42        stderr.into_owned()
43    } else {
44        format!("{stdout}{stderr}")
45    };
46
47    Ok((combined, output.status.success()))
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[tokio::test]
55    async fn run_echo_no_shell() {
56        let (out, ok) = run_argv_command("echo hello", 5).await.unwrap();
57        assert!(ok);
58        assert!(out.contains("hello"));
59    }
60
61    #[tokio::test]
62    async fn semicolon_not_shell_metachar() {
63        let (out, ok) = run_argv_command("echo one;two", 5).await.unwrap();
64        assert!(ok);
65        assert!(out.contains("one;two") || out.contains("one"));
66    }
67}