blue_build_utils/
command_output.rs

1use std::{
2    ffi::OsStr,
3    fmt::Debug,
4    io::{Error, ErrorKind, Result},
5    process::{Command, Stdio},
6    time::{Duration, Instant},
7};
8
9use process_control::{ChildExt, Control};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct CommandOutput {
13    pub stdout: String,
14    pub stderr: String,
15}
16
17/// # Attempt to resolve `binary_name` from and creates a new `Command` pointing at it
18/// # This allows executing cmd files on Windows and prevents running executable from cwd on Windows
19/// # This function also initializes std{err,out,in} to protect against processes changing the console mode
20/// #
21/// # Errors
22///
23fn create_command<T: AsRef<OsStr>>(binary_name: T) -> Result<Command> {
24    let binary_name = binary_name.as_ref();
25    log::trace!("Creating Command for binary {}", binary_name.display());
26
27    let full_path = match which::which(binary_name) {
28        Ok(full_path) => {
29            log::trace!("Using {} as {}", full_path.display(), binary_name.display());
30            full_path
31        }
32        Err(error) => {
33            log::trace!(
34                "Unable to find {} in PATH, {error:?}",
35                binary_name.display()
36            );
37            return Err(Error::new(ErrorKind::NotFound, error));
38        }
39    };
40
41    let mut cmd = Command::new(full_path);
42    cmd.stderr(Stdio::piped())
43        .stdout(Stdio::piped())
44        .stdin(Stdio::null());
45
46    Ok(cmd)
47}
48
49/// Execute a command and return the output on stdout and stderr if successful
50pub fn exec_cmd<T: AsRef<OsStr> + Debug, U: AsRef<OsStr> + Debug>(
51    cmd: T,
52    args: &[U],
53    time_limit: Duration,
54) -> Option<CommandOutput> {
55    log::trace!("Executing command {cmd:?} with args {args:?}");
56    internal_exec_cmd(cmd, args, time_limit)
57}
58
59fn internal_exec_cmd<T: AsRef<OsStr> + Debug, U: AsRef<OsStr> + Debug>(
60    cmd: T,
61    args: &[U],
62    time_limit: Duration,
63) -> Option<CommandOutput> {
64    let mut cmd = create_command(cmd).ok()?;
65    cmd.args(args);
66    exec_timeout(&mut cmd, time_limit)
67}
68
69fn exec_timeout(cmd: &mut Command, time_limit: Duration) -> Option<CommandOutput> {
70    let start = Instant::now();
71    let process = match cmd.spawn() {
72        Ok(process) => process,
73        Err(error) => {
74            log::trace!("Unable to run {}, {:?}", cmd.get_program().display(), error);
75            return None;
76        }
77    };
78    match process
79        .controlled_with_output()
80        .time_limit(time_limit)
81        .terminate_for_timeout()
82        .wait()
83    {
84        Ok(Some(output)) => {
85            let stdout_string = match String::from_utf8(output.stdout) {
86                Ok(stdout) => stdout,
87                Err(error) => {
88                    log::warn!("Unable to decode stdout: {error:?}");
89                    return None;
90                }
91            };
92            let stderr_string = match String::from_utf8(output.stderr) {
93                Ok(stderr) => stderr,
94                Err(error) => {
95                    log::warn!("Unable to decode stderr: {error:?}");
96                    return None;
97                }
98            };
99
100            log::trace!(
101                "stdout: {:?}, stderr: {:?}, exit code: \"{:?}\", took {:?}",
102                stdout_string,
103                stderr_string,
104                output.status.code(),
105                start.elapsed()
106            );
107
108            if !output.status.success() {
109                return None;
110            }
111
112            Some(CommandOutput {
113                stdout: stdout_string,
114                stderr: stderr_string,
115            })
116        }
117        Ok(None) => {
118            log::warn!(
119                "Executing command {} timed out.",
120                cmd.get_program().display()
121            );
122            log::warn!(
123                "You can set command_timeout in your config to a higher value to allow longer-running commands to keep executing."
124            );
125            None
126        }
127        Err(error) => {
128            log::trace!(
129                "Executing command {} failed by: {:?}",
130                cmd.get_program().display(),
131                error
132            );
133            None
134        }
135    }
136}