Skip to main content

smux/
process.rs

1use std::io;
2use std::process::{Command, Stdio};
3use std::sync::{Arc, OnceLock};
4
5#[cfg(test)]
6use std::collections::VecDeque;
7#[cfg(test)]
8use std::sync::Mutex;
9
10#[derive(Debug, Clone, Copy, Eq, PartialEq)]
11pub struct CommandStatus {
12    pub success: bool,
13    pub code: Option<i32>,
14}
15
16#[derive(Debug, Clone, Eq, PartialEq)]
17pub struct CommandOutput {
18    pub status: CommandStatus,
19    pub stdout: Vec<u8>,
20    pub stderr: Vec<u8>,
21}
22
23pub trait CommandRunner: Send + Sync {
24    fn run_capture(&self, program: &str, args: &[String]) -> io::Result<CommandOutput>;
25    fn run_capture_with_input(
26        &self,
27        program: &str,
28        args: &[String],
29        input: &str,
30    ) -> io::Result<CommandOutput>;
31    fn run_inherit(&self, program: &str, args: &[String]) -> io::Result<CommandStatus>;
32}
33
34pub fn default_runner() -> Arc<dyn CommandRunner> {
35    static RUNNER: OnceLock<Arc<dyn CommandRunner>> = OnceLock::new();
36    RUNNER.get_or_init(|| Arc::new(RealCommandRunner)).clone()
37}
38
39#[derive(Debug)]
40struct RealCommandRunner;
41
42impl CommandRunner for RealCommandRunner {
43    fn run_capture(&self, program: &str, args: &[String]) -> io::Result<CommandOutput> {
44        let output = Command::new(program).args(args).output()?;
45        Ok(CommandOutput {
46            status: CommandStatus {
47                success: output.status.success(),
48                code: output.status.code(),
49            },
50            stdout: output.stdout,
51            stderr: output.stderr,
52        })
53    }
54
55    fn run_capture_with_input(
56        &self,
57        program: &str,
58        args: &[String],
59        input: &str,
60    ) -> io::Result<CommandOutput> {
61        use std::io::Write;
62
63        let mut child = Command::new(program)
64            .args(args)
65            .stdin(Stdio::piped())
66            .stdout(Stdio::piped())
67            .stderr(Stdio::piped())
68            .spawn()?;
69
70        if let Some(mut stdin) = child.stdin.take() {
71            stdin.write_all(input.as_bytes())?;
72        }
73
74        let output = child.wait_with_output()?;
75        Ok(CommandOutput {
76            status: CommandStatus {
77                success: output.status.success(),
78                code: output.status.code(),
79            },
80            stdout: output.stdout,
81            stderr: output.stderr,
82        })
83    }
84
85    fn run_inherit(&self, program: &str, args: &[String]) -> io::Result<CommandStatus> {
86        let status = Command::new(program)
87            .args(args)
88            .stdin(Stdio::inherit())
89            .stdout(Stdio::inherit())
90            .stderr(Stdio::inherit())
91            .status()?;
92
93        Ok(CommandStatus {
94            success: status.success(),
95            code: status.code(),
96        })
97    }
98}
99
100#[cfg(test)]
101#[derive(Debug, Clone, Copy, Eq, PartialEq)]
102pub enum IoMode {
103    Capture,
104    Inherit,
105}
106
107#[cfg(test)]
108#[derive(Debug, Clone, Eq, PartialEq)]
109pub struct RecordedCommand {
110    pub program: String,
111    pub args: Vec<String>,
112    pub stdin: Option<String>,
113    pub io_mode: IoMode,
114}
115
116#[cfg(test)]
117#[derive(Debug)]
118enum PlannedResponse {
119    Capture(io::Result<CommandOutput>),
120    Inherit(io::Result<CommandStatus>),
121}
122
123#[cfg(test)]
124#[derive(Debug)]
125pub struct FakeCommandRunner {
126    planned: Mutex<VecDeque<PlannedResponse>>,
127    recorded: Mutex<Vec<RecordedCommand>>,
128}
129
130#[cfg(test)]
131impl FakeCommandRunner {
132    pub fn new() -> Self {
133        Self {
134            planned: Mutex::new(VecDeque::new()),
135            recorded: Mutex::new(Vec::new()),
136        }
137    }
138
139    pub fn push_capture(&self, result: io::Result<CommandOutput>) {
140        self.planned
141            .lock()
142            .expect("planned queue should lock")
143            .push_back(PlannedResponse::Capture(result));
144    }
145
146    pub fn push_inherit(&self, result: io::Result<CommandStatus>) {
147        self.planned
148            .lock()
149            .expect("planned queue should lock")
150            .push_back(PlannedResponse::Inherit(result));
151    }
152
153    pub fn recorded(&self) -> Vec<RecordedCommand> {
154        self.recorded
155            .lock()
156            .expect("recorded queue should lock")
157            .clone()
158    }
159}
160
161#[cfg(test)]
162impl Default for FakeCommandRunner {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168#[cfg(test)]
169impl CommandRunner for FakeCommandRunner {
170    fn run_capture(&self, program: &str, args: &[String]) -> io::Result<CommandOutput> {
171        self.recorded
172            .lock()
173            .expect("recorded queue should lock")
174            .push(RecordedCommand {
175                program: program.to_owned(),
176                args: args.to_vec(),
177                stdin: None,
178                io_mode: IoMode::Capture,
179            });
180
181        match self
182            .planned
183            .lock()
184            .expect("planned queue should lock")
185            .pop_front()
186            .expect("missing planned capture response")
187        {
188            PlannedResponse::Capture(result) => result,
189            PlannedResponse::Inherit(_) => {
190                panic!("expected capture response but inherit response was queued")
191            }
192        }
193    }
194
195    fn run_capture_with_input(
196        &self,
197        program: &str,
198        args: &[String],
199        input: &str,
200    ) -> io::Result<CommandOutput> {
201        self.recorded
202            .lock()
203            .expect("recorded queue should lock")
204            .push(RecordedCommand {
205                program: program.to_owned(),
206                args: args.to_vec(),
207                stdin: Some(input.to_owned()),
208                io_mode: IoMode::Capture,
209            });
210
211        match self
212            .planned
213            .lock()
214            .expect("planned queue should lock")
215            .pop_front()
216            .expect("missing planned capture-with-input response")
217        {
218            PlannedResponse::Capture(result) => result,
219            PlannedResponse::Inherit(_) => {
220                panic!("expected capture response but inherit response was queued")
221            }
222        }
223    }
224
225    fn run_inherit(&self, program: &str, args: &[String]) -> io::Result<CommandStatus> {
226        self.recorded
227            .lock()
228            .expect("recorded queue should lock")
229            .push(RecordedCommand {
230                program: program.to_owned(),
231                args: args.to_vec(),
232                stdin: None,
233                io_mode: IoMode::Inherit,
234            });
235
236        match self
237            .planned
238            .lock()
239            .expect("planned queue should lock")
240            .pop_front()
241            .expect("missing planned inherit response")
242        {
243            PlannedResponse::Inherit(result) => result,
244            PlannedResponse::Capture(_) => {
245                panic!("expected inherit response but capture response was queued")
246            }
247        }
248    }
249}