asimov_runner/
executor.rs1use 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"); 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}