blue_build_utils/
command_output.rs1use 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
17fn 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
49pub 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}