Skip to main content

orch/
cmd.rs

1//! Thin wrapper around `std::process::Command` / `tokio::process::Command`
2//! that attaches the binary path to any spawn error so "No such file or
3//! directory" always says *which* file was missing.
4
5use std::ffi::OsStr;
6use std::path::PathBuf;
7
8use tokio::process;
9
10use std::future::Future;
11use std::process::Command;
12use std::process::ExitStatus;
13use std::process::Output;
14
15/// Extension trait that maps spawn errors to include the program path.
16pub trait CommandErrorContext {
17    /// Like `.output()` but the error includes the program name.
18    fn output_with_context(&mut self) -> impl Future<Output = anyhow::Result<Output>>;
19}
20
21impl CommandErrorContext for process::Command {
22    async fn output_with_context(&mut self) -> anyhow::Result<Output> {
23        let prog = program_name(self.as_std());
24        self.output()
25            .await
26            .map_err(|e| anyhow::anyhow!("failed to execute `{prog}`: {e}"))
27    }
28}
29
30/// Sync version for `std::process::Command`.
31pub trait SyncCommandErrorContext {
32    fn output_with_context(&mut self) -> anyhow::Result<Output>;
33    fn status_with_context(&mut self) -> anyhow::Result<ExitStatus>;
34}
35
36impl SyncCommandErrorContext for Command {
37    fn output_with_context(&mut self) -> anyhow::Result<Output> {
38        let prog = program_name(self);
39        self.output()
40            .map_err(|e| anyhow::anyhow!("failed to execute `{prog}`: {e}"))
41    }
42
43    fn status_with_context(&mut self) -> anyhow::Result<ExitStatus> {
44        let prog = program_name(self);
45        self.status()
46            .map_err(|e| anyhow::anyhow!("failed to execute `{prog}`: {e}"))
47    }
48}
49
50/// Extract the program name from a `std::process::Command`.
51fn program_name(cmd: &Command) -> String {
52    let prog: &OsStr = cmd.get_program();
53    let path = PathBuf::from(prog);
54    path.display().to_string()
55}