1use std::sync::mpsc;
13use std::thread;
14use std::time::Duration;
15
16use crate::runtime::ExecError;
17
18pub fn with_timeout<F, T>(timeout: Duration, work: F) -> Result<T, ExecError>
25where
26 F: FnOnce() -> Result<T, ExecError> + Send + 'static,
27 T: Send + 'static,
28{
29 let (tx, rx) = mpsc::sync_channel::<Result<T, ExecError>>(1);
30 thread::Builder::new()
31 .name("outl-exec".into())
32 .spawn(move || {
33 let _ = tx.send(work());
37 })
38 .map_err(|e| ExecError::Sandbox(format!("spawn worker: {e}")))?;
39
40 match rx.recv_timeout(timeout) {
41 Ok(result) => result,
42 Err(mpsc::RecvTimeoutError::Timeout) => Err(ExecError::Timeout(timeout)),
43 Err(mpsc::RecvTimeoutError::Disconnected) => Err(ExecError::Sandbox(
44 "worker thread vanished without a result".into(),
45 )),
46 }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52
53 #[test]
54 fn fast_work_returns_value() {
55 let v: Result<i32, ExecError> = with_timeout(Duration::from_secs(2), || Ok(42));
56 assert!(matches!(v, Ok(42)));
57 }
58
59 #[test]
60 fn slow_work_times_out() {
61 let v: Result<(), ExecError> = with_timeout(Duration::from_millis(50), || {
62 std::thread::sleep(Duration::from_millis(500));
63 Ok(())
64 });
65 assert!(matches!(v, Err(ExecError::Timeout(_))));
66 }
67
68 #[test]
69 fn inner_error_propagates() {
70 let v: Result<(), ExecError> = with_timeout(Duration::from_secs(2), || {
71 Err(ExecError::Language("oops".into()))
72 });
73 match v {
74 Err(ExecError::Language(m)) => assert_eq!(m, "oops"),
75 other => panic!("expected Language error, got {other:?}"),
76 }
77 }
78}