rightkit_child_process/
run.rs1use crate::{CapturedOutput, OutputCapture, OwnedCommand};
11use std::{
12 ffi::OsStr,
13 io,
14 process::{ExitStatus, Stdio},
15 time::Duration,
16};
17
18const DRAIN_WAIT: Duration = Duration::from_secs(2);
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub struct RunOptions {
25 pub timeout: Duration,
27 pub stdout_cap: usize,
29 pub stderr_cap: usize,
31 pub no_window: bool,
33}
34
35impl RunOptions {
36 pub fn new(timeout: Duration) -> Self {
38 Self {
39 timeout,
40 stdout_cap: 1024 * 1024,
41 stderr_cap: 1024 * 1024,
42 no_window: true,
43 }
44 }
45
46 pub fn stdout_cap(mut self, bytes: usize) -> Self {
47 self.stdout_cap = bytes;
48 self
49 }
50
51 pub fn stderr_cap(mut self, bytes: usize) -> Self {
52 self.stderr_cap = bytes;
53 self
54 }
55
56 pub fn no_window(mut self, hidden: bool) -> Self {
57 self.no_window = hidden;
58 self
59 }
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct RunOutput {
65 pub status: ExitStatus,
68 pub timed_out: bool,
70 pub stdout: CapturedOutput,
71 pub stderr: CapturedOutput,
72}
73
74pub fn run<I, S>(program: impl AsRef<OsStr>, args: I, options: &RunOptions) -> io::Result<RunOutput>
78where
79 I: IntoIterator<Item = S>,
80 S: AsRef<OsStr>,
81{
82 let mut command = OwnedCommand::new(program);
83 command
84 .command_mut()
85 .args(args)
86 .stdin(Stdio::null())
87 .stdout(Stdio::piped())
88 .stderr(Stdio::piped());
89 if options.no_window {
90 command.windows_hide();
91 } else {
92 command.windows_visible();
93 }
94 let mut child = command.spawn()?;
95 let mut stdout = child
96 .take_stdout()
97 .map(|pipe| OutputCapture::capture(pipe, options.stdout_cap));
98 let mut stderr = child
99 .take_stderr()
100 .map(|pipe| OutputCapture::capture(pipe, options.stderr_cap));
101 let (status, timed_out) = match child.wait_then_kill(options.timeout)? {
104 crate::WaitOutcome::Exited(status) => (status, false),
105 crate::WaitOutcome::Terminated(status) => (status, true),
106 };
107 let stdout = stdout
108 .as_mut()
109 .map_or_else(CapturedOutput::default, |capture| {
110 capture.finish_within(DRAIN_WAIT)
111 });
112 let stderr = stderr
113 .as_mut()
114 .map_or_else(CapturedOutput::default, |capture| {
115 capture.finish_within(DRAIN_WAIT)
116 });
117 Ok(RunOutput {
118 status,
119 timed_out,
120 stdout,
121 stderr,
122 })
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn defaults_hide_the_window_and_cap_both_streams() {
131 let options = RunOptions::new(Duration::from_secs(1));
132 assert!(options.no_window);
133 assert_eq!(options.stdout_cap, 1024 * 1024);
134 assert_eq!(options.stderr_cap, 1024 * 1024);
135 let options = options.stdout_cap(4).stderr_cap(8).no_window(false);
136 assert_eq!((options.stdout_cap, options.stderr_cap), (4, 8));
137 assert!(!options.no_window);
138 }
139
140 #[cfg(unix)]
141 #[test]
142 fn a_quick_program_returns_its_output_and_status() {
143 let options = RunOptions::new(Duration::from_secs(10));
144 let output = run("sh", ["-c", "printf hi; exit 3"], &options).unwrap();
145 assert!(!output.timed_out);
146 assert_eq!(output.status.code(), Some(3));
147 assert_eq!(output.stdout.bytes, b"hi".to_vec());
148 }
149
150 #[cfg(unix)]
151 #[test]
152 fn a_program_running_within_the_deadline_is_not_signalled() {
153 let options = RunOptions::new(Duration::from_secs(10));
154 let output = run("sh", ["-c", "sleep 1; printf done"], &options).unwrap();
155 assert!(!output.timed_out);
156 assert!(output.status.success());
157 assert_eq!(output.stdout.bytes, b"done".to_vec());
158 }
159
160 #[cfg(unix)]
161 #[test]
162 fn a_slow_program_is_terminated_at_the_deadline() {
163 let options = RunOptions::new(Duration::from_millis(200));
164 let output = run("sh", ["-c", "sleep 30"], &options).unwrap();
165 assert!(output.timed_out);
166 }
167
168 #[cfg(unix)]
169 #[test]
170 fn output_past_the_cap_is_truncated_not_blocking() {
171 let options = RunOptions::new(Duration::from_secs(10)).stdout_cap(4);
172 let output = run("sh", ["-c", "head -c 100000 /dev/zero"], &options).unwrap();
173 assert_eq!(output.stdout.bytes.len(), 4);
174 assert!(output.stdout.truncated);
175 }
176}