1use spate_core::pipeline::{ExitReport, StartError};
6use std::thread::JoinHandle;
7use std::time::{Duration, Instant};
8
9#[derive(Debug)]
14pub struct PipelineRun {
15 rx: crossbeam_channel::Receiver<Result<ExitReport, StartError>>,
16 join: JoinHandle<()>,
17}
18
19impl PipelineRun {
20 pub fn spawn(run: impl FnOnce() -> Result<ExitReport, StartError> + Send + 'static) -> Self {
23 let (tx, rx) = crossbeam_channel::bounded(1);
24 #[expect(
25 clippy::disallowed_methods,
26 reason = "the thread ends when `run` returns; `PipelineRun::join` joins it, and dropping a run never blocks"
27 )]
28 let join = std::thread::spawn(move || {
29 let _ = tx.send(run());
31 });
32 Self { rx, join }
33 }
34
35 pub fn wait_exit(&self, timeout: Duration) -> Option<Result<ExitReport, StartError>> {
40 self.rx.recv_timeout(timeout).ok()
41 }
42
43 pub fn join(self) -> Result<ExitReport, StartError> {
46 let report = self.rx.recv().expect("pipeline thread dropped its result");
47 self.join.join().expect("pipeline thread panicked");
48 report
49 }
50}
51
52const POLL_INTERVAL: Duration = Duration::from_millis(5);
56
57pub fn wait_until(timeout: Duration, what: &str, mut check: impl FnMut() -> bool) {
60 if poll_until(timeout, || check().then_some(())).is_none() {
61 panic!("timed out after {timeout:?} waiting for: {what}");
62 }
63}
64
65pub(crate) fn poll_until<T>(timeout: Duration, mut check: impl FnMut() -> Option<T>) -> Option<T> {
67 let deadline = Instant::now() + timeout;
68 while Instant::now() < deadline {
69 if let Some(value) = check() {
70 return Some(value);
71 }
72 std::thread::sleep(POLL_INTERVAL);
73 }
74 None
75}