Skip to main content

vantage_cmd/
exec.rs

1//! Subprocess execution with a locked-down environment.
2//!
3//! Synchronous on purpose: callers run this inside
4//! `tokio::task::spawn_blocking`, so the std blocking call never stalls
5//! the async runtime. The child gets a *cleared* environment plus only
6//! the declared vars (and optionally `PATH`/`HOME`).
7
8use indexmap::IndexMap;
9use vantage_core::{Result, error};
10
11/// Captured result of a single command invocation.
12#[derive(Clone, Debug)]
13pub struct CmdOutput {
14    pub stdout: String,
15    pub stderr: String,
16    pub exit_code: i32,
17}
18
19/// Run `command` with `args`, passing only `env` (plus `PATH`/`HOME` when
20/// `pass_path`). Returns the captured output; a non-zero exit is *not* an
21/// error here — the Rhai script decides what to do with `exit_code`.
22pub fn run_command(
23    command: &str,
24    args: &[String],
25    env: &IndexMap<String, String>,
26    pass_path: bool,
27) -> Result<CmdOutput> {
28    let mut cmd = std::process::Command::new(command);
29    cmd.args(args);
30    cmd.env_clear();
31
32    if pass_path {
33        if let Ok(path) = std::env::var("PATH") {
34            cmd.env("PATH", path);
35        }
36        if let Ok(home) = std::env::var("HOME") {
37            cmd.env("HOME", home);
38        }
39    }
40    for (k, v) in env {
41        cmd.env(k, v);
42    }
43
44    let output = cmd.output().map_err(|e| {
45        error!(
46            "failed to execute command",
47            command = command.to_string(),
48            detail = e.to_string()
49        )
50    })?;
51
52    Ok(CmdOutput {
53        stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
54        stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
55        exit_code: output.status.code().unwrap_or(-1),
56    })
57}