Skip to main content

asimov_runner/
executor.rs

1// This is free and unencumbered software released into the public domain.
2
3use crate::{Command, ExecutorError, ExecutorResult, Input};
4use std::{
5    ffi::OsStr,
6    io::{Cursor, ErrorKind},
7    process::Stdio,
8};
9use tokio::{io::AsyncReadExt, process::Child};
10
11#[derive(Debug)]
12pub struct Executor(Command);
13
14impl Executor {
15    pub fn new(program: impl AsRef<OsStr>) -> Self {
16        let libexec_path = asimov_env::paths::asimov_root()
17            .join("libexec")
18            .join(program.as_ref());
19
20        let mut command = if libexec_path.exists() {
21            Command::new(libexec_path)
22        } else {
23            Command::new(program)
24        };
25
26        command.env("NO_COLOR", "1"); // See: https://no-color.org
27        command.stdin(Stdio::null());
28        command.stdout(Stdio::null());
29        command.stderr(Stdio::null());
30        command.kill_on_drop(true);
31        Self(command)
32    }
33
34    pub fn command(&mut self) -> &mut Command {
35        &mut self.0
36    }
37
38    pub fn ignore_stdin(&mut self) {
39        self.0.stdin(Stdio::null());
40    }
41
42    pub fn ignore_stdout(&mut self) {
43        self.0.stdout(Stdio::null());
44    }
45
46    pub fn ignore_stderr(&mut self) {
47        self.0.stderr(Stdio::null());
48    }
49
50    pub fn capture_stdout(&mut self) {
51        self.0.stdout(Stdio::piped());
52    }
53
54    pub fn capture_stderr(&mut self) {
55        self.0.stderr(Stdio::piped());
56    }
57
58    pub async fn execute(&mut self) -> ExecutorResult {
59        let process = self.spawn().await?;
60        self.wait(process).await
61    }
62
63    pub async fn execute_with_input(&mut self, input: &mut Input) -> ExecutorResult {
64        let mut process = self.spawn().await?;
65        match input {
66            Input::Ignored => {},
67            Input::AsyncRead(reader) => {
68                let mut stdin = process.stdin.take().expect("should capture stdin");
69                tokio::io::copy(&mut *reader, &mut stdin).await?;
70            },
71        }
72        self.wait(process).await
73    }
74
75    pub async fn spawn(&mut self) -> Result<Child, ExecutorError> {
76        match self.0.spawn() {
77            Ok(process) => Ok(process),
78            Err(err) if err.kind() == ErrorKind::NotFound => {
79                let program = self.0.as_std().get_program().to_owned();
80                return Err(ExecutorError::MissingProgram(program));
81            },
82            Err(err) => return Err(ExecutorError::SpawnFailure(err)),
83        }
84    }
85
86    pub async fn wait(&mut self, process: Child) -> ExecutorResult {
87        let output = process.wait_with_output().await?;
88
89        #[cfg(feature = "tracing")]
90        tracing::trace!("The command exited with: {}", output.status);
91
92        if !output.status.success() {
93            return Err(output.into());
94        }
95
96        Ok(Cursor::new(output.stdout))
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[tokio::test]
105    async fn test_success() {
106        let mut runner = Executor::new("curl");
107        runner.command().arg("https://www.google.com");
108        let result = runner.execute().await;
109        assert!(result.is_ok());
110    }
111
112    #[tokio::test]
113    async fn test_missing_program() {
114        let mut runner = Executor::new("this-command-does-not-exist");
115        let result = runner.execute().await;
116        assert!(matches!(result, Err(ExecutorError::MissingProgram(_))));
117    }
118
119    #[cfg(unix)]
120    #[tokio::test]
121    async fn test_spawn_failure() {
122        let mut runner = Executor::new("/dev/null");
123        let result = runner.execute().await;
124        assert!(matches!(result, Err(ExecutorError::SpawnFailure(_))));
125    }
126
127    #[tokio::test]
128    async fn test_unexpected_failure() {
129        let mut runner = Executor::new("curl");
130        let result = runner.execute().await;
131        assert!(matches!(
132            result,
133            Err(ExecutorError::UnexpectedFailure(_, _))
134        ));
135    }
136}