1use std::collections::HashMap;
6use std::io::Read;
7use std::path::PathBuf;
8use std::process::{Command, Stdio};
9use std::sync::{Arc, Condvar, Mutex};
10use std::time::{Duration, Instant};
11
12use nix::sys::signal::{Signal, killpg};
13use nix::unistd::Pid;
14
15pub const KEPT: usize = 1024 * 1024;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Exit {
21 pub code: Option<u32>,
23 pub signal: Option<String>,
25}
26
27#[derive(Debug, Default)]
28struct Shared {
29 output: Vec<u8>,
30 total: usize,
31 exit: Option<Exit>,
32}
33
34#[derive(Debug)]
35struct Term {
36 shared: Arc<(Mutex<Shared>, Condvar)>,
37 pgid: Option<Pid>,
38 limit: Option<usize>,
39}
40
41#[derive(Debug, Default)]
43pub struct Terminals {
44 next: Mutex<u64>,
45 all: Mutex<HashMap<String, Term>>,
46}
47
48fn kill(pgid: Option<Pid>) {
49 if let Some(p) = pgid {
50 let _ = killpg(p, Signal::SIGKILL);
51 }
52}
53
54impl Terminals {
55 pub fn create(
62 &self,
63 runner: &crate::sandbox::Runner,
64 argv: &[String],
65 cwd: PathBuf,
66 mut env: Vec<(String, String)>,
67 limit: Option<usize>,
68 deadline: Duration,
69 ) -> Result<String, String> {
70 let (wrapped, tmp) = runner.wrap(argv)?;
71 if let Some(tmp) = tmp {
72 env.retain(|(k, _)| !matches!(k.as_str(), "TMPDIR" | "TMP" | "TEMP"));
73 let t = tmp.to_string_lossy().into_owned();
74 env.extend(["TMPDIR", "TMP", "TEMP"].map(|k| (k.to_owned(), t.clone())));
75 }
76 let Some((program, args)) = wrapped.split_first() else {
77 return Err("empty command".to_owned());
78 };
79 let mut cmd = Command::new(program);
80 cmd.args(args)
81 .current_dir(&cwd)
82 .env_clear()
83 .envs(env)
84 .stdin(Stdio::null())
85 .stdout(Stdio::piped())
86 .stderr(Stdio::piped());
87 #[cfg(unix)]
88 std::os::unix::process::CommandExt::process_group(&mut cmd, 0);
89 let mut child = cmd
90 .spawn()
91 .map_err(|e| format!("could not start `{}`: {e}", argv.join(" ")))?;
92 let pgid = i32::try_from(child.id()).ok().map(Pid::from_raw);
93 let shared: Arc<(Mutex<Shared>, Condvar)> = Arc::default();
94 let finished_reading = Arc::new(std::sync::atomic::AtomicUsize::new(0));
95 let readers: Vec<std::thread::JoinHandle<()>> = [
96 child
97 .stdout
98 .take()
99 .map(|s| Box::new(s) as Box<dyn Read + Send>),
100 child
101 .stderr
102 .take()
103 .map(|s| Box::new(s) as Box<dyn Read + Send>),
104 ]
105 .into_iter()
106 .flatten()
107 .map(|mut stream| {
108 let shared = Arc::clone(&shared);
109 let finished = Arc::clone(&finished_reading);
110 std::thread::spawn(move || {
111 let mut buf = [0u8; 8192];
112 while let Ok(n) = stream.read(&mut buf) {
113 if n == 0 {
114 break;
115 }
116 if let Ok(mut s) = shared.0.lock() {
117 s.total += n;
118 s.output.extend_from_slice(&buf[..n]);
119 let keep = KEPT + crate::secrets::longest();
121 if s.output.len() > keep {
122 let cut = s.output.len() - keep;
123 s.output.drain(..cut);
124 }
125 }
126 }
127 finished.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
128 })
129 })
130 .collect();
131 let waiter = Arc::clone(&shared);
132 std::thread::spawn(move || {
133 let began = Instant::now();
134 let status = loop {
135 match child.try_wait() {
136 Ok(Some(s)) => break Some(s),
137 Ok(None) if began.elapsed() >= deadline => {
138 kill(pgid);
139 break child.wait().ok();
140 }
141 Ok(None) => std::thread::sleep(Duration::from_millis(10)),
142 Err(_) => break None,
143 }
144 };
145 kill(pgid);
147 let n = readers.len();
151 let read_until = Instant::now() + Duration::from_secs(2);
152 while finished_reading.load(std::sync::atomic::Ordering::SeqCst) < n
153 && Instant::now() < read_until
154 {
155 std::thread::sleep(Duration::from_millis(10));
156 }
157 drop(readers);
158 let exit = {
159 #[cfg(unix)]
160 let signal = status
161 .and_then(|s| std::os::unix::process::ExitStatusExt::signal(&s))
162 .map(|n| {
163 Signal::try_from(n)
164 .map_or_else(|_| n.to_string(), |s| s.as_str().to_owned())
165 });
166 #[cfg(not(unix))]
167 let signal: Option<String> = None;
168 Exit {
169 code: status
170 .and_then(|s| s.code())
171 .and_then(|c| u32::try_from(c).ok()),
172 signal,
173 }
174 };
175 if let Ok(mut s) = waiter.0.lock() {
176 s.exit = Some(exit);
177 }
178 waiter.1.notify_all();
179 });
180 let id = {
181 let mut n = self.next.lock().map_err(|e| e.to_string())?;
182 *n += 1;
183 format!("term-{n}")
184 };
185 self.all.lock().map_err(|e| e.to_string())?.insert(
186 id.clone(),
187 Term {
188 shared,
189 pgid,
190 limit,
191 },
192 );
193 Ok(id)
194 }
195
196 fn with<T>(&self, id: &str, f: impl FnOnce(&Term) -> T) -> Result<T, String> {
197 let all = self.all.lock().map_err(|e| e.to_string())?;
198 all.get(id)
199 .map(f)
200 .ok_or_else(|| format!("no terminal `{id}`"))
201 }
202
203 pub fn output(&self, id: &str) -> Result<(String, bool, Option<Exit>), String> {
210 self.with(id, |t| {
211 let Ok(s) = t.shared.0.lock() else {
212 return (String::new(), false, None);
213 };
214 let masked = crate::secrets::mask(&String::from_utf8_lossy(&s.output));
215 let limit = t.limit.unwrap_or(KEPT).min(KEPT);
216 let mut cut = masked.len().saturating_sub(limit);
217 while !masked.is_char_boundary(cut) {
218 cut += 1;
219 }
220 let truncated = cut > 0 || s.total > s.output.len();
221 (masked[cut..].to_owned(), truncated, s.exit.clone())
222 })
223 }
224
225 pub fn wait(&self, id: &str) -> Result<Exit, String> {
231 let shared = self.with(id, |t| Arc::clone(&t.shared))?;
232 let (lock, cv) = &*shared;
233 let mut s = lock.lock().map_err(|e| e.to_string())?;
234 loop {
235 if let Some(e) = &s.exit {
236 return Ok(e.clone());
237 }
238 s = cv.wait(s).map_err(|e| e.to_string())?;
239 }
240 }
241
242 pub fn kill(&self, id: &str) -> Result<(), String> {
248 self.with(id, |t| kill(t.pgid))
249 }
250
251 pub fn release(&self, id: &str) -> Result<(), String> {
257 let t = self
258 .all
259 .lock()
260 .map_err(|e| e.to_string())?
261 .remove(id)
262 .ok_or_else(|| format!("no terminal `{id}`"))?;
263 kill(t.pgid);
264 Ok(())
265 }
266
267 pub fn kill_all(&self) {
269 if let Ok(all) = self.all.lock() {
270 for t in all.values() {
271 if t.shared.0.lock().is_ok_and(|s| s.exit.is_none()) {
272 kill(t.pgid);
273 }
274 }
275 }
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn a_command_runs_and_its_output_is_kept_from_the_end() {
285 let t = Terminals::default();
286 let dir = std::env::temp_dir();
287 let env: Vec<(String, String)> = std::env::vars().collect();
288 let argv = ["sh", "-c", "printf 'hello world'; exit 3"].map(str::to_owned);
289 let id = t
290 .create(
291 &crate::sandbox::Runner::Plain,
292 &argv,
293 dir.clone(),
294 env.clone(),
295 Some(5),
296 Duration::from_secs(10),
297 )
298 .unwrap_or_default();
299 let exit = t.wait(&id).unwrap_or(Exit {
300 code: None,
301 signal: None,
302 });
303 assert_eq!(exit.code, Some(3));
304 let (out, truncated, ended) = t.output(&id).unwrap_or_default();
305 assert_eq!(
306 (out.as_str(), truncated, ended.is_some()),
307 ("world", true, true)
308 );
309 let argv = ["sleep", "5"].map(str::to_owned);
311 let id = t
312 .create(
313 &crate::sandbox::Runner::Plain,
314 &argv,
315 dir,
316 env,
317 None,
318 Duration::from_millis(100),
319 )
320 .unwrap_or_default();
321 let exit = t.wait(&id).unwrap_or(Exit {
322 code: Some(0),
323 signal: None,
324 });
325 assert_eq!(exit.code, None);
326 assert!(t.release(&id).is_ok());
327 assert!(t.output(&id).is_err());
328 }
329
330 #[test]
331 fn a_process_that_leaves_the_group_never_keeps_the_exit_from_being_said() {
332 let t = Terminals::default();
333 let env: Vec<(String, String)> = std::env::vars().collect();
334 let argv = [
336 "python3",
337 "-c",
338 "import os, time\nif os.fork() == 0:\n os.setsid()\n time.sleep(30)\nprint('done')",
339 ]
340 .map(str::to_owned);
341 let began = Instant::now();
342 let id = t
343 .create(
344 &crate::sandbox::Runner::Plain,
345 &argv,
346 std::env::temp_dir(),
347 env,
348 None,
349 Duration::from_secs(60),
350 )
351 .unwrap_or_default();
352 let exit = t.wait(&id).unwrap_or(Exit {
353 code: None,
354 signal: None,
355 });
356 assert_eq!(exit.code, Some(0));
357 assert!(
358 began.elapsed() < Duration::from_secs(10),
359 "{:?}",
360 began.elapsed()
361 );
362 assert!(t.output(&id).unwrap_or_default().0.contains("done"));
363 }
364}