1use anyhow::{Context, Result, bail, ensure};
2use std::io::{self, Read, Write};
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::thread::{self, JoinHandle};
6use std::time::Duration;
7
8pub const DEFAULT_TIMEOUT: Duration = Duration::from_mins(1);
9pub const DEFAULT_OUTPUT_LIMIT: usize = 8 * 1024 * 1024;
10
11pub fn filter(argv: &[String], stdin: &str, env: &[(&str, &str)]) -> Result<String> {
17 filter_limited(argv, stdin, env, DEFAULT_TIMEOUT, DEFAULT_OUTPUT_LIMIT)
18}
19
20pub fn run_limited(argv: &[String], env: &[(&str, &str)], timeout: Duration) -> Result<String> {
22 filter_limited(argv, "", env, timeout, DEFAULT_OUTPUT_LIMIT)
23}
24
25const STDERR_TAIL: usize = 64 * 1024;
27
28pub fn run_inherit(argv: &[String], timeout: Duration) -> Result<()> {
32 ensure!(!argv.is_empty(), "driver argv is empty");
33 let (stderr_r, stderr_w) = os_pipe::pipe().context("command stderr pipe")?;
34 let handle = duct::cmd(&argv[0], &argv[1..])
35 .stderr_file(stderr_w)
36 .unchecked()
37 .start()
38 .context("spawn command")?;
39 let tail = thread::spawn(move || stream_tail(stderr_r));
40 let waited = match handle.wait_timeout(timeout) {
41 Ok(output) => output.is_some(),
42 Err(error) => {
43 let _ = handle.kill();
44 let _ = handle.wait();
45 let _ = tail.join();
46 return Err(error).context("command");
47 }
48 };
49 if !waited {
50 let _ = handle.kill();
51 let _ = handle.wait();
52 let _ = tail.join();
53 bail!("command timed out after {timeout:?}");
54 }
55 let stderr = tail
56 .join()
57 .map_err(|_| anyhow::anyhow!("stderr reader panicked"))?;
58 let status = handle
59 .try_wait()
60 .context("command")?
61 .context("command exited without a status")?
62 .status;
63 if !status.success() {
64 bail!(
65 "command failed: {}: {}",
66 argv.join(" "),
67 String::from_utf8_lossy(&stderr).trim()
68 );
69 }
70 Ok(())
71}
72
73fn stream_tail(mut reader: impl Read) -> Vec<u8> {
76 let mut tail = Vec::new();
77 let mut chunk = [0_u8; 8192];
78 let mut stderr = io::stderr();
79 loop {
80 match reader.read(&mut chunk) {
81 Ok(0) | Err(_) => return tail,
82 Ok(read) => {
83 let _ = stderr.write_all(&chunk[..read]);
84 tail.extend_from_slice(&chunk[..read]);
85 if tail.len() > STDERR_TAIL {
86 tail.drain(..tail.len() - STDERR_TAIL);
87 }
88 }
89 }
90 }
91}
92
93#[must_use]
95pub fn argv(parts: &[&str]) -> Vec<String> {
96 parts.iter().map(|part| (*part).to_owned()).collect()
97}
98
99pub fn filter_limited(
100 argv: &[String],
101 stdin: &str,
102 env: &[(&str, &str)],
103 timeout: Duration,
104 output_limit: usize,
105) -> Result<String> {
106 ensure!(!argv.is_empty(), "driver argv is empty");
107 let mut expr = duct::cmd(&argv[0], &argv[1..]);
108 for (key, value) in env {
109 expr = expr.env(key, value);
110 }
111 let (stdout_r, stdout_w) = os_pipe::pipe().context("driver stdout pipe")?;
112 let (stderr_r, stderr_w) = os_pipe::pipe().context("driver stderr pipe")?;
113 let handle = Arc::new(
114 expr.stdin_bytes(stdin.as_bytes().to_vec())
115 .stdout_file(stdout_w)
116 .stderr_file(stderr_w)
117 .unchecked()
118 .start()
119 .context("spawn driver command")?,
120 );
121 let mut guard = ChildGuard {
122 handle: Arc::clone(&handle),
123 reaped: false,
124 };
125 let exceeded = Arc::new(AtomicBool::new(false));
126 let stdout_h = spawn_capped(
127 stdout_r,
128 output_limit,
129 Arc::clone(&handle),
130 Arc::clone(&exceeded),
131 );
132 let stderr_h = spawn_capped(
133 stderr_r,
134 output_limit,
135 Arc::clone(&handle),
136 Arc::clone(&exceeded),
137 );
138
139 let waited = match handle.wait_timeout(timeout) {
140 Ok(output) => output.is_some(),
141 Err(error) => {
142 let _ = handle.kill();
143 let _ = handle.wait();
144 let _ = take_capped(stdout_h);
145 let _ = take_capped(stderr_h);
146 guard.reaped = true;
147 return Err(error).context("driver command");
148 }
149 };
150 if !waited {
151 let _ = handle.kill();
152 let _ = handle.wait();
153 let _ = take_capped(stdout_h);
154 let _ = take_capped(stderr_h);
155 guard.reaped = true;
156 bail!("driver command timed out after {timeout:?}");
157 }
158
159 let stdout = take_capped(stdout_h)?;
160 let stderr = take_capped(stderr_h)?;
161 guard.reaped = true;
162
163 if exceeded.load(Ordering::SeqCst) {
164 bail!("driver command output exceeded {output_limit} bytes");
165 }
166 let status = handle
167 .try_wait()
168 .context("driver command")?
169 .context("driver exited without a status")?
170 .status;
171 if !status.success() {
172 let command = argv.join(" ");
173 let stderr = String::from_utf8_lossy(&stderr);
174 let stderr = stderr.trim();
175 if stderr.is_empty() {
176 bail!("driver command failed: {command}: {status}");
177 }
178 bail!("driver command failed: {command}: {status}: {stderr}");
179 }
180 String::from_utf8(stdout).context("driver stdout is not UTF-8")
181}
182
183struct ChildGuard {
184 handle: Arc<duct::Handle>,
185 reaped: bool,
186}
187
188impl Drop for ChildGuard {
189 fn drop(&mut self) {
190 if !self.reaped {
191 let _ = self.handle.kill();
192 let _ = self.handle.try_wait();
193 }
194 }
195}
196
197enum CapRead {
198 Bytes(Vec<u8>),
199 Exceeded,
200}
201
202fn spawn_capped(
203 reader: os_pipe::PipeReader,
204 limit: usize,
205 handle: Arc<duct::Handle>,
206 exceeded: Arc<AtomicBool>,
207) -> JoinHandle<io::Result<CapRead>> {
208 thread::spawn(move || {
209 let result = read_capped(reader, limit);
210 if matches!(result, Ok(CapRead::Exceeded)) {
211 exceeded.store(true, Ordering::SeqCst);
212 let _ = handle.kill();
213 }
214 result
215 })
216}
217
218fn read_capped(mut reader: os_pipe::PipeReader, limit: usize) -> io::Result<CapRead> {
219 let mut buf = Vec::new();
220 let mut chunk = [0_u8; 8192];
221 loop {
222 let n = reader.read(&mut chunk)?;
223 if n == 0 {
224 return Ok(CapRead::Bytes(buf));
225 }
226 if buf.len().saturating_add(n) > limit {
227 return Ok(CapRead::Exceeded);
228 }
229 buf.extend_from_slice(&chunk[..n]);
230 }
231}
232
233fn take_capped(handle: JoinHandle<io::Result<CapRead>>) -> Result<Vec<u8>> {
234 match handle.join() {
235 Ok(Ok(CapRead::Bytes(bytes))) => Ok(bytes),
236 Ok(Ok(CapRead::Exceeded)) => bail!("driver command output exceeded the cap"),
237 Ok(Err(error)) => Err(error).context("read driver output"),
238 Err(_) => bail!("driver output reader panicked"),
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::{filter, filter_limited, run_inherit};
245 use std::time::{Duration, Instant};
246
247 #[test]
248 fn a_failed_command_reports_its_stderr() {
249 let error = run_inherit(
250 &[
251 "sh".into(),
252 "-c".into(),
253 "echo 'crate x@0.0.1 already exists on crates.io index' >&2; exit 1".into(),
254 ],
255 Duration::from_secs(5),
256 )
257 .expect_err("failure");
258 let message = format!("{error:#}");
259 assert!(
260 message.contains("already exists on crates.io index"),
261 "{message}"
262 );
263 }
264
265 #[test]
266 fn filter_returns_stdout() {
267 let out = filter(&["tr".into(), "-d".into(), "\n".into()], "1.2.3\n", &[]).expect("filter");
268 assert_eq!(out, "1.2.3");
269 }
270
271 #[test]
272 fn filter_times_out_and_kills() {
273 let start = Instant::now();
274 let error = filter_limited(
275 &["sleep".into(), "30".into()],
276 "",
277 &[],
278 Duration::from_millis(200),
279 1024,
280 )
281 .expect_err("timeout");
282 let message = format!("{error:#}");
283 assert!(message.contains("timed out"), "{message}");
284 assert!(
285 start.elapsed() < Duration::from_secs(5),
286 "kill did not return promptly: {:?}",
287 start.elapsed()
288 );
289 }
290
291 #[test]
292 fn filter_rejects_oversized_stdout_without_buffering_it() {
293 let start = Instant::now();
294 let error = filter_limited(
295 &[
296 "sh".into(),
297 "-c".into(),
298 format!("while :; do echo {}; done", "x".repeat(64)),
299 ],
300 "",
301 &[],
302 Duration::from_secs(10),
303 4096,
304 )
305 .expect_err("limit");
306 let message = format!("{error:#}");
307 assert!(message.contains("exceeded"), "{message}");
308 assert!(
309 start.elapsed() < Duration::from_secs(3),
310 "cap waited for the firehose instead of killing: {:?}",
311 start.elapsed()
312 );
313 }
314
315 #[test]
316 fn filter_reports_nonzero_status() {
317 let error = filter(&["false".into()], "", &[]).expect_err("fail");
318 assert_eq!(
319 format!("{error:#}"),
320 "driver command failed: false: exit status: 1"
321 );
322 }
323
324 #[test]
325 fn filter_reports_the_stderr_of_a_failed_command() {
326 let error = filter(
327 &["sh".into(), "-c".into(), "echo boom >&2; exit 3".into()],
328 "",
329 &[],
330 )
331 .expect_err("fail");
332 assert_eq!(
333 format!("{error:#}"),
334 "driver command failed: sh -c echo boom >&2; exit 3: exit status: 3: boom"
335 );
336 }
337}