isothrum_runner/
process.rs1mod elevated;
7#[cfg(any(test, feature = "fake"))]
8pub mod fake;
9mod lines;
10mod shim;
11mod system;
12mod tail;
13
14use std::path::PathBuf;
15
16pub(crate) use elevated::Elevated;
17pub use system::SystemProcesses;
18pub(crate) use tail::Tail;
19
20use crate::BoxFuture;
21use crate::events::Stream;
22
23#[derive(Debug, Clone, Default, PartialEq, Eq)]
25#[non_exhaustive]
26pub struct ProcessSpec {
27 pub program: String,
29 pub args: Vec<String>,
31 pub cwd: Option<PathBuf>,
33 pub env: Vec<(String, String)>,
35 pub stdin: Option<PathBuf>,
37}
38
39impl ProcessSpec {
40 pub fn new(program: &str, args: impl IntoIterator<Item = impl Into<String>>) -> ProcessSpec {
42 ProcessSpec {
43 program: program.to_owned(),
44 args: args.into_iter().map(Into::into).collect(),
45 ..ProcessSpec::default()
46 }
47 }
48
49 #[must_use]
51 pub fn line(&self) -> String {
52 command_line(&self.program, &self.args)
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58#[non_exhaustive]
59pub struct Exit {
60 pub code: Option<i32>,
62}
63
64impl Exit {
65 #[must_use]
67 pub fn new(code: Option<i32>) -> Exit {
68 Exit { code }
69 }
70
71 #[must_use]
73 pub fn success(self) -> bool {
74 self.code == Some(0)
75 }
76}
77
78#[derive(Debug, thiserror::Error)]
80#[non_exhaustive]
81pub enum ProcessError {
82 #[error("`{program}` isn't installed or isn't on PATH")]
84 Missing {
85 program: String,
87 },
88 #[error("refusing to pass `{arg}` to {program}: it holds a character cmd.exe would interpret")]
90 Refused {
91 program: String,
93 arg: String,
95 },
96 #[error("elevation: {reason}")]
99 Elevation {
100 reason: String,
102 },
103 #[error("`{program}`: {source}")]
105 Io {
106 program: String,
108 source: std::io::Error,
110 },
111}
112
113pub type OnLine<'a> = &'a mut (dyn FnMut(Stream, &str) + Send);
115
116pub trait ProcessRunner: Send + Sync {
119 fn run<'a>(&'a self, spec: &'a ProcessSpec, on_line: OnLine<'a>) -> BoxFuture<'a, Result<Exit, ProcessError>>;
121}
122
123fn command_line(program: &str, args: &[String]) -> String {
125 std::iter::once(program).chain(args.iter().map(String::as_str)).collect::<Vec<_>>().join(" ")
126}
127
128#[derive(Debug, Clone, Default, PartialEq, Eq)]
130#[non_exhaustive]
131pub struct Captured {
132 pub exit: Option<Exit>,
134 pub stdout: Vec<String>,
136 pub stderr: Vec<String>,
138}
139
140pub async fn capture(runner: &dyn ProcessRunner, spec: &ProcessSpec) -> Result<Captured, ProcessError> {
146 let mut stdout = Vec::new();
147 let mut tail = Tail::default();
148 let mut keep = |stream: Stream, line: &str| match stream {
149 Stream::Stdout => stdout.push(line.to_owned()),
150 Stream::Stderr => tail.push(line),
151 };
152 let exit = runner.run(spec, &mut keep).await?;
153 Ok(Captured {
154 exit: Some(exit),
155 stdout,
156 stderr: tail.into_lines(),
157 })
158}
159
160impl From<Option<i32>> for Exit {
161 fn from(code: Option<i32>) -> Exit {
162 Exit { code }
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn a_spec_reads_as_its_command_line() {
172 let spec = ProcessSpec::new("docker", ["compose", "ps"]);
173 assert_eq!(spec.line(), "docker compose ps");
174 assert!(Exit { code: Some(0) }.success() && !Exit { code: None }.success());
175 }
176}