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