conch_runtime_pshaw/env/
executable.rs

1use crate::env::SubEnvironment;
2use crate::error::CommandError;
3use crate::io::FileDesc;
4use crate::{ExitStatus, EXIT_ERROR};
5use futures_core::future::BoxFuture;
6use std::ffi::OsStr;
7use std::io::{Error as IoError, ErrorKind as IoErrorKind};
8use std::path::Path;
9use std::process::Stdio;
10use tokio::process::Command;
11
12/// Any data required to execute a child process.
13#[derive(Debug, PartialEq, Eq)]
14pub struct ExecutableData<'a> {
15    /// The name/path to the executable.
16    pub name: &'a OsStr,
17    /// Arguments to be provided to the executable.
18    pub args: &'a [&'a OsStr],
19    /// Any environment variables that should be passed to the executable.
20    /// Environment variables from the current process must **NOT** be inherited
21    /// if they do not appear in this collection.
22    pub env_vars: &'a [(&'a OsStr, &'a OsStr)],
23    /// The current working directory the executable should start out with.
24    pub current_dir: &'a Path,
25    /// The executable's standard input will be redirected to this descriptor
26    /// or the equivalent of `/dev/null` if not specified.
27    pub stdin: Option<FileDesc>,
28    /// The executable's standard output will be redirected to this descriptor
29    /// or the equivalent of `/dev/null` if not specified.
30    pub stdout: Option<FileDesc>,
31    /// The executable's standard error will be redirected to this descriptor
32    /// or the equivalent of `/dev/null` if not specified.
33    pub stderr: Option<FileDesc>,
34}
35
36/// An interface for asynchronously spawning executables.
37pub trait ExecutableEnvironment {
38    /// Attempt to spawn the executable command.
39    fn spawn_executable(
40        &self,
41        data: ExecutableData<'_>,
42    ) -> Result<BoxFuture<'static, ExitStatus>, CommandError>;
43}
44
45impl<'a, T: ExecutableEnvironment> ExecutableEnvironment for &'a T {
46    fn spawn_executable(
47        &self,
48        data: ExecutableData<'_>,
49    ) -> Result<BoxFuture<'static, ExitStatus>, CommandError> {
50        (**self).spawn_executable(data)
51    }
52}
53
54/// An `ExecutableEnvironment` implementation that uses `tokio`
55/// to monitor when child processes have exited.
56#[derive(Clone, Debug, Default)]
57#[allow(missing_copy_implementations)]
58pub struct TokioExecEnv(());
59
60impl SubEnvironment for TokioExecEnv {
61    fn sub_env(&self) -> Self {
62        self.clone()
63    }
64}
65
66impl TokioExecEnv {
67    /// Construct a new environment.
68    pub fn new() -> Self {
69        Self(())
70    }
71}
72
73impl ExecutableEnvironment for TokioExecEnv {
74    fn spawn_executable(
75        &self,
76        data: ExecutableData<'_>,
77    ) -> Result<BoxFuture<'static, ExitStatus>, CommandError> {
78        let stdio = |fdes: Option<FileDesc>| fdes.map(Into::into).unwrap_or_else(Stdio::null);
79
80        let name = data.name;
81        let mut cmd = Command::new(&name);
82        cmd.args(data.args)
83            .kill_on_drop(true) // Ensure we clean up any dropped handles
84            .env_clear() // Ensure we don't inherit from the process
85            .current_dir(&data.current_dir)
86            .stdin(stdio(data.stdin))
87            .stdout(stdio(data.stdout))
88            .stderr(stdio(data.stderr));
89
90        // Ensure a PATH env var is defined, otherwise it appears that
91        // things default to the PATH env var defined for the process
92        cmd.env("PATH", "");
93
94        for (k, v) in data.env_vars {
95            cmd.env(k, v);
96        }
97
98        let child = cmd
99            .spawn()
100            .map_err(|err| map_io_err(err, name.to_string_lossy().into_owned()))?;
101
102        Ok(Box::pin(async move {
103            child.await.map(ExitStatus::from).unwrap_or(EXIT_ERROR)
104        }))
105    }
106}
107
108fn map_io_err(err: IoError, name: String) -> CommandError {
109    #[cfg(unix)]
110    fn is_enoexec(err: &IoError) -> bool {
111        Some(::libc::ENOEXEC) == err.raw_os_error()
112    }
113
114    #[cfg(windows)]
115    fn is_enoexec(_err: &IoError) -> bool {
116        false
117    }
118
119    if IoErrorKind::NotFound == err.kind() {
120        CommandError::NotFound(name)
121    } else if is_enoexec(&err) {
122        CommandError::NotExecutable(name)
123    } else {
124        CommandError::Io(err, Some(name))
125    }
126}