ts_io/
child_command.rs

1//! Easy wrapper to process some data using a child process.
2
3use std::{
4    ffi::OsStr,
5    io::{self, Write},
6    process::{Command, ExitStatus, Stdio},
7    thread,
8};
9
10/// Error variants for using a child command.
11#[derive(Debug)]
12#[non_exhaustive]
13#[allow(missing_docs)]
14pub enum ChildCommandError {
15    #[non_exhaustive]
16    SpawnChild { source: io::Error },
17
18    #[non_exhaustive]
19    WriteToStdin { source: io::Error },
20
21    #[non_exhaustive]
22    ReadOutput { source: io::Error },
23
24    #[non_exhaustive]
25    UnsuccessfulStatus { status: ExitStatus, stderr: String },
26}
27impl core::fmt::Display for ChildCommandError {
28    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
29        match &self {
30            Self::SpawnChild { .. } => write!(f, "could not spawn child process"),
31            Self::WriteToStdin { .. } => write!(f, "writing to child's stdin failed"),
32            Self::ReadOutput { .. } => write!(f, "reading child's output failed"),
33            Self::UnsuccessfulStatus { status, stderr, .. } => write!(
34                f,
35                "child process reported exit code {status:?}, with stderr: {stderr}"
36            ),
37        }
38    }
39}
40impl core::error::Error for ChildCommandError {
41    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
42        match &self {
43            Self::ReadOutput { source, .. }
44            | Self::WriteToStdin { source, .. }
45            | Self::SpawnChild { source, .. } => Some(source),
46            _ => None,
47        }
48    }
49}
50
51/// Write `data` to a child process' `stdin`, and return the process' `stdout`.
52///
53/// ## Panics
54/// * If handle to child's `stdin` could not be taken.
55/// * If the writer thread panics.
56pub fn process_using_child<C: AsRef<OsStr>, I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
57    command: C,
58    args: I,
59    data: &[u8],
60) -> Result<Vec<u8>, ChildCommandError> {
61    let mut child = Command::new(command)
62        .args(args)
63        .stdout(Stdio::piped())
64        .stderr(Stdio::piped())
65        .stdin(Stdio::piped())
66        .spawn()
67        .map_err(|source| ChildCommandError::SpawnChild { source })?;
68
69    let mut stdin = child.stdin.take().expect("stdin handle to be present");
70    let output = thread::scope(|s| {
71        let writer = s.spawn(move || stdin.write_all(data));
72
73        let output = child
74            .wait_with_output()
75            .map_err(|source| ChildCommandError::ReadOutput { source });
76
77        let write_result = writer
78            .join()
79            .expect("writer thread to not panic")
80            .map_err(|source| ChildCommandError::WriteToStdin { source });
81
82        if let Some(error) = write_result.err() {
83            return Err(error);
84        }
85
86        output
87    })?;
88
89    if !output.status.success() {
90        return Err(ChildCommandError::UnsuccessfulStatus {
91            status: output.status,
92            stderr: String::from_utf8_lossy(&output.stderr).to_string(),
93        });
94    }
95
96    Ok(output.stdout)
97}