1use std::path::Path;
6pub use tokio::process::Command;
7
8use crate::redact;
9
10#[derive(thiserror::Error, Debug)]
12pub enum Error {
13 #[error("Unable to run subcommand {command}")]
15 IO {
16 command: String,
18 source: std::io::Error,
20 },
21
22 #[error("Command {command:?} failed with STDERR: {stderr}")]
24 CommandError {
25 command: String,
27 status: std::process::ExitStatus,
29 stderr: String,
31 stdout: String,
33 },
34}
35
36impl Error {
37 pub fn status(&self) -> Option<i32> {
39 if let Error::CommandError {
40 command: _,
41 status,
42 stderr: _,
43 stdout: _,
44 } = self
45 {
46 status.code()
47 } else {
48 None
49 }
50 }
51}
52
53pub trait CommandBuilder {
55 fn build(&self, current_dir: &Path) -> Command;
58}
59
60#[tracing::instrument(name = "execute", skip_all)]
61pub async fn execute<T>(builder: &T, current_dir: &Path) -> Result<(String, String), Error>
63where
64 T: CommandBuilder,
65{
66 let mut cmd = builder.build(current_dir);
67 cmd.stdin(std::process::Stdio::null());
68 let child_process_output = cmd.output().await.map_err(|e| Error::IO {
69 command: format!("{cmd:?}"),
70 source: e,
71 })?;
72 let succeeded = child_process_output.status.success();
73 let out = String::from_utf8_lossy(&child_process_output.stdout);
74 let err = String::from_utf8_lossy(&child_process_output.stderr);
75 tracing::debug!(
76 "Command:{:?} \n Exit code:{:?} \n Stdout: {}, Stderr: {} \n",
77 cmd,
78 child_process_output.status.code(),
79 out,
80 err,
81 );
82 if !succeeded {
83 tracing::debug!(
84 "Failed to run {:?}, Status was: {:?}, Stdout was:\n{:?},Stderr was:\n{:?}",
85 cmd,
86 child_process_output.status.code(),
87 out,
88 err
89 );
90 Err(Error::CommandError {
91 status: child_process_output.status,
92 stdout: redact::redact(&out.to_string()),
93 stderr: redact::redact(&err.to_string()),
94 command: redact::redact(&format!("{cmd:?}")),
95 })
96 } else {
97 Ok((out.to_string(), err.to_string()))
98 }
99}