Skip to main content

isothrum_runner/process/
system.rs

1//! Real processes, through `process-wrap` (no `unsafe` here): each tool in its own process group
2//! (unix) or job object (Windows), so a timeout or a cancellation ends the whole tree it started
3//! (`vagrant` is a wrapper around `ruby`, `cmd /C az` around `python`), headless on Windows.
4//! Ported from the launcher's `platform/exec.rs` and `platform/exec/group.rs`.
5
6use std::path::Path;
7use std::process::Stdio;
8
9use process_wrap::tokio::{ChildWrapper, CommandWrap};
10use tokio::process::Command;
11
12use super::{Exit, OnLine, ProcessError, ProcessRunner, ProcessSpec, lines, shim};
13use crate::BoxFuture;
14
15/// Starts real processes.
16#[derive(Debug, Clone, Default)]
17pub struct SystemProcesses {
18    /// Drop `DOCKER_HOST` and `DOCKER_CONTEXT` for `docker` (the user picked a context).
19    strip_engine_overrides: bool,
20}
21
22impl SystemProcesses {
23    /// Real processes; `docker_cli_config` is the Docker CLI's `config.json`, when there is one
24    /// (it says whether the user picked an engine context of their own).
25    #[must_use]
26    pub fn new(docker_cli_config: Option<&str>) -> SystemProcesses {
27        SystemProcesses {
28            strip_engine_overrides: shim::strips_engine_overrides(docker_cli_config),
29        }
30    }
31
32    /// [`SystemProcesses::new`] with the Docker CLI config read from `$DOCKER_CONFIG/config.json`,
33    /// else `<home>/.docker/config.json` (`var` reads the environment, `home` is the user's).
34    pub fn for_host(var: impl Fn(&str) -> Option<String>, home: Option<&Path>) -> SystemProcesses {
35        let dir = var("DOCKER_CONFIG").map(std::path::PathBuf::from).or_else(|| home.map(|h| h.join(".docker")));
36        let config = dir.and_then(|d| std::fs::read_to_string(d.join("config.json")).ok());
37        SystemProcesses::new(config.as_deref())
38    }
39
40    async fn run_to_end(&self, spec: &ProcessSpec, on_line: OnLine<'_>) -> Result<Exit, ProcessError> {
41        if let Some(arg) = shim::refused_arg(&spec.program, &spec.args) {
42            return Err(ProcessError::Refused {
43                program: spec.program.clone(),
44                arg: arg.escape_debug().to_string(),
45            });
46        }
47        let io = |source| ProcessError::Io {
48            program: spec.program.clone(),
49            source,
50        };
51        // The program and its arguments only: values reach a tool through its environment,
52        // which is never logged.
53        tracing::debug!(program = %spec.program, args = ?spec.args, "starting a process");
54        let mut cmd = self.command(spec).map_err(io)?;
55        let child = cmd.spawn().map_err(|e| spawned(&spec.program, e))?;
56        let mut tree = Tree(Some(child));
57        let status = match tree.0.as_deref_mut() {
58            Some(started) => lines::forward_lines(started, on_line).await.map_err(io)?,
59            None => return Err(io(std::io::Error::other("the process went away"))),
60        };
61        // It exited on its own: what it left running is its business.
62        tree.0 = None;
63        tracing::debug!(program = %spec.program, code = ?status.code(), "process exited");
64        if shim::missing_behind_cmd(&spec.program, status.code()) {
65            return Err(ProcessError::Missing { program: spec.program.clone() });
66        }
67        Ok(Exit { code: status.code() })
68    }
69
70    /// The command for `spec`: its own group or job, its output piped, headless.
71    fn command(&self, spec: &ProcessSpec) -> std::io::Result<CommandWrap> {
72        let stdin = match &spec.stdin {
73            Some(path) => Stdio::from(std::fs::File::open(path)?),
74            None => Stdio::null(),
75        };
76        let strip = self.strip_engine_overrides && shim::uses_engine(&spec.program);
77        let inherited = std::env::vars_os().filter_map(|(k, _)| k.into_string().ok());
78        let withheld = shim::withheld(&spec.program, inherited);
79        let (program, args) = wrapped(spec);
80        let mut cmd = CommandWrap::with_new(program, |c: &mut Command| {
81            c.args(args).stdin(stdin).stdout(Stdio::piped()).stderr(Stdio::piped());
82            // Before the spec's own: what the run delivers is set again after.
83            for var in &withheld {
84                c.env_remove(var);
85            }
86            c.envs(spec.env.iter().map(|(k, v)| (k.as_str(), v.as_str())));
87            if let Some(dir) = &spec.cwd {
88                c.current_dir(dir);
89            }
90            if strip {
91                for var in shim::ENGINE_OVERRIDES {
92                    c.env_remove(var);
93                }
94            }
95        });
96        group(&mut cmd);
97        Ok(cmd)
98    }
99}
100
101impl ProcessRunner for SystemProcesses {
102    fn run<'a>(&'a self, spec: &'a ProcessSpec, on_line: OnLine<'a>) -> BoxFuture<'a, Result<Exit, ProcessError>> {
103        Box::pin(self.run_to_end(spec, on_line))
104    }
105}
106
107/// A spawned tool's tree, ended when dropped before the tool exited (a timeout, a cancellation).
108struct Tree(Option<Box<dyn ChildWrapper>>);
109
110impl Drop for Tree {
111    fn drop(&mut self) {
112        if let Some(child) = self.0.as_mut() {
113            // SIGKILL to the process group, or the job object terminated: nothing it started
114            // outlives it. Already gone is fine.
115            tracing::debug!(pid = ?child.id(), "ending a process tree cut short");
116            if let Err(e) = child.start_kill() {
117                tracing::debug!(error = %e, "the process tree had already ended");
118            }
119        }
120    }
121}
122
123/// The program and arguments to start: a Windows `.cmd` tool through `cmd /C`.
124fn wrapped(spec: &ProcessSpec) -> (&str, Vec<&str>) {
125    let args = spec.args.iter().map(String::as_str);
126    if cfg!(windows) && shim::SHIMMED.contains(&spec.program.as_str()) {
127        return ("cmd", ["/C", spec.program.as_str()].into_iter().chain(args).collect());
128    }
129    (spec.program.as_str(), args.collect())
130}
131
132#[cfg(unix)]
133fn group(cmd: &mut CommandWrap) {
134    cmd.wrap(process_wrap::tokio::ProcessGroup::leader());
135}
136
137#[cfg(windows)]
138fn group(cmd: &mut CommandWrap) {
139    use windows::Win32::System::Threading::CREATE_NO_WINDOW;
140    // Headless: a status poll would otherwise flash a console window every few seconds.
141    cmd.wrap(process_wrap::tokio::CreationFlags(CREATE_NO_WINDOW));
142    cmd.wrap(process_wrap::tokio::JobObject);
143}
144
145/// The error of a tool that couldn't start: missing, or the OS's reason.
146fn spawned(program: &str, e: std::io::Error) -> ProcessError {
147    match e.kind() {
148        std::io::ErrorKind::NotFound => ProcessError::Missing { program: program.to_owned() },
149        _ => ProcessError::Io {
150            program: program.to_owned(),
151            source: e,
152        },
153    }
154}
155
156#[cfg(test)]
157#[path = "system_tests.rs"]
158mod tests;