coding_tools/
supervise.rs1use std::ffi::OsString;
19use std::io::Read;
20use std::process::{Child, Command, ExitStatus, Stdio};
21use std::time::{Duration, Instant};
22
23pub struct Outcome {
25 pub stdout: String,
27 pub stderr: String,
29 pub status: Option<ExitStatus>,
31 pub timed_out: bool,
33}
34
35pub fn resolve_program(cmd: &str, name: &str) -> OsString {
40 if name.starts_with("ct-")
41 && !cmd.contains('/')
42 && let Ok(exe) = std::env::current_exe()
43 && let Some(dir) = exe.parent()
44 {
45 let candidate = dir.join(name);
46 if candidate.is_file() {
47 return candidate.into_os_string();
48 }
49 }
50 OsString::from(cmd)
51}
52
53#[cfg(unix)]
55fn kill_tree(child: &mut Child) {
56 let pid = child.id() as i32;
59 unsafe {
60 libc::kill(-pid, libc::SIGKILL);
61 }
62 let _ = child.kill();
63}
64
65#[cfg(not(unix))]
66fn kill_tree(child: &mut Child) {
67 let _ = child.kill();
68}
69
70pub fn run_captured(
74 mut command: Command,
75 stdin_text: Option<&str>,
76 timeout: Option<Duration>,
77) -> Result<Outcome, String> {
78 command
79 .stdin(Stdio::piped())
80 .stdout(Stdio::piped())
81 .stderr(Stdio::piped());
82 #[cfg(unix)]
83 {
84 use std::os::unix::process::CommandExt;
85 command.process_group(0);
87 }
88
89 let mut child = command.spawn().map_err(|e| format!("failed to launch: {e}"))?;
90
91 let stdin_pipe = child.stdin.take();
95 let stdin_thread = stdin_text.map(|text| {
96 let text = text.to_string();
97 std::thread::spawn(move || {
98 if let Some(mut pipe) = stdin_pipe {
99 use std::io::Write;
100 let _ = pipe.write_all(text.as_bytes());
101 }
102 })
103 });
104
105 let mut out_pipe = child.stdout.take().expect("stdout was piped");
108 let mut err_pipe = child.stderr.take().expect("stderr was piped");
109 let out_thread = std::thread::spawn(move || {
110 let mut buf = Vec::new();
111 let _ = out_pipe.read_to_end(&mut buf);
112 buf
113 });
114 let err_thread = std::thread::spawn(move || {
115 let mut buf = Vec::new();
116 let _ = err_pipe.read_to_end(&mut buf);
117 buf
118 });
119
120 let deadline = timeout.map(|t| Instant::now() + t);
121 let (status, timed_out) = loop {
122 match child.try_wait() {
123 Ok(Some(status)) => break (Some(status), false),
124 Ok(None) => {}
125 Err(e) => return Err(format!("waiting for command: {e}")),
126 }
127 if let Some(d) = deadline
128 && Instant::now() >= d
129 {
130 kill_tree(&mut child);
131 let _ = child.wait();
132 break (None, true);
133 }
134 std::thread::sleep(Duration::from_millis(10));
135 };
136
137 if let Some(t) = stdin_thread {
138 let _ = t.join();
139 }
140 let stdout = String::from_utf8_lossy(&out_thread.join().unwrap_or_default()).into_owned();
141 let stderr = String::from_utf8_lossy(&err_thread.join().unwrap_or_default()).into_owned();
142
143 Ok(Outcome {
144 stdout,
145 stderr,
146 status,
147 timed_out,
148 })
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn resolve_falls_back_to_bare_name() {
157 assert_eq!(resolve_program("grep", "grep"), OsString::from("grep"));
159 assert_eq!(
161 resolve_program("/bin/ls", "ls"),
162 OsString::from("/bin/ls")
163 );
164 }
165
166 #[cfg(unix)]
167 #[test]
168 fn captures_streams_and_status() {
169 let mut c = Command::new("sh");
170 c.args(["-c", "echo out; echo err >&2; exit 3"]);
171 let r = run_captured(c, None, None).unwrap();
172 assert_eq!(r.stdout, "out\n");
173 assert_eq!(r.stderr, "err\n");
174 assert_eq!(r.status.unwrap().code(), Some(3));
175 assert!(!r.timed_out);
176 }
177
178 #[cfg(unix)]
179 #[test]
180 fn stdin_text_reaches_the_child() {
181 let mut c = Command::new("cat");
182 c.arg("-");
183 let r = run_captured(c, Some("hello\n"), None).unwrap();
184 assert_eq!(r.stdout, "hello\n");
185 }
186
187 #[cfg(unix)]
188 #[test]
189 fn timeout_kills_and_reports() {
190 let mut c = Command::new("sh");
191 c.args(["-c", "sleep 30"]);
192 let started = Instant::now();
193 let r = run_captured(c, None, Some(Duration::from_millis(100))).unwrap();
194 assert!(r.timed_out);
195 assert!(r.status.is_none());
196 assert!(
197 started.elapsed() < Duration::from_secs(5),
198 "kill must be prompt"
199 );
200 }
201}