nd_300/actions/fix/
cmd.rs1use std::process::Output;
2use std::time::{Duration, Instant};
3use tokio::process::Command;
4
5pub const TIMEOUT_QUICK: Duration = Duration::from_secs(15);
6pub const TIMEOUT_MEDIUM: Duration = Duration::from_secs(30);
7pub const TIMEOUT_SLOW: Duration = Duration::from_secs(60);
8
9pub async fn run_cmd(mut cmd: Command, timeout: Duration) -> Result<Output, String> {
10 let label = format!("{:?}", cmd.as_std().get_program());
11 match tokio::time::timeout(timeout, cmd.output()).await {
12 Ok(Ok(output)) => Ok(output),
13 Ok(Err(e)) => Err(format!("{} failed: {}", label, e)),
14 Err(_) => Err(format!("{} timed out after {}s", label, timeout.as_secs())),
15 }
16}
17
18#[derive(Debug, Clone)]
22pub struct CmdOutcome {
23 pub command: String,
25 pub args: Vec<String>,
27 pub exit_code: Option<i32>,
30 pub stdout: String,
33 pub stderr: String,
35 pub duration: Duration,
38 pub ok: bool,
41 pub error: Option<String>,
44}
45
46impl CmdOutcome {
47 pub fn summary(&self) -> String {
50 if self.ok {
51 format!("ok ({:.1}s)", self.duration.as_secs_f64())
52 } else if let Some(err) = &self.error {
53 format!("failed: {}", err)
54 } else if let Some(code) = self.exit_code {
55 format!("exit {} ({:.1}s)", code, self.duration.as_secs_f64())
56 } else {
57 "failed".to_string()
58 }
59 }
60
61 pub fn cmdline(&self) -> String {
63 let mut s = self.command.clone();
64 for a in &self.args {
65 s.push(' ');
66 if a.contains(' ') {
67 s.push('"');
68 s.push_str(a);
69 s.push('"');
70 } else {
71 s.push_str(a);
72 }
73 }
74 s
75 }
76}
77
78pub async fn run_cmd_capture(mut cmd: Command, timeout: Duration) -> CmdOutcome {
87 let std_cmd = cmd.as_std();
88 let command = std_cmd.get_program().to_string_lossy().into_owned();
89 let args: Vec<String> = std_cmd
90 .get_args()
91 .map(|s| s.to_string_lossy().into_owned())
92 .collect();
93
94 let started = Instant::now();
95 let raced = tokio::time::timeout(timeout, cmd.output()).await;
96 let duration = started.elapsed();
97
98 match raced {
99 Ok(Ok(output)) => {
100 let exit_code = output.status.code();
101 let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
102 let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
103 let ok = output.status.success();
104 CmdOutcome {
105 command,
106 args,
107 exit_code,
108 stdout,
109 stderr,
110 duration,
111 ok,
112 error: None,
113 }
114 }
115 Ok(Err(e)) => CmdOutcome {
116 command,
117 args,
118 exit_code: None,
119 stdout: String::new(),
120 stderr: String::new(),
121 duration,
122 ok: false,
123 error: Some(format!("spawn failed: {}", e)),
124 },
125 Err(_) => CmdOutcome {
126 command,
127 args,
128 exit_code: None,
129 stdout: String::new(),
130 stderr: String::new(),
131 duration,
132 ok: false,
133 error: Some(format!("timed out after {}s", timeout.as_secs())),
134 },
135 }
136}