greentic_dev/util/
process.rs1use std::ffi::OsString;
2use std::path::PathBuf;
3use std::process::{Command, ExitStatus, Stdio};
4
5use anyhow::{Context, Result};
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub enum StreamMode {
9 Inherit,
10 Capture,
11}
12
13pub struct CommandSpec {
14 pub program: OsString,
15 pub args: Vec<OsString>,
16 pub env: Vec<(OsString, OsString)>,
17 pub current_dir: Option<PathBuf>,
18 pub stdout: StreamMode,
19 pub stderr: StreamMode,
20}
21
22impl CommandSpec {
23 pub fn new(program: impl Into<OsString>) -> Self {
24 Self {
25 program: program.into(),
26 args: Vec::new(),
27 env: Vec::new(),
28 current_dir: None,
29 stdout: StreamMode::Inherit,
30 stderr: StreamMode::Inherit,
31 }
32 }
33}
34
35pub struct CommandOutput {
36 pub status: ExitStatus,
37 #[allow(dead_code)]
38 pub stdout: Option<Vec<u8>>,
39 pub stderr: Option<Vec<u8>>,
40}
41
42pub fn run(spec: CommandSpec) -> Result<CommandOutput> {
43 let mut command = Command::new(&spec.program);
44 command.args(&spec.args);
45 if let Some(dir) = &spec.current_dir {
46 command.current_dir(dir);
47 }
48 for (key, value) in &spec.env {
49 command.env(key, value);
50 }
51
52 match (spec.stdout, spec.stderr) {
53 (StreamMode::Inherit, StreamMode::Inherit) => {
54 command.stdout(Stdio::inherit());
55 command.stderr(Stdio::inherit());
56 let status = command
57 .status()
58 .with_context(|| format!("failed to spawn `{}`", spec.program.to_string_lossy()))?;
59 Ok(CommandOutput {
60 status,
61 stdout: None,
62 stderr: None,
63 })
64 }
65 (StreamMode::Capture, StreamMode::Capture) => {
66 command.stdout(Stdio::piped());
67 command.stderr(Stdio::piped());
68 let output = command
69 .output()
70 .with_context(|| format!("failed to spawn `{}`", spec.program.to_string_lossy()))?;
71 Ok(CommandOutput {
72 status: output.status,
73 stdout: Some(output.stdout),
74 stderr: Some(output.stderr),
75 })
76 }
77 _ => anyhow::bail!("mixed capture/inherit mode is not supported yet"),
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::{CommandSpec, StreamMode, run};
84 use std::ffi::OsString;
85
86 #[test]
87 fn capture_mode_collects_stdout_and_stderr() {
88 let mut spec = CommandSpec::new("sh");
89 spec.args = vec![
90 OsString::from("-c"),
91 OsString::from("printf hello; printf world >&2"),
92 ];
93 spec.stdout = StreamMode::Capture;
94 spec.stderr = StreamMode::Capture;
95
96 let output = run(spec).unwrap();
97 assert!(output.status.success());
98 assert_eq!(output.stdout.unwrap(), b"hello");
99 assert_eq!(output.stderr.unwrap(), b"world");
100 }
101
102 #[test]
103 fn inherit_mode_returns_status_without_buffers() {
104 let mut spec = CommandSpec::new("sh");
105 spec.args = vec![OsString::from("-c"), OsString::from("exit 0")];
106
107 let output = run(spec).unwrap();
108 assert!(output.status.success());
109 assert!(output.stdout.is_none());
110 assert!(output.stderr.is_none());
111 }
112
113 #[test]
114 fn mixed_modes_are_rejected() {
115 let mut spec = CommandSpec::new("sh");
116 spec.args = vec![OsString::from("-c"), OsString::from("exit 0")];
117 spec.stdout = StreamMode::Capture;
118 spec.stderr = StreamMode::Inherit;
119
120 let err = run(spec).err().expect("expected mixed-mode failure");
121 assert!(err.to_string().contains("mixed capture/inherit mode"));
122 }
123}