1use std::io::{self, Read, Write};
9use std::path::{Path, PathBuf};
10use std::process::{Command as StdCommand, ExitStatus, Stdio};
11use std::sync::Arc;
12use std::sync::mpsc;
13use std::thread;
14use std::time::Duration;
15
16use crate::{Command, TaskPolicy};
17
18#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct ProcessSpec {
21 program: Arc<str>,
22 args: Vec<Arc<str>>,
23 cwd: Option<PathBuf>,
24 env: Vec<(Arc<str>, Arc<str>)>,
25 stdin: Option<Vec<u8>>,
26}
27
28impl ProcessSpec {
29 pub fn new(program: impl Into<Arc<str>>) -> Self {
31 Self {
32 program: program.into(),
33 args: Vec::new(),
34 cwd: None,
35 env: Vec::new(),
36 stdin: None,
37 }
38 }
39
40 #[must_use]
42 pub fn arg(mut self, arg: impl Into<Arc<str>>) -> Self {
43 self.args.push(arg.into());
44 self
45 }
46
47 #[must_use]
49 pub fn args<I, S>(mut self, args: I) -> Self
50 where
51 I: IntoIterator<Item = S>,
52 S: Into<Arc<str>>,
53 {
54 self.args.extend(args.into_iter().map(Into::into));
55 self
56 }
57
58 #[must_use]
60 pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
61 self.cwd = Some(cwd.into());
62 self
63 }
64
65 #[must_use]
67 pub fn env(mut self, key: impl Into<Arc<str>>, value: impl Into<Arc<str>>) -> Self {
68 self.env.push((key.into(), value.into()));
69 self
70 }
71
72 #[must_use]
74 pub fn stdin(mut self, stdin: impl Into<Vec<u8>>) -> Self {
75 self.stdin = Some(stdin.into());
76 self
77 }
78
79 pub fn program(&self) -> &str {
81 &self.program
82 }
83
84 pub fn args_slice(&self) -> &[Arc<str>] {
86 &self.args
87 }
88
89 pub fn cwd_path(&self) -> Option<&Path> {
91 self.cwd.as_deref()
92 }
93
94 pub fn env_slice(&self) -> &[(Arc<str>, Arc<str>)] {
96 &self.env
97 }
98
99 pub fn stdin_bytes(&self) -> Option<&[u8]> {
101 self.stdin.as_deref()
102 }
103
104 pub fn stream(self, emit: impl FnMut(ProcessEvent)) -> io::Result<()> {
109 stream_process(self, emit)
110 }
111
112 pub fn command<Msg, F>(self, map: F) -> Command
118 where
119 Msg: Send + 'static,
120 F: Fn(ProcessEvent) -> Msg + Send + 'static,
121 {
122 process_command(self, map)
123 }
124
125 pub fn command_keyed<Msg, F>(
131 self,
132 key: impl Into<Arc<str>>,
133 policy: TaskPolicy,
134 map: F,
135 ) -> Command
136 where
137 Msg: Send + 'static,
138 F: Fn(ProcessEvent) -> Msg + Send + 'static,
139 {
140 process_command_keyed(key, policy, self, map)
141 }
142
143 fn to_std_command(&self) -> StdCommand {
144 let mut command = StdCommand::new(self.program.as_ref());
145 command.args(self.args.iter().map(AsRef::<str>::as_ref));
146 if let Some(cwd) = &self.cwd {
147 command.current_dir(cwd);
148 }
149 for (key, value) in &self.env {
150 command.env(key.as_ref(), value.as_ref());
151 }
152 command.stdin(if self.stdin.is_some() {
153 Stdio::piped()
154 } else {
155 Stdio::null()
156 });
157 command.stdout(Stdio::piped());
158 command.stderr(Stdio::piped());
159 command
160 }
161}
162
163#[derive(Clone, Copy, Debug, PartialEq, Eq)]
165pub struct ProcessExitStatus {
166 code: Option<i32>,
167 success: bool,
168}
169
170impl ProcessExitStatus {
171 pub fn code(self) -> Option<i32> {
173 self.code
174 }
175
176 pub fn success(self) -> bool {
178 self.success
179 }
180}
181
182impl From<ExitStatus> for ProcessExitStatus {
183 fn from(status: ExitStatus) -> Self {
184 Self {
185 code: status.code(),
186 success: status.success(),
187 }
188 }
189}
190
191#[derive(Clone, Debug, PartialEq, Eq)]
193pub enum ProcessEvent {
194 Stdout(Vec<u8>),
196 Stderr(Vec<u8>),
198 Exited(ProcessExitStatus),
200 Error(Arc<str>),
202}
203
204pub fn stream_process(spec: ProcessSpec, mut emit: impl FnMut(ProcessEvent)) -> io::Result<()> {
206 stream_process_until(spec, || false, &mut emit)
207}
208
209pub fn stream_process_until(
214 mut spec: ProcessSpec,
215 should_cancel: impl Fn() -> bool,
216 mut emit: impl FnMut(ProcessEvent),
217) -> io::Result<()> {
218 let mut command = spec.to_std_command();
219 let stdin_bytes = spec.stdin.take();
220 let mut child = command.spawn()?;
221
222 let stdout = child.stdout.take();
223 let stderr = child.stderr.take();
224 let child_stdin = child.stdin.take();
225 let (tx, rx) = mpsc::channel();
226
227 let stdout_thread = stdout.map(|stdout| spawn_reader(stdout, tx.clone(), ProcessEvent::Stdout));
228 let stderr_thread = stderr.map(|stderr| spawn_reader(stderr, tx.clone(), ProcessEvent::Stderr));
229 let stdin_thread = stdin_bytes.and_then(|bytes| {
230 child_stdin.map(|mut stdin| {
231 let tx = tx.clone();
232 thread::spawn(move || {
233 if let Err(err) = stdin.write_all(&bytes) {
234 send_error(&tx, err);
235 }
236 })
237 })
238 });
239
240 let status = loop {
241 while let Ok(event) = rx.try_recv() {
242 emit(event);
243 }
244
245 if should_cancel() {
246 let _ = child.kill();
247 break child.wait();
248 }
249
250 if let Some(status) = child.try_wait()? {
251 break Ok(status);
252 }
253
254 match rx.recv_timeout(Duration::from_millis(20)) {
255 Ok(event) => emit(event),
256 Err(mpsc::RecvTimeoutError::Timeout) => {}
257 Err(mpsc::RecvTimeoutError::Disconnected) => {
258 if let Some(status) = child.try_wait()? {
259 break Ok(status);
260 }
261 }
262 }
263 };
264
265 join_optional(stdin_thread);
266 join_optional(stdout_thread);
267 join_optional(stderr_thread);
268
269 for event in rx.try_iter() {
270 emit(event);
271 }
272
273 emit(ProcessEvent::Exited(status?.into()));
274 Ok(())
275}
276
277pub fn process_command<Msg, F>(spec: ProcessSpec, map: F) -> Command
279where
280 Msg: Send + 'static,
281 F: Fn(ProcessEvent) -> Msg + Send + 'static,
282{
283 Command::spawn::<Msg, _>(move |link| {
284 if let Err(err) = stream_process_until(
285 spec,
286 || link.is_cancelled(),
287 |event| {
288 let _ = link.send_if_not_cancelled(map(event));
289 },
290 ) {
291 let _ =
292 link.send_if_not_cancelled(map(ProcessEvent::Error(Arc::from(err.to_string()))));
293 }
294 })
295}
296
297pub fn process_command_keyed<Msg, F>(
299 key: impl Into<Arc<str>>,
300 policy: TaskPolicy,
301 spec: ProcessSpec,
302 map: F,
303) -> Command
304where
305 Msg: Send + 'static,
306 F: Fn(ProcessEvent) -> Msg + Send + 'static,
307{
308 Command::spawn_keyed::<Msg, _>(key, policy, move |link| {
309 if let Err(err) = stream_process_until(
310 spec,
311 || link.is_cancelled(),
312 |event| {
313 let _ = link.send_if_not_cancelled(map(event));
314 },
315 ) {
316 let _ =
317 link.send_if_not_cancelled(map(ProcessEvent::Error(Arc::from(err.to_string()))));
318 }
319 })
320}
321
322fn spawn_reader<R>(
323 mut reader: R,
324 tx: mpsc::Sender<ProcessEvent>,
325 wrap: fn(Vec<u8>) -> ProcessEvent,
326) -> thread::JoinHandle<()>
327where
328 R: Read + Send + 'static,
329{
330 thread::spawn(move || {
331 let mut buf = [0_u8; 8192];
332 loop {
333 match reader.read(&mut buf) {
334 Ok(0) => break,
335 Ok(n) => {
336 if tx.send(wrap(buf[..n].to_vec())).is_err() {
337 break;
338 }
339 }
340 Err(err) => {
341 send_error(&tx, err);
342 break;
343 }
344 }
345 }
346 })
347}
348
349fn join_optional(handle: Option<thread::JoinHandle<()>>) {
350 if let Some(handle) = handle {
351 let _ = handle.join();
352 }
353}
354
355fn send_error(tx: &mpsc::Sender<ProcessEvent>, err: io::Error) {
356 let _ = tx.send(ProcessEvent::Error(Arc::from(err.to_string())));
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn process_spec_builder_sets_fields() {
365 let spec = ProcessSpec::new("sh")
366 .arg("-c")
367 .args(["printf", "ok"])
368 .cwd("/")
369 .env("KEY", "VALUE")
370 .stdin(b"input".to_vec());
371
372 assert_eq!(spec.program(), "sh");
373 assert_eq!(spec.args_slice().len(), 3);
374 assert_eq!(spec.args_slice()[0].as_ref(), "-c");
375 assert_eq!(spec.cwd_path(), Some(Path::new("/")));
376 assert_eq!(spec.env_slice()[0].0.as_ref(), "KEY");
377 assert_eq!(spec.env_slice()[0].1.as_ref(), "VALUE");
378 assert_eq!(spec.stdin_bytes(), Some(b"input".as_slice()));
379 }
380
381 #[cfg(unix)]
382 #[test]
383 fn streams_stdout_stderr_and_exit() {
384 let spec = ProcessSpec::new("sh").args(["-c", "printf out; printf err >&2"]);
385 let mut events = Vec::new();
386
387 spec.stream(|event| events.push(event)).unwrap();
388
389 let stdout: Vec<u8> = events
390 .iter()
391 .filter_map(|event| match event {
392 ProcessEvent::Stdout(bytes) => Some(bytes.as_slice()),
393 _ => None,
394 })
395 .flatten()
396 .copied()
397 .collect();
398 let stderr: Vec<u8> = events
399 .iter()
400 .filter_map(|event| match event {
401 ProcessEvent::Stderr(bytes) => Some(bytes.as_slice()),
402 _ => None,
403 })
404 .flatten()
405 .copied()
406 .collect();
407
408 assert_eq!(stdout, b"out");
409 assert_eq!(stderr, b"err");
410 assert!(matches!(events.last(), Some(ProcessEvent::Exited(status)) if status.success()));
411 }
412
413 #[cfg(unix)]
414 #[test]
415 fn stream_process_until_kills_child_when_cancelled() {
416 let spec = ProcessSpec::new("sh").args(["-c", "sleep 5"]);
417 let mut events = Vec::new();
418
419 stream_process_until(spec, || true, |event| events.push(event)).unwrap();
420
421 assert!(matches!(events.last(), Some(ProcessEvent::Exited(status)) if !status.success()));
422 }
423}