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 #[cfg(windows)]
48 {
49 let win = dir.join(format!("{name}.exe"));
50 if win.is_file() {
51 return win.into_os_string();
52 }
53 }
54 let candidate = dir.join(name);
55 if candidate.is_file() {
56 return candidate.into_os_string();
57 }
58 }
59 OsString::from(cmd)
60}
61
62#[cfg(unix)]
64fn kill_tree(child: &mut Child) {
65 let pid = child.id() as i32;
68 unsafe {
69 libc::kill(-pid, libc::SIGKILL);
70 }
71 let _ = child.kill();
72}
73
74#[cfg(not(unix))]
75fn kill_tree(child: &mut Child) {
76 let _ = child.kill();
77}
78
79pub fn run_captured(
83 mut command: Command,
84 stdin_text: Option<&str>,
85 timeout: Option<Duration>,
86) -> Result<Outcome, String> {
87 command
88 .stdin(Stdio::piped())
89 .stdout(Stdio::piped())
90 .stderr(Stdio::piped());
91 #[cfg(unix)]
92 {
93 use std::os::unix::process::CommandExt;
94 command.process_group(0);
96 }
97
98 let mut child = command
99 .spawn()
100 .map_err(|e| format!("failed to launch: {e}"))?;
101
102 let stdin_pipe = child.stdin.take();
106 let stdin_thread = stdin_text.map(|text| {
107 let text = text.to_string();
108 std::thread::spawn(move || {
109 if let Some(mut pipe) = stdin_pipe {
110 use std::io::Write;
111 let _ = pipe.write_all(text.as_bytes());
112 }
113 })
114 });
115
116 let mut out_pipe = child.stdout.take().expect("stdout was piped");
119 let mut err_pipe = child.stderr.take().expect("stderr was piped");
120 let out_thread = std::thread::spawn(move || {
121 let mut buf = Vec::new();
122 let _ = out_pipe.read_to_end(&mut buf);
123 buf
124 });
125 let err_thread = std::thread::spawn(move || {
126 let mut buf = Vec::new();
127 let _ = err_pipe.read_to_end(&mut buf);
128 buf
129 });
130
131 let deadline = timeout.map(|t| Instant::now() + t);
132 let (status, timed_out) = loop {
133 match child.try_wait() {
134 Ok(Some(status)) => break (Some(status), false),
135 Ok(None) => {}
136 Err(e) => return Err(format!("waiting for command: {e}")),
137 }
138 if let Some(d) = deadline
139 && Instant::now() >= d
140 {
141 kill_tree(&mut child);
142 let _ = child.wait();
143 break (None, true);
144 }
145 std::thread::sleep(Duration::from_millis(10));
146 };
147
148 if let Some(t) = stdin_thread {
149 let _ = t.join();
150 }
151 let stdout = String::from_utf8_lossy(&out_thread.join().unwrap_or_default()).into_owned();
152 let stderr = String::from_utf8_lossy(&err_thread.join().unwrap_or_default()).into_owned();
153
154 Ok(Outcome {
155 stdout,
156 stderr,
157 status,
158 timed_out,
159 })
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn resolve_falls_back_to_bare_name() {
168 assert_eq!(resolve_program("grep", "grep"), OsString::from("grep"));
170 assert_eq!(resolve_program("/bin/ls", "ls"), OsString::from("/bin/ls"));
172 }
173
174 #[cfg(unix)]
175 #[test]
176 fn captures_streams_and_status() {
177 let mut c = Command::new("sh");
178 c.args(["-c", "echo out; echo err >&2; exit 3"]);
179 let r = run_captured(c, None, None).unwrap();
180 assert_eq!(r.stdout, "out\n");
181 assert_eq!(r.stderr, "err\n");
182 assert_eq!(r.status.unwrap().code(), Some(3));
183 assert!(!r.timed_out);
184 }
185
186 #[cfg(unix)]
187 #[test]
188 fn stdin_text_reaches_the_child() {
189 let mut c = Command::new("cat");
190 c.arg("-");
191 let r = run_captured(c, Some("hello\n"), None).unwrap();
192 assert_eq!(r.stdout, "hello\n");
193 }
194
195 #[cfg(unix)]
196 #[test]
197 fn timeout_kills_and_reports() {
198 let mut c = Command::new("sh");
199 c.args(["-c", "sleep 30"]);
200 let started = Instant::now();
201 let r = run_captured(c, None, Some(Duration::from_millis(100))).unwrap();
202 assert!(r.timed_out);
203 assert!(r.status.is_none());
204 assert!(
205 started.elapsed() < Duration::from_secs(5),
206 "kill must be prompt"
207 );
208 }
209}