Skip to main content

turtle_build/infrastructure/command_runner/
os.rs

1use crate::infrastructure::{CommandError, CommandRunner};
2use async_trait::async_trait;
3use std::{
4    io,
5    process::{ExitStatus, Output, Stdio},
6};
7use tokio::{process::Command, sync::Semaphore};
8
9#[cfg(any(windows, test))]
10const BLANK_CHARACTERS: [char; 2] = [' ', '\t'];
11
12/// A command runner backed by an operating system.
13#[derive(Debug)]
14pub struct OsCommandRunner {
15    semaphore: Semaphore,
16}
17
18impl OsCommandRunner {
19    /// Creates a command runner.
20    pub fn new(job_limit: usize) -> Self {
21        Self {
22            semaphore: Semaphore::new(job_limit),
23        }
24    }
25
26    fn create_command(command: &str) -> Command {
27        cfg_select! {
28            windows => {
29                let (program, arguments) = split_program(command);
30                let mut process = Command::new(program);
31
32                process.raw_arg(arguments);
33                process
34            }
35            _ => {
36                let mut process = Command::new("sh");
37
38                process.arg("-ec").arg(command);
39                process
40            }
41        }
42    }
43
44    fn error(error: io::Error, process: &Command) -> CommandError {
45        CommandError::new(format!(
46            "{}: {}",
47            error,
48            process.as_std().get_program().display()
49        ))
50    }
51}
52
53#[async_trait]
54impl CommandRunner for OsCommandRunner {
55    async fn run(&self, command: &str) -> Result<Output, CommandError> {
56        let _permit = self.semaphore.acquire().await?;
57        let mut process = Self::create_command(command);
58
59        process
60            .stdin(Stdio::null())
61            .output()
62            .await
63            .map_err(|error| Self::error(error, &process))
64    }
65
66    async fn run_with_console(&self, command: &str) -> Result<ExitStatus, CommandError> {
67        let _permit = self.semaphore.acquire().await?;
68        let mut process = Self::create_command(command);
69
70        // Inherit standard input, output, and error.
71        process
72            .status()
73            .await
74            .map_err(|error| Self::error(error, &process))
75    }
76}
77
78#[cfg(any(windows, test))]
79fn split_program(command: &str) -> (String, &str) {
80    let command = command.trim_start_matches(BLANK_CHARACTERS);
81    let (program, arguments) = command.split_at(
82        command
83            .char_indices()
84            .scan(false, |quoted, (index, character)| {
85                *quoted ^= character == '"';
86
87                Some((index, !*quoted && BLANK_CHARACTERS.contains(&character)))
88            })
89            .find(|&(_, end)| end)
90            .map_or(command.len(), |(index, _)| index),
91    );
92
93    (
94        program.replace('"', ""),
95        arguments.trim_start_matches(BLANK_CHARACTERS),
96    )
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    mod run {
104        use super::*;
105        use pretty_assertions::assert_eq;
106
107        #[tokio::test]
108        async fn run_command() {
109            let output = OsCommandRunner::new(1)
110                .run(cfg_select! {
111                    windows => "cmd /c echo foo",
112                    _ => "echo foo",
113                })
114                .await
115                .unwrap();
116
117            assert!(output.status.success());
118            assert_eq!(String::from_utf8(output.stdout).unwrap().trim_end(), "foo");
119        }
120
121        #[tokio::test]
122        async fn run_command_with_quoted_argument() {
123            let output = OsCommandRunner::new(1)
124                .run(cfg_select! {
125                    windows => "cmd /c \"echo foo  bar\"",
126                    _ => "echo 'foo  bar'",
127                })
128                .await
129                .unwrap();
130
131            assert!(output.status.success());
132            assert_eq!(
133                String::from_utf8(output.stdout).unwrap().trim_end(),
134                "foo  bar"
135            );
136        }
137
138        #[tokio::test]
139        async fn run_failing_command() {
140            assert_eq!(
141                OsCommandRunner::new(1)
142                    .run(cfg_select! {
143                        windows => "cmd /c exit 42",
144                        _ => "exit 42",
145                    })
146                    .await
147                    .unwrap()
148                    .status
149                    .code(),
150                Some(42)
151            );
152        }
153
154        #[cfg(windows)]
155        #[tokio::test]
156        async fn fail_to_run_missing_program() {
157            assert!(
158                OsCommandRunner::new(1)
159                    .run("missing-program foo")
160                    .await
161                    .unwrap_err()
162                    .to_string()
163                    .ends_with(": missing-program")
164            );
165        }
166    }
167
168    mod run_with_console {
169        use super::*;
170        use pretty_assertions::assert_eq;
171
172        #[tokio::test]
173        async fn run_command() {
174            assert!(
175                OsCommandRunner::new(1)
176                    .run_with_console(cfg_select! {
177                        windows => "cmd /c exit 0",
178                        _ => "exit 0",
179                    })
180                    .await
181                    .unwrap()
182                    .success()
183            );
184        }
185
186        #[tokio::test]
187        async fn run_failing_command() {
188            assert_eq!(
189                OsCommandRunner::new(1)
190                    .run_with_console(cfg_select! {
191                        windows => "cmd /c exit 42",
192                        _ => "exit 42",
193                    })
194                    .await
195                    .unwrap()
196                    .code(),
197                Some(42)
198            );
199        }
200
201        #[cfg(windows)]
202        #[tokio::test]
203        async fn fail_to_run_missing_program() {
204            assert!(
205                OsCommandRunner::new(1)
206                    .run_with_console("missing-program foo")
207                    .await
208                    .unwrap_err()
209                    .to_string()
210                    .ends_with(": missing-program")
211            );
212        }
213    }
214
215    mod split_program {
216        use super::*;
217        use pretty_assertions::assert_eq;
218
219        #[test]
220        fn split_no_argument() {
221            assert_eq!(split_program("foo"), ("foo".into(), ""));
222        }
223
224        #[test]
225        fn split_arguments() {
226            assert_eq!(split_program("foo bar baz"), ("foo".into(), "bar baz"));
227            assert_eq!(split_program("foo\tbar"), ("foo".into(), "bar"));
228            assert_eq!(split_program("foo  bar  baz "), ("foo".into(), "bar  baz "));
229        }
230
231        #[test]
232        fn split_quoted_arguments() {
233            assert_eq!(
234                split_program("foo \"bar baz\" \\\"qux"),
235                ("foo".into(), "\"bar baz\" \\\"qux")
236            );
237        }
238
239        #[test]
240        fn split_leading_blanks() {
241            assert_eq!(split_program(" \tfoo bar"), ("foo".into(), "bar"));
242        }
243
244        #[test]
245        fn split_quoted_program() {
246            assert_eq!(
247                split_program("\"C:\\Program Files\\foo.exe\" bar"),
248                ("C:\\Program Files\\foo.exe".into(), "bar")
249            );
250            assert_eq!(split_program("\"foo bar\""), ("foo bar".into(), ""));
251        }
252
253        #[test]
254        fn split_partially_quoted_program() {
255            assert_eq!(
256                split_program("\"C:\\Program Files\"\\foo.exe bar"),
257                ("C:\\Program Files\\foo.exe".into(), "bar")
258            );
259        }
260
261        #[test]
262        fn split_program_with_unclosed_quote() {
263            assert_eq!(split_program("\"foo bar"), ("foo bar".into(), ""));
264        }
265
266        #[test]
267        fn split_empty_command() {
268            assert_eq!(split_program(""), ("".into(), ""));
269            assert_eq!(split_program(" "), ("".into(), ""));
270        }
271    }
272}