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
8/// Extension trait that maps spawn errors to include the program path.
9pub trait CommandErrorContext {
10    /// Like `.output()` but the error includes the program name.
11    fn output_with_context(
12        &mut self,
13    ) -> impl std::future::Future<Output = anyhow::Result<std::process::Output>>;
14}
15
16impl CommandErrorContext for tokio::process::Command {
17    async fn output_with_context(&mut self) -> anyhow::Result<std::process::Output> {
18        let prog = program_name(self.as_std());
19        self.output()
20            .await
21            .map_err(|e| anyhow::anyhow!("failed to execute `{prog}`: {e}"))
22    }
23}
24
25/// Sync version for `std::process::Command`.
26pub trait SyncCommandErrorContext {
27    fn output_with_context(&mut self) -> anyhow::Result<std::process::Output>;
28    fn status_with_context(&mut self) -> anyhow::Result<std::process::ExitStatus>;
29}
30
31impl SyncCommandErrorContext for std::process::Command {
32    fn output_with_context(&mut self) -> anyhow::Result<std::process::Output> {
33        let prog = program_name(self);
34        self.output()
35            .map_err(|e| anyhow::anyhow!("failed to execute `{prog}`: {e}"))
36    }
37
38    fn status_with_context(&mut self) -> anyhow::Result<std::process::ExitStatus> {
39        let prog = program_name(self);
40        self.status()
41            .map_err(|e| anyhow::anyhow!("failed to execute `{prog}`: {e}"))
42    }
43}
44
45/// Extract the program name from a `std::process::Command`.
46fn program_name(cmd: &std::process::Command) -> String {
47    let prog: &OsStr = cmd.get_program();
48    let path = PathBuf::from(prog);
49    path.display().to_string()
50}