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(private) = tmp {
75 env.retain(|(k, _)| !private.iter().any(|(p, _)| p == k));
76 env.extend(private);
77 }
78 let Some((program, args)) = wrapped.split_first() else {
79 return Err("empty command".to_owned());
80 };
81 let mut cmd = Command::new(program);
82 cmd.args(args)
83 .current_dir(&cwd)
84 .env_clear()
85 .envs(env)
86 .stdin(Stdio::null())
87 .stdout(Stdio::piped())
88 .stderr(Stdio::piped());
89 #[cfg(unix)]
90 if !caged {
91 std::os::unix::process::CommandExt::process_group(&mut cmd, 0);
92 }
93 let mut child = cmd
94 .spawn()
95 .map_err(|e| format!("could not start `{}`: {e}", argv.join(" ")))?;
96 let pid = Some(child.id());
97 let shared: Arc<(Mutex<Shared>, Condvar)> = Arc::default();
98 let finished_reading = Arc::new(std::sync::atomic::AtomicUsize::new(0));
99 let readers: Vec<std::thread::JoinHandle<()>> = [
100 child
101 .stdout
102 .take()
103 .map(|s| Box::new(s) as Box<dyn Read + Send>),
104 child
105 .stderr
106 .take()
107 .map(|s| Box::new(s) as Box<dyn Read + Send>),
108 ]
109 .into_iter()
110 .flatten()
111 .map(|mut stream| {
112 let shared = Arc::clone(&shared);
113 let finished = Arc::clone(&finished_reading);
114 std::thread::spawn(move || {
115 let mut buf = [0u8; 8192];
116 while let Ok(n) = stream.read(&mut buf) {
117 if n == 0 {
118 break;
119 }
120 if let Ok(mut s) = shared.0.lock() {
121 s.total += n;
122 s.output.extend_from_slice(&buf[..n]);
123 let keep = KEPT + crate::secrets::longest();
125 if s.output.len() > keep {
126 let cut = s.output.len() - keep;
127 s.output.drain(..cut);
128 }
129 }
130 }
131 finished.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
132 })
133 })
134 .collect();
135 let waiter = Arc::clone(&shared);
136 std::thread::spawn(move || {
137 let began = Instant::now();
138 let status = loop {
139 match child.try_wait() {
140 Ok(Some(s)) => break Some(s),
141 Ok(None) if began.elapsed() >= deadline => {
142 kill(pid);
143 break child.wait().ok();
144 }
145 Ok(None) => std::thread::sleep(Duration::from_millis(10)),
146 Err(_) => break None,
147 }
148 };
149 kill(pid);
151 let n = readers.len();
155 let read_until = Instant::now() + Duration::from_secs(2);
156 while finished_reading.load(std::sync::atomic::Ordering::SeqCst) < n
157 && Instant::now() < read_until
158 {
159 std::thread::sleep(Duration::from_millis(10));
160 }
161 drop(readers);
162 let exit = {
163 #[cfg(unix)]
164 let signal = status
165 .and_then(|s| std::os::unix::process::ExitStatusExt::signal(&s))
166 .map(|n| {
167 Signal::try_from(n)
168 .map_or_else(|_| n.to_string(), |s| s.as_str().to_owned())
169 });
170 #[cfg(not(unix))]
171 let signal: Option<String> = None;
172 Exit {
173 code: status
174 .and_then(|s| s.code())
175 .and_then(|c| u32::try_from(c).ok()),
176 signal,
177 }
178 };
179 if let Ok(mut s) = waiter.0.lock() {
180 s.exit = Some(exit);
181 }
182 waiter.1.notify_all();
183 });
184 let id = {
185 let mut n = self.next.lock().map_err(|e| e.to_string())?;
186 *n += 1;
187 format!("term-{n}")
188 };
189 self.all
190 .lock()
191 .map_err(|e| e.to_string())?
192 .insert(id.clone(), Term { shared, pid, limit });
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.pid))
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.pid);
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.pid);
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}