1use anyhow::Context;
2use std::process::Output;
3use std::process::Stdio;
4use tokio::process::Command;
5use tracing::debug;
6
7use crate::sdk::{command_debug_log, command_failure_message, decode_output};
8
9#[derive(Debug)]
11pub struct CommandOutcome {
12 pub output: Output,
13 pub stdout: String,
14 pub stderr: String,
15}
16
17impl CommandOutcome {
18 pub fn is_success(&self) -> bool {
19 self.output.status.success()
20 }
21}
22
23#[derive(Clone, Debug)]
25pub struct CommandRunner {
26 debug: bool,
27}
28
29impl CommandRunner {
30 pub fn new(debug: bool) -> Self {
31 Self { debug }
32 }
33
34 pub async fn run(&self, program: &str, args: &[&str]) -> anyhow::Result<CommandOutcome> {
35 let output = Command::new(program)
36 .args(args)
37 .output()
38 .await
39 .with_context(|| format!("Failed to execute {} {}", program, args.join(" ")))?;
40
41 command_debug_log(self.debug, program, args, &output, |msg| debug!("{}", msg));
42
43 let stdout = decode_output(&output.stdout);
44 let stderr = decode_output(&output.stderr);
45
46 Ok(CommandOutcome {
47 output,
48 stdout,
49 stderr,
50 })
51 }
52
53 pub async fn run_checked(
54 &self,
55 program: &str,
56 args: &[&str],
57 hint: Option<&str>,
58 ) -> anyhow::Result<CommandOutcome> {
59 let outcome = self.run(program, args).await?;
60 if !outcome.output.status.success() {
61 let message = command_failure_message(program, args, &outcome.output, hint);
62 return Err(anyhow::anyhow!(message));
63 }
64 Ok(outcome)
65 }
66
67 pub async fn run_with_stdio(
68 &self,
69 program: &str,
70 args: &[&str],
71 ) -> anyhow::Result<std::process::ExitStatus> {
72 let status = Command::new(program)
73 .args(args)
74 .stdout(Stdio::inherit())
75 .stderr(Stdio::inherit())
76 .status()
77 .await
78 .with_context(|| format!("Failed to execute {} {}", program, args.join(" ")))?;
79
80 if self.debug {
81 debug!(
82 "{} {} -> status={}",
83 program,
84 args.join(" "),
85 status
86 .code()
87 .map(|c| c.to_string())
88 .unwrap_or_else(|| "SIG".into())
89 );
90 }
91 Ok(status)
92 }
93
94 pub async fn run_builder<F>(
95 &self,
96 program: &str,
97 args: &[&str],
98 configure: F,
99 ) -> anyhow::Result<CommandOutcome>
100 where
101 F: FnOnce(&mut Command),
102 {
103 let mut cmd = Command::new(program);
104 cmd.args(args);
105 configure(&mut cmd);
106
107 let output = cmd
108 .output()
109 .await
110 .with_context(|| format!("Failed to execute {} {}", program, args.join(" ")))?;
111
112 command_debug_log(self.debug, program, args, &output, |msg| debug!("{}", msg));
113
114 let stdout = decode_output(&output.stdout);
115 let stderr = decode_output(&output.stderr);
116
117 Ok(CommandOutcome {
118 output,
119 stdout,
120 stderr,
121 })
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[tokio::test]
130 async fn command_runner_success() {
131 let runner = CommandRunner::new(true);
132 #[cfg(windows)]
133 let outcome = runner
134 .run_checked("cmd", &["/C", "echo hello"], None)
135 .await
136 .unwrap();
137
138 #[cfg(not(windows))]
139 let outcome = runner
140 .run_checked("sh", &["-c", "echo hello"], None)
141 .await
142 .unwrap();
143
144 assert!(outcome.is_success());
145 assert_eq!(outcome.stdout, "hello");
146 }
147
148 #[tokio::test]
149 async fn command_runner_failure_returns_error() {
150 let runner = CommandRunner::new(false);
151 #[cfg(windows)]
152 let err = runner
153 .run_checked("cmd", &["/C", "exit /b 1"], Some("intentional failure"))
154 .await
155 .unwrap_err();
156
157 #[cfg(not(windows))]
158 let err = runner
159 .run_checked("sh", &["-c", "false"], Some("intentional failure"))
160 .await
161 .unwrap_err();
162
163 assert!(err.to_string().contains("failed with status"));
164 }
165}