1use std::collections::VecDeque;
21use std::fs::File;
22use std::io::{BufRead, BufReader, Read};
23use std::path::Path;
24use std::process::{Child, Command, Stdio};
25use std::sync::atomic::{AtomicBool, Ordering};
26use std::sync::{Arc, Mutex};
27use std::time::{Duration, Instant};
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum TreeRun {
32 Passed,
34 Failed(String),
37 TimedOut,
39 Cancelled,
41 Spawn(String),
43}
44
45const TAIL: usize = 64;
48
49const POLL: Duration = Duration::from_millis(20);
51
52const GRACE: Duration = Duration::from_millis(500);
54
55#[cfg(unix)]
57const NICENESS: i32 = 10;
58
59#[cfg(unix)]
60extern "C" {
61 #[link_name = "setpriority"]
62 fn libc_setpriority(which: i32, who: u32, prio: i32) -> i32;
63 #[link_name = "kill"]
64 fn libc_kill(pid: i32, sig: i32) -> i32;
65 #[link_name = "pipe"]
66 fn libc_pipe(fds: *mut i32) -> i32;
67}
68
69#[cfg(unix)]
72fn merged_pipe() -> Option<(File, Stdio, Stdio)> {
73 use std::os::unix::io::FromRawFd;
74 let mut fds = [0i32; 2];
75 if unsafe { libc_pipe(fds.as_mut_ptr()) } != 0 {
77 return None;
78 }
79 let (read, write) = unsafe { (File::from_raw_fd(fds[0]), File::from_raw_fd(fds[1])) };
81 let write2 = write.try_clone().ok()?;
82 Some((read, Stdio::from(write), Stdio::from(write2)))
83}
84
85#[cfg(unix)]
86const SIGTERM: i32 = 15;
87#[cfg(unix)]
88const SIGKILL: i32 = 9;
89
90#[cfg(unix)]
92fn signal_group(pgid: u32, sig: i32) -> bool {
93 unsafe { libc_kill(-(pgid as i32), sig) == 0 }
95}
96
97#[cfg(unix)]
99pub fn group_alive(pgid: u32) -> bool {
100 signal_group(pgid, 0)
101}
102
103fn kill_tree(child: &mut Child) {
104 #[cfg(unix)]
105 {
106 let pgid = child.id();
107 signal_group(pgid, SIGTERM);
108 let until = Instant::now() + GRACE;
109 while Instant::now() < until {
110 let _ = child.try_wait();
113 if !group_alive(pgid) {
114 break;
115 }
116 std::thread::sleep(POLL);
117 }
118 if group_alive(pgid) {
119 signal_group(pgid, SIGKILL);
120 }
121 }
122 #[cfg(not(unix))]
123 {
124 let _ = Command::new("taskkill")
125 .args(["/PID", &child.id().to_string(), "/T", "/F"])
126 .stdin(Stdio::null())
127 .stdout(Stdio::null())
128 .stderr(Stdio::null())
129 .status();
130 }
131 let _ = child.kill();
132 let _ = child.wait();
133}
134
135fn drain(
136 stream: impl Read + Send + 'static,
137 tail: Arc<Mutex<VecDeque<String>>>,
138) -> std::thread::JoinHandle<()> {
139 std::thread::spawn(move || {
140 for line in BufReader::new(stream).lines().map_while(Result::ok) {
141 if let Ok(mut t) = tail.lock() {
142 if t.len() == TAIL {
143 t.pop_front();
144 }
145 t.push_back(line);
146 }
147 }
148 })
149}
150
151fn summary(tail: &Mutex<VecDeque<String>>) -> String {
152 tail.lock()
153 .ok()
154 .and_then(|t| {
155 t.iter()
156 .rev()
157 .map(|l| l.trim())
158 .find(|l| !l.is_empty())
159 .map(str::to_string)
160 })
161 .unwrap_or_else(|| "no output".to_string())
162}
163
164pub fn run(argv: &[String], cwd: &Path, deadline: Instant, cancel: &AtomicBool) -> TreeRun {
166 run_inner(argv, cwd, deadline, cancel, false).0
167}
168
169pub fn run_output(
172 argv: &[String],
173 cwd: &Path,
174 deadline: Instant,
175 cancel: &AtomicBool,
176) -> Result<String, TreeRun> {
177 match run_inner(argv, cwd, deadline, cancel, true) {
178 (TreeRun::Passed, out) => Ok(out),
179 (other, _) => Err(other),
180 }
181}
182
183fn run_inner(
184 argv: &[String],
185 cwd: &Path,
186 deadline: Instant,
187 cancel: &AtomicBool,
188 need_output: bool,
189) -> (TreeRun, String) {
190 let Some((program, rest)) = argv.split_first() else {
191 return (TreeRun::Spawn("empty command".into()), String::new());
192 };
193 let mut cmd = Command::new(program);
194 cmd.args(rest).current_dir(cwd).stdin(Stdio::null());
195 #[cfg(unix)]
196 let merged = merged_pipe();
197 #[cfg(not(unix))]
198 let merged: Option<(File, Stdio, Stdio)> = None;
199 let reader = match merged {
200 Some((read, out, err)) => {
201 cmd.stdout(out).stderr(err);
202 Some(read)
203 }
204 None => {
205 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
206 None
207 }
208 };
209 crate::hooks::common::strip_git_env(&mut cmd);
210 #[cfg(unix)]
211 {
212 use std::os::unix::process::CommandExt;
213 cmd.process_group(0);
214 unsafe {
217 cmd.pre_exec(|| {
218 libc_setpriority(0, 0, NICENESS);
219 Ok(())
220 });
221 }
222 }
223 let mut child = match cmd.spawn() {
224 Ok(c) => c,
225 Err(e) => return (TreeRun::Spawn(format!("{program}: {e}")), String::new()),
226 };
227 drop(cmd);
230 let tail = Arc::new(Mutex::new(VecDeque::with_capacity(TAIL)));
231 let mut drains = Vec::new();
232 if let Some(read) = reader {
233 drains.push(drain(read, Arc::clone(&tail)));
234 }
235 if let Some(out) = child.stdout.take() {
236 drains.push(drain(out, Arc::clone(&tail)));
237 }
238 if let Some(err) = child.stderr.take() {
239 drains.push(drain(err, Arc::clone(&tail)));
240 }
241 loop {
242 match child.try_wait() {
243 Ok(Some(status)) => {
244 let until = if need_output {
251 deadline
252 } else {
253 Instant::now() + GRACE
254 };
255 while Instant::now() < until
256 && !(need_output && cancel.load(Ordering::SeqCst))
257 && drains.iter().any(|d| !d.is_finished())
258 {
259 std::thread::sleep(Duration::from_millis(5));
260 }
261 if need_output && drains.iter().any(|d| !d.is_finished()) {
262 return (TreeRun::TimedOut, String::new());
263 }
264 let out = tail
265 .lock()
266 .map(|t| t.iter().cloned().collect::<Vec<_>>().join("\n"))
267 .unwrap_or_default();
268 return if status.success() {
269 (TreeRun::Passed, out)
270 } else {
271 (TreeRun::Failed(summary(&tail)), out)
272 };
273 }
274 Ok(None) => {}
275 Err(e) => {
276 kill_tree(&mut child);
277 return (TreeRun::Spawn(format!("{program}: {e}")), String::new());
278 }
279 }
280 if cancel.load(Ordering::SeqCst) {
281 kill_tree(&mut child);
282 return (TreeRun::Cancelled, String::new());
283 }
284 if Instant::now() >= deadline {
285 kill_tree(&mut child);
286 return (TreeRun::TimedOut, String::new());
287 }
288 std::thread::sleep(POLL);
289 }
290}
291
292#[cfg(all(test, unix))]
293mod tests {
294 use super::*;
295
296 fn sh(script: &str) -> Vec<String> {
297 vec!["sh".into(), "-c".into(), script.into()]
298 }
299
300 fn soon(ms: u64) -> Instant {
301 Instant::now() + Duration::from_millis(ms)
302 }
303
304 #[test]
305 fn tree_run_passed() {
306 let r = run(
307 &sh("exit 0"),
308 Path::new("."),
309 soon(5_000),
310 &AtomicBool::new(false),
311 );
312 assert_eq!(r, TreeRun::Passed);
313 }
314
315 #[test]
316 fn tree_run_failed_summarises_the_last_line() {
317 let r = run(
318 &sh("echo 'a.ts: bad'; echo '3 problems' >&2; exit 1"),
319 Path::new("."),
320 soon(5_000),
321 &AtomicBool::new(false),
322 );
323 assert_eq!(r, TreeRun::Failed("3 problems".into()));
324 }
325
326 #[test]
327 fn tree_run_spawn_error() {
328 let r = run(
329 &["amont-no-such-program-xyz".to_string()],
330 Path::new("."),
331 soon(5_000),
332 &AtomicBool::new(false),
333 );
334 assert!(matches!(r, TreeRun::Spawn(_)), "{r:?}");
335 }
336
337 #[test]
340 fn tree_run_timed_out_leaves_no_child_alive() {
341 let dir = std::env::temp_dir().join(format!("amont-tree-run-{}", std::process::id()));
342 std::fs::create_dir_all(&dir).unwrap();
343 let pidfile = dir.join("grandchild.pid");
344 let script = format!("sleep 30 & echo $! > {}; wait", pidfile.display());
345 let r = run(&sh(&script), &dir, soon(400), &AtomicBool::new(false));
346 assert_eq!(r, TreeRun::TimedOut);
347 let pid: i32 = std::fs::read_to_string(&pidfile)
348 .unwrap()
349 .trim()
350 .parse()
351 .unwrap();
352 std::thread::sleep(Duration::from_millis(100));
353 let alive = unsafe { libc_kill(pid, 0) == 0 };
355 assert!(!alive, "grandchild {pid} survived the deadline");
356 let _ = std::fs::remove_dir_all(&dir);
357 }
358
359 #[test]
360 fn tree_run_cancelled() {
361 let flag = Arc::new(AtomicBool::new(false));
362 let setter = Arc::clone(&flag);
363 std::thread::spawn(move || {
364 std::thread::sleep(Duration::from_millis(150));
365 setter.store(true, Ordering::SeqCst);
366 });
367 let r = run(&sh("sleep 30"), Path::new("."), soon(10_000), &flag);
368 assert_eq!(r, TreeRun::Cancelled);
369 }
370}