planarian_backend_linux/
cmd.rs1use std::io::Read;
4use std::process::{Command, Stdio};
5use std::time::{Duration, Instant};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct Output {
9 pub status: Option<i32>,
10 pub stdout: String,
11 pub stderr: String,
12 pub timed_out: bool,
13 pub truncated: bool,
16}
17
18impl Output {
19 pub fn ok(&self) -> bool {
20 self.status == Some(0) && !self.timed_out
21 }
22
23 pub fn tail(&self) -> String {
25 let src = if self.stderr.trim().is_empty() { &self.stdout } else { &self.stderr };
26 let lines: Vec<&str> = src.lines().collect();
27 lines[lines.len().saturating_sub(4)..].join(" | ")
28 }
29}
30
31pub const MAX_OUTPUT: usize = 1 << 20;
32
33pub fn run(argv: &[&str], timeout: Duration) -> Output {
36 run_limited(argv, timeout, MAX_OUTPUT)
37}
38
39pub fn run_limited(argv: &[&str], timeout: Duration, max_output: usize) -> Output {
40 let fail =
41 |e: String| Output { status: None, stdout: String::new(), stderr: e, timed_out: false, truncated: false };
42 let Some((prog, args)) = argv.split_first() else { return fail("empty command".into()) };
43 let mut cmd = Command::new(prog);
44 cmd.args(args).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
45 std::os::unix::process::CommandExt::process_group(&mut cmd, 0);
46 let mut child = match cmd.spawn() {
47 Ok(c) => c,
48 Err(e) => return fail(format!("{prog}: {e}")),
49 };
50 let mut out = child.stdout.take().expect("piped");
51 let mut err = child.stderr.take().expect("piped");
52 let out_t = std::thread::spawn(move || {
53 let mut buf = Vec::new();
54 let _ = (&mut out).take(max_output as u64).read_to_end(&mut buf);
55 let dropped = std::io::copy(&mut out, &mut std::io::sink()).unwrap_or(0);
56 (buf, dropped > 0)
57 });
58 let err_t = std::thread::spawn(move || {
59 let mut buf = Vec::new();
60 let _ = (&mut err).take(max_output as u64).read_to_end(&mut buf);
61 let dropped = std::io::copy(&mut err, &mut std::io::sink()).unwrap_or(0);
62 (buf, dropped > 0)
63 });
64 let deadline = Instant::now() + timeout;
65 let mut timed_out = false;
66 let status = loop {
67 match child.try_wait() {
68 Ok(Some(s)) => break s.code(),
69 Ok(None) if Instant::now() >= deadline => {
70 timed_out = true;
71 if let Some(pid) = rustix::process::Pid::from_raw(child.id() as i32) {
72 let _ = rustix::process::kill_process_group(pid, rustix::process::Signal::KILL);
73 }
74 let _ = child.kill();
75 break child.wait().ok().and_then(|s| s.code());
76 }
77 Ok(None) => std::thread::sleep(Duration::from_millis(20)),
78 Err(e) => return fail(e.to_string()),
79 }
80 };
81 let (out, out_cut) = out_t.join().unwrap_or_default();
82 let (err, err_cut) = err_t.join().unwrap_or_default();
83 let stdout = String::from_utf8_lossy(&out).into_owned();
84 let stderr = String::from_utf8_lossy(&err).into_owned();
85 Output { status, stdout, stderr, timed_out, truncated: out_cut || err_cut }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn runs_and_times_out() {
94 let o = run(&["sh", "-c", "echo hi; echo err >&2; exit 3"], Duration::from_secs(5));
95 assert_eq!(o.status, Some(3));
96 assert_eq!(o.stdout.trim(), "hi");
97 assert_eq!(o.tail(), "err");
98 let o = run(&["sh", "-c", "sleep 5 & sleep 5"], Duration::from_millis(200));
99 assert!(o.timed_out && !o.ok());
100 let o = run_limited(&["sh", "-c", "yes | head -c 100000"], Duration::from_secs(5), 10);
101 assert_eq!(o.stdout.len(), 10);
102 assert!(!run(&["/nonexistent"], Duration::from_secs(1)).ok());
103 }
104}