Skip to main content

dejavu/exec/
spawn.rs

1//! Process spawning behind a `CommandRunner` seam (so bench/tests can inject
2//! fixture output), plus the single exit-code normalization point.
3
4use super::ExecOutcome;
5use crate::env;
6use std::ffi::OsString;
7use std::os::unix::process::ExitStatusExt;
8use std::path::PathBuf;
9use std::process::{Command, ExitStatus, Stdio};
10use std::time::Instant;
11
12/// What to run and how.
13pub struct SpawnSpec {
14    pub program: PathBuf,
15    pub args: Vec<OsString>,
16    pub cwd: PathBuf,
17    /// Sanitized PATH (shim dir removed) for the child.
18    pub env_path: OsString,
19    /// Capture stdout/stderr (Optimize) vs inherit stdio (Passthrough).
20    pub capture: bool,
21    /// When capturing, whether to inherit stdin (pipe) or use `/dev/null` (tty).
22    pub inherit_stdin: bool,
23    /// Reserved for M3 capped capture; ignored in the full-capture path.
24    pub capture_limit: Option<usize>,
25}
26
27/// Abstraction over "run this command and give me its output + exit code".
28pub trait CommandRunner {
29    fn run(&self, spec: &SpawnSpec) -> std::io::Result<ExecOutcome>;
30}
31
32/// Production runner: actually spawns the process.
33pub struct RealRunner;
34
35impl CommandRunner for RealRunner {
36    fn run(&self, spec: &SpawnSpec) -> std::io::Result<ExecOutcome> {
37        let mut cmd = Command::new(&spec.program);
38        cmd.args(&spec.args)
39            .current_dir(&spec.cwd)
40            .env("PATH", &spec.env_path)
41            // Belt-and-suspenders anti-recursion: even if a shim dir lingers on
42            // the child's PATH, the nested `dejavu run` will fast-passthrough.
43            .env(env::DISABLED, "1");
44
45        if spec.capture {
46            cmd.stdin(if spec.inherit_stdin {
47                Stdio::inherit()
48            } else {
49                Stdio::null()
50            })
51            .stdout(Stdio::piped())
52            .stderr(Stdio::piped());
53
54            let start = Instant::now();
55            let output = cmd.spawn()?.wait_with_output()?;
56            let duration = start.elapsed();
57            Ok(ExecOutcome {
58                stdout: output.stdout,
59                stderr: output.stderr,
60                exit_code: normalized_exit_code(output.status),
61                duration,
62                captured: true,
63                truncated_raw: false,
64            })
65        } else {
66            cmd.stdin(Stdio::inherit())
67                .stdout(Stdio::inherit())
68                .stderr(Stdio::inherit());
69
70            let start = Instant::now();
71            let status = cmd.spawn()?.wait()?;
72            Ok(ExecOutcome {
73                stdout: Vec::new(),
74                stderr: Vec::new(),
75                exit_code: normalized_exit_code(status),
76                duration: start.elapsed(),
77                captured: false,
78                truncated_raw: false,
79            })
80        }
81    }
82}
83
84/// Normalize an `ExitStatus` into an `i32` the way a shell would: a normal exit
85/// yields its code; a signal death yields `128 + signum`.
86pub fn normalized_exit_code(status: ExitStatus) -> i32 {
87    if let Some(code) = status.code() {
88        code
89    } else if let Some(signal) = status.signal() {
90        128 + signal
91    } else {
92        1
93    }
94}