1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
//! Running the tools a manifest names: the [`ProcessRunner`] port. [`SystemProcesses`] starts
//! real processes (never through a shell, headless on Windows, each tool in a process group or
//! job object so a timeout or a cancellation ends the whole tree it started); the `fake` feature's
//! `fake::FakeProcesses` answers from a test's script instead.
mod elevated;
#[cfg(any(test, feature = "fake"))]
pub mod fake;
mod lines;
mod shim;
mod system;
mod tail;
use std::path::PathBuf;
pub(crate) use elevated::Elevated;
pub use system::SystemProcesses;
pub(crate) use tail::Tail;
use crate::BoxFuture;
use crate::events::Stream;
/// One process to start.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct ProcessSpec {
/// The program: a bare tool name, found on PATH.
pub program: String,
/// Its arguments, passed as they are (no shell ever reads them).
pub args: Vec<String>,
/// Its working folder (default: the runner's).
pub cwd: Option<PathBuf>,
/// Variables added to the runner's environment.
pub env: Vec<(String, String)>,
/// A file piped into its standard input (default: none).
pub stdin: Option<PathBuf>,
}
impl ProcessSpec {
/// `program` with `args`, everything else at its default.
pub fn new(program: &str, args: impl IntoIterator<Item = impl Into<String>>) -> ProcessSpec {
ProcessSpec {
program: program.to_owned(),
args: args.into_iter().map(Into::into).collect(),
..ProcessSpec::default()
}
}
/// The command as one line (`docker compose ps`), for messages and tests.
#[must_use]
pub fn line(&self) -> String {
command_line(&self.program, &self.args)
}
}
/// How a process ended.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Exit {
/// Its exit code, when it exited (not killed by a signal).
pub code: Option<i32>,
}
impl Exit {
/// A process that ended with `code` (`None`: killed by a signal).
#[must_use]
pub fn new(code: Option<i32>) -> Exit {
Exit { code }
}
/// Whether it exited with 0.
#[must_use]
pub fn success(self) -> bool {
self.code == Some(0)
}
}
/// Why a process couldn't run to its end.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ProcessError {
/// The program isn't on PATH.
#[error("`{program}` isn't installed or isn't on PATH")]
Missing {
/// The program.
program: String,
},
/// An argument a Windows `cmd /C` wrapper would read as an operator.
#[error("refusing to pass `{arg}` to {program}: it holds a character cmd.exe would interpret")]
Refused {
/// The program.
program: String,
/// The argument, escaped.
arg: String,
},
/// It needed elevation (Hyper-V's), which couldn't be had: the prompt declined, the
/// elevated session ended.
#[error("elevation: {reason}")]
Elevation {
/// Why.
reason: String,
},
/// Starting it, piping it or waiting for it failed.
#[error("`{program}`: {source}")]
Io {
/// The program.
program: String,
/// The OS's reason.
source: std::io::Error,
},
}
/// Receives each line a process prints, with the output it came from.
pub type OnLine<'a> = &'a mut (dyn FnMut(Stream, &str) + Send);
/// Starts processes. Dropping the future of [`ProcessRunner::run`] before it resolves (a timeout,
/// a cancelled operation) ends the process and everything it started.
pub trait ProcessRunner: Send + Sync {
/// Runs `spec` to its end, handing each line it prints to `on_line` (decoded lossily).
fn run<'a>(&'a self, spec: &'a ProcessSpec, on_line: OnLine<'a>) -> BoxFuture<'a, Result<Exit, ProcessError>>;
}
/// `program` and `args` as one line, separated by spaces.
fn command_line(program: &str, args: &[String]) -> String {
std::iter::once(program).chain(args.iter().map(String::as_str)).collect::<Vec<_>>().join(" ")
}
/// What a process printed, kept: its standard output, and its last standard error lines.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Captured {
/// How it ended.
pub exit: Option<Exit>,
/// Its standard output, one line each.
pub stdout: Vec<String>,
/// Its last standard error lines (a Ruby backtrace's frames left out).
pub stderr: Vec<String>,
}
/// Runs `spec`, keeping what it prints.
///
/// # Errors
///
/// Fails when the process can't start: missing, refused, or an I/O error.
pub async fn capture(runner: &dyn ProcessRunner, spec: &ProcessSpec) -> Result<Captured, ProcessError> {
let mut stdout = Vec::new();
let mut tail = Tail::default();
let mut keep = |stream: Stream, line: &str| match stream {
Stream::Stdout => stdout.push(line.to_owned()),
Stream::Stderr => tail.push(line),
};
let exit = runner.run(spec, &mut keep).await?;
Ok(Captured {
exit: Some(exit),
stdout,
stderr: tail.into_lines(),
})
}
impl From<Option<i32>> for Exit {
fn from(code: Option<i32>) -> Exit {
Exit { code }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_spec_reads_as_its_command_line() {
let spec = ProcessSpec::new("docker", ["compose", "ps"]);
assert_eq!(spec.line(), "docker compose ps");
assert!(Exit { code: Some(0) }.success() && !Exit { code: None }.success());
}
}