1use std::collections::BTreeSet;
17use std::io::{self, Read, Write};
18use std::process::{Child, Command, ExitStatus, Stdio};
19use std::time::{Duration, Instant};
20
21pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(300);
23pub const DEFAULT_MAX_OUTPUT: usize = 64 * 1024 * 1024;
25
26static SECRET_ENV: std::sync::Mutex<Option<BTreeSet<String>>> = std::sync::Mutex::new(None);
27
28pub fn deny_env_var(name: &str) {
33 if name.trim().is_empty() {
34 return;
35 }
36 let mut guard = SECRET_ENV.lock().unwrap_or_else(|e| e.into_inner());
37 guard.get_or_insert_with(BTreeSet::new).insert(name.to_string());
38}
39
40pub fn secret_env_vars() -> Vec<String> {
42 let guard = SECRET_ENV.lock().unwrap_or_else(|e| e.into_inner());
43 guard.as_ref().map(|s| s.iter().cloned().collect()).unwrap_or_default()
44}
45
46#[derive(Debug)]
48pub struct SpawnOutput {
49 pub status: ExitStatus,
50 pub stdout: Vec<u8>,
51 pub timed_out: bool,
52 pub stdout_truncated: bool,
53}
54
55impl SpawnOutput {
56 pub fn failure(&self, what: &str) -> Option<String> {
58 if self.timed_out {
59 return Some(format!("{what} timed out and was killed"));
60 }
61 if !self.status.success() {
62 return Some(format!("{what} exited with {}", self.status));
63 }
64 None
65 }
66}
67
68pub fn run_argv(argv: &[String], stdin: &str, timeout: Option<Duration>) -> io::Result<SpawnOutput> {
73 let mut cmd = Command::new(&argv[0]);
74 cmd.args(&argv[1..]);
75 for var in secret_env_vars() {
76 cmd.env_remove(var);
77 }
78 cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::inherit());
79
80 let mut child = cmd.spawn()?;
81
82 let payload = stdin.as_bytes().to_vec();
85 let stdin_thread = child.stdin.take().map(|mut pipe| {
86 std::thread::spawn(move || {
87 let _ = pipe.write_all(&payload);
88 })
89 });
90 let out_thread = child.stdout.take().map(|pipe| std::thread::spawn(move || drain(pipe)));
91
92 let (status, timed_out) = wait_bounded(&mut child, timeout)?;
93
94 if let Some(t) = stdin_thread {
95 let _ = t.join();
96 }
97 let (stdout, stdout_truncated) =
98 out_thread.and_then(|t| t.join().ok()).unwrap_or((Vec::new(), false));
99
100 Ok(SpawnOutput { status, stdout, timed_out, stdout_truncated })
101}
102
103fn drain<R: Read>(mut src: R) -> (Vec<u8>, bool) {
106 let mut kept = Vec::new();
107 let mut buf = [0u8; 16 * 1024];
108 let mut truncated = false;
109 loop {
110 match src.read(&mut buf) {
111 Ok(0) => break,
112 Ok(n) => {
113 if kept.len() < DEFAULT_MAX_OUTPUT {
114 let take = (DEFAULT_MAX_OUTPUT - kept.len()).min(n);
115 kept.extend_from_slice(&buf[..take]);
116 if take < n {
117 truncated = true;
118 }
119 } else {
120 truncated = true;
121 }
122 }
123 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
124 Err(_) => break,
125 }
126 }
127 (kept, truncated)
128}
129
130fn wait_bounded(child: &mut Child, timeout: Option<Duration>) -> io::Result<(ExitStatus, bool)> {
131 let Some(limit) = timeout else {
132 return Ok((child.wait()?, false));
133 };
134 let deadline = Instant::now() + limit;
135 let mut nap = Duration::from_millis(1);
136 loop {
137 if let Some(status) = child.try_wait()? {
138 return Ok((status, false));
139 }
140 if Instant::now() >= deadline {
141 let _ = child.kill();
142 let status = child.wait()?;
143 return Ok((status, true));
144 }
145 std::thread::sleep(nap);
146 nap = (nap * 2).min(Duration::from_millis(50));
147 }
148}