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
103#[cfg(unix)]
113static LIVE: std::sync::Mutex<Vec<u32>> = std::sync::Mutex::new(Vec::new());
114
115static DYING: AtomicBool = AtomicBool::new(false);
117
118fn park_if_dying() {
122 while DYING.load(Ordering::SeqCst) {
123 std::thread::sleep(POLL);
124 }
125}
126
127#[cfg(unix)]
129struct Listed(u32);
130
131#[cfg(unix)]
132impl Listed {
133 fn new(pgid: u32) -> Listed {
134 LIVE.lock().unwrap_or_else(|p| p.into_inner()).push(pgid);
135 Listed(pgid)
136 }
137}
138
139#[cfg(unix)]
140impl Drop for Listed {
141 fn drop(&mut self) {
142 LIVE.lock()
143 .unwrap_or_else(|p| p.into_inner())
144 .retain(|g| *g != self.0);
145 }
146}
147
148#[cfg(unix)]
152pub fn reap_live() {
153 DYING.store(true, std::sync::atomic::Ordering::SeqCst);
157 let groups: Vec<u32> = LIVE.lock().unwrap_or_else(|p| p.into_inner()).clone();
158 if groups.is_empty() {
159 return;
160 }
161 for g in &groups {
162 signal_group(*g, SIGTERM);
163 }
164 let until = Instant::now() + GRACE;
165 while Instant::now() < until && groups.iter().any(|g| group_alive(*g)) {
166 std::thread::sleep(POLL);
167 }
168 for g in groups.iter().filter(|g| group_alive(**g)) {
169 signal_group(*g, SIGKILL);
170 }
171}
172
173#[cfg(not(unix))]
174pub fn reap_live() {}
175
176fn kill_tree(child: &mut Child) {
177 #[cfg(unix)]
178 {
179 let pgid = child.id();
180 signal_group(pgid, SIGTERM);
181 let until = Instant::now() + GRACE;
182 while Instant::now() < until {
183 let _ = child.try_wait();
186 if !group_alive(pgid) {
187 break;
188 }
189 std::thread::sleep(POLL);
190 }
191 if group_alive(pgid) {
192 signal_group(pgid, SIGKILL);
193 }
194 }
195 #[cfg(not(unix))]
196 {
197 let _ = Command::new("taskkill")
198 .args(["/PID", &child.id().to_string(), "/T", "/F"])
199 .stdin(Stdio::null())
200 .stdout(Stdio::null())
201 .stderr(Stdio::null())
202 .status();
203 }
204 let _ = child.kill();
205 let _ = child.wait();
206}
207
208fn drain(
209 stream: impl Read + Send + 'static,
210 tail: Arc<Mutex<VecDeque<String>>>,
211) -> std::thread::JoinHandle<()> {
212 std::thread::spawn(move || {
213 for line in BufReader::new(stream).lines().map_while(Result::ok) {
214 if let Ok(mut t) = tail.lock() {
215 if t.len() == TAIL {
216 t.pop_front();
217 }
218 t.push_back(line);
219 }
220 }
221 })
222}
223
224fn summary(tail: &Mutex<VecDeque<String>>) -> String {
225 tail.lock()
226 .ok()
227 .and_then(|t| {
228 t.iter()
229 .rev()
230 .map(|l| l.trim())
231 .find(|l| !l.is_empty())
232 .map(str::to_string)
233 })
234 .unwrap_or_else(|| "no output".to_string())
235}
236
237pub fn run(argv: &[String], cwd: &Path, deadline: Instant, cancel: &AtomicBool) -> TreeRun {
239 let ran = run_inner(argv, cwd, deadline, cancel, false).0;
240 park_if_dying();
241 ran
242}
243
244pub fn run_output(
247 argv: &[String],
248 cwd: &Path,
249 deadline: Instant,
250 cancel: &AtomicBool,
251) -> Result<String, TreeRun> {
252 let ran = run_inner(argv, cwd, deadline, cancel, true);
253 park_if_dying();
254 match ran {
255 (TreeRun::Passed, out) => Ok(out),
256 (other, _) => Err(other),
257 }
258}
259
260fn run_inner(
261 argv: &[String],
262 cwd: &Path,
263 deadline: Instant,
264 cancel: &AtomicBool,
265 need_output: bool,
266) -> (TreeRun, String) {
267 let Some((program, rest)) = argv.split_first() else {
268 return (TreeRun::Spawn("empty command".into()), String::new());
269 };
270 let mut cmd = Command::new(program);
271 cmd.args(rest).current_dir(cwd).stdin(Stdio::null());
272 #[cfg(unix)]
273 let merged = merged_pipe();
274 #[cfg(not(unix))]
275 let merged: Option<(File, Stdio, Stdio)> = None;
276 let reader = match merged {
277 Some((read, out, err)) => {
278 cmd.stdout(out).stderr(err);
279 Some(read)
280 }
281 None => {
282 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
283 None
284 }
285 };
286 crate::hooks::common::strip_git_env(&mut cmd);
287 #[cfg(unix)]
288 {
289 use std::os::unix::process::CommandExt;
290 cmd.process_group(0);
291 unsafe {
294 cmd.pre_exec(|| {
295 libc_setpriority(0, 0, NICENESS);
296 Ok(())
297 });
298 }
299 }
300 crate::staged_only::install_signal_handler();
303 let mut child = match cmd.spawn() {
304 Ok(c) => c,
305 Err(e) => return (TreeRun::Spawn(format!("{program}: {e}")), String::new()),
306 };
307 #[cfg(unix)]
308 let _listed = Listed::new(child.id());
309 drop(cmd);
312 let tail = Arc::new(Mutex::new(VecDeque::with_capacity(TAIL)));
313 let mut drains = Vec::new();
314 if let Some(read) = reader {
315 drains.push(drain(read, Arc::clone(&tail)));
316 }
317 if let Some(out) = child.stdout.take() {
318 drains.push(drain(out, Arc::clone(&tail)));
319 }
320 if let Some(err) = child.stderr.take() {
321 drains.push(drain(err, Arc::clone(&tail)));
322 }
323 loop {
324 match child.try_wait() {
325 Ok(Some(status)) => {
326 let until = if need_output {
333 deadline
334 } else {
335 Instant::now() + GRACE
336 };
337 while Instant::now() < until
338 && !(need_output && cancel.load(Ordering::SeqCst))
339 && drains.iter().any(|d| !d.is_finished())
340 {
341 std::thread::sleep(Duration::from_millis(5));
342 }
343 if need_output && drains.iter().any(|d| !d.is_finished()) {
344 return (TreeRun::TimedOut, String::new());
345 }
346 let out = tail
347 .lock()
348 .map(|t| t.iter().cloned().collect::<Vec<_>>().join("\n"))
349 .unwrap_or_default();
350 return if status.success() {
351 (TreeRun::Passed, out)
352 } else {
353 (TreeRun::Failed(summary(&tail)), out)
354 };
355 }
356 Ok(None) => {}
357 Err(e) => {
358 kill_tree(&mut child);
359 return (TreeRun::Spawn(format!("{program}: {e}")), String::new());
360 }
361 }
362 if cancel.load(Ordering::SeqCst) {
363 kill_tree(&mut child);
364 return (TreeRun::Cancelled, String::new());
365 }
366 if Instant::now() >= deadline {
367 kill_tree(&mut child);
368 return (TreeRun::TimedOut, String::new());
369 }
370 std::thread::sleep(POLL);
371 }
372}
373
374#[cfg(all(test, unix))]
375mod tests {
376 use super::*;
377
378 fn sh(script: &str) -> Vec<String> {
379 vec!["sh".into(), "-c".into(), script.into()]
380 }
381
382 fn soon(ms: u64) -> Instant {
383 Instant::now() + Duration::from_millis(ms)
384 }
385
386 #[test]
387 fn tree_run_passed() {
388 let r = run(
389 &sh("exit 0"),
390 Path::new("."),
391 soon(5_000),
392 &AtomicBool::new(false),
393 );
394 assert_eq!(r, TreeRun::Passed);
395 }
396
397 #[test]
398 fn tree_run_failed_summarises_the_last_line() {
399 let r = run(
400 &sh("echo 'a.ts: bad'; echo '3 problems' >&2; exit 1"),
401 Path::new("."),
402 soon(5_000),
403 &AtomicBool::new(false),
404 );
405 assert_eq!(r, TreeRun::Failed("3 problems".into()));
406 }
407
408 #[test]
409 fn tree_run_spawn_error() {
410 let r = run(
411 &["amont-no-such-program-xyz".to_string()],
412 Path::new("."),
413 soon(5_000),
414 &AtomicBool::new(false),
415 );
416 assert!(matches!(r, TreeRun::Spawn(_)), "{r:?}");
417 }
418
419 #[test]
422 fn tree_run_timed_out_leaves_no_child_alive() {
423 let dir = std::env::temp_dir().join(format!("amont-tree-run-{}", std::process::id()));
424 std::fs::create_dir_all(&dir).unwrap();
425 let pidfile = dir.join("grandchild.pid");
426 let script = format!("sleep 30 & echo $! > {}; wait", pidfile.display());
427 let r = run(&sh(&script), &dir, soon(400), &AtomicBool::new(false));
428 assert_eq!(r, TreeRun::TimedOut);
429 let pid: i32 = std::fs::read_to_string(&pidfile)
430 .unwrap()
431 .trim()
432 .parse()
433 .unwrap();
434 std::thread::sleep(Duration::from_millis(100));
435 let alive = unsafe { libc_kill(pid, 0) == 0 };
437 assert!(!alive, "grandchild {pid} survived the deadline");
438 let _ = std::fs::remove_dir_all(&dir);
439 }
440
441 #[test]
442 fn tree_run_cancelled() {
443 let flag = Arc::new(AtomicBool::new(false));
444 let setter = Arc::clone(&flag);
445 std::thread::spawn(move || {
446 std::thread::sleep(Duration::from_millis(150));
447 setter.store(true, Ordering::SeqCst);
448 });
449 let r = run(&sh("sleep 30"), Path::new("."), soon(10_000), &flag);
450 assert_eq!(r, TreeRun::Cancelled);
451 }
452}