Skip to main content

sim_lib_server/trigger/
runtime.rs

1use std::{
2    fs,
3    path::PathBuf,
4    sync::{
5        Arc, Mutex,
6        atomic::{AtomicBool, Ordering},
7        mpsc,
8    },
9    thread,
10    time::{Duration, SystemTime, UNIX_EPOCH},
11};
12
13use sim_kernel::{Args, Cx, Error, Expr, Result, Symbol, Value};
14
15use crate::{
16    Server, ServerAddress, ServerFrame, decode_frame_payload, helpers::clone_server_cx,
17    symbol_list_value,
18};
19
20#[cfg(not(test))]
21use super::sources::spawn_stdin_reader;
22#[cfg(test)]
23use super::sources::{remove_test_stdin_sender, test_stdin_sender, test_stdin_senders};
24use super::{
25    NEXT_TRIGGER_ID, StdinSource, TriggerConfig, TriggerDecoder, TriggerHandle, TriggerState,
26    delivery_timeout, io_error_to_host, require_source_capability, source_capability,
27    trigger_read_policy,
28};
29
30impl TriggerHandle {
31    pub(super) fn new(server: Arc<Server>, config: TriggerConfig) -> Self {
32        let TriggerConfig {
33            source,
34            source_expr,
35            role,
36            codec,
37            decode_expr,
38            decoder,
39            cron,
40            network_source,
41        } = config;
42        let id = NEXT_TRIGGER_ID.fetch_add(1, Ordering::Relaxed);
43        let stdin = match &source {
44            ServerAddress::Stdin => {
45                let (tx, rx) = mpsc::channel();
46                #[cfg(not(test))]
47                spawn_stdin_reader(tx);
48                #[cfg(test)]
49                {
50                    test_stdin_senders()
51                        .lock()
52                        .expect("test stdin sender registry poisoned")
53                        .insert(id, tx);
54                }
55                StdinSource::Channel(Mutex::new(rx))
56            }
57            _ => StdinSource::Unavailable,
58        };
59        Self {
60            id,
61            server,
62            source,
63            source_expr,
64            role,
65            codec,
66            decode_expr,
67            decoder,
68            cron,
69            network_source: network_source.map(Mutex::new),
70            stopping: AtomicBool::new(false),
71            handle: Mutex::new(None),
72            stdin,
73            state: Mutex::new(TriggerState::default()),
74        }
75    }
76
77    pub(crate) fn start(self: &Arc<Self>, seed: &Cx) -> Result<()> {
78        let mut handle = self
79            .handle
80            .lock()
81            .map_err(|_| Error::PoisonedLock("trigger handle"))?;
82        if handle.is_some() {
83            return Ok(());
84        }
85        let mut cx = clone_server_cx(seed);
86        let trigger = Arc::clone(self);
87        *handle = Some(thread::spawn(move || {
88            trigger.run(&mut cx);
89        }));
90        Ok(())
91    }
92
93    pub(crate) fn stop(&self) -> Result<()> {
94        self.stopping.store(true, Ordering::Relaxed);
95        #[cfg(test)]
96        remove_test_stdin_sender(self.id);
97        let join = self
98            .handle
99            .lock()
100            .map_err(|_| Error::PoisonedLock("trigger handle"))?
101            .take();
102        if let Some(join) = join {
103            join.join()
104                .map_err(|_| Error::HostError("trigger thread panicked".to_owned()))?;
105        }
106        Ok(())
107    }
108
109    pub(crate) fn poll(&self, cx: &mut Cx) -> Result<u64> {
110        match &self.source {
111            ServerAddress::Stdin => self.poll_stdin(cx),
112            ServerAddress::FileTail { path } => self.poll_file_tail(cx, path),
113            ServerAddress::Cron { .. } => self.poll_cron(cx),
114            ServerAddress::Webhook { .. }
115            | ServerAddress::Imap { .. }
116            | ServerAddress::Smtp { .. }
117            | ServerAddress::Telegram { .. }
118            | ServerAddress::Matrix { .. } => self.poll_network_source(cx),
119            other => Err(Error::Eval(format!(
120                "server/trigger does not support source {}",
121                other.kind_symbol()
122            ))),
123        }
124    }
125
126    #[cfg(test)]
127    pub(crate) fn inject_text(&self, cx: &mut Cx, text: &str) -> Result<u64> {
128        let mut delivered = 0;
129        for line in text.lines() {
130            if line.trim().is_empty() {
131                continue;
132            }
133            self.inject_event(cx, line.as_bytes())?;
134            delivered += 1;
135        }
136        Ok(delivered)
137    }
138
139    #[cfg(test)]
140    pub(crate) fn feed_stdin(&self, text: &str) -> Result<()> {
141        let sender = test_stdin_sender(self.id)?;
142        for line in text.lines() {
143            sender
144                .send(Some(format!("{line}\n").into_bytes()))
145                .map_err(|_| Error::HostError("stdin trigger source closed".to_owned()))?;
146        }
147        Ok(())
148    }
149
150    #[cfg(test)]
151    pub(crate) fn finish_stdin(&self) -> Result<()> {
152        test_stdin_sender(self.id)?
153            .send(None)
154            .map_err(|_| Error::HostError("stdin trigger source closed".to_owned()))
155    }
156
157    #[cfg(test)]
158    pub(crate) fn is_source_closed(&self) -> bool {
159        self.state
160            .lock()
161            .map(|state| state.source_closed)
162            .unwrap_or(true)
163    }
164
165    #[cfg(all(test, feature = "trigger-webhook"))]
166    pub(crate) fn webhook_port(&self) -> Result<Option<u16>> {
167        let Some(source) = &self.network_source else {
168            return Ok(None);
169        };
170        let source = source
171            .lock()
172            .map_err(|_| Error::PoisonedLock("trigger source"))?;
173        let Some(source) = source
174            .as_any()
175            .downcast_ref::<super::sources::network::WebhookSource>()
176        else {
177            return Ok(None);
178        };
179        source.local_port().map(Some)
180    }
181
182    /// Reflects the trigger's state as a descriptor table value.
183    ///
184    /// Includes its kind, role, delivered-event count, and required
185    /// capabilities.
186    pub fn reflect_value(&self, cx: &mut Cx) -> Result<Value> {
187        let role = match &self.role {
188            Some(role) => cx.factory().symbol(role.clone())?,
189            None => cx.factory().nil()?,
190        };
191        let delivered = self
192            .state
193            .lock()
194            .map_err(|_| Error::PoisonedLock("trigger state"))?
195            .delivered;
196        let capabilities = match source_capability(&self.source) {
197            Some(capability) => symbol_list_value(cx, &[capability.as_symbol()])?,
198            None => cx.factory().list(Vec::new())?,
199        };
200        cx.factory().table(vec![
201            (
202                Symbol::new("kind"),
203                cx.factory().symbol(Symbol::new("trigger"))?,
204            ),
205            (Symbol::new("id"), cx.factory().string(self.id.to_string())?),
206            (
207                Symbol::new("source"),
208                cx.factory().expr(self.source_expr.clone())?,
209            ),
210            (Symbol::new("role"), role),
211            (
212                Symbol::new("codec"),
213                cx.factory().symbol(self.codec.clone())?,
214            ),
215            (
216                Symbol::new("decode"),
217                cx.factory().expr(self.decode_expr.clone())?,
218            ),
219            (
220                Symbol::new("delivered"),
221                cx.factory().string(delivered.to_string())?,
222            ),
223            (Symbol::new("requires"), capabilities),
224        ])
225    }
226
227    fn poll_file_tail(&self, cx: &mut Cx, path: &PathBuf) -> Result<u64> {
228        let mut bytes = fs::read(path).map_err(io_error_to_host)?;
229        let lines = {
230            let mut state = self
231                .state
232                .lock()
233                .map_err(|_| Error::PoisonedLock("trigger state"))?;
234            if state.file_offset > bytes.len() {
235                state.file_offset = 0;
236                state.file_remainder.clear();
237            }
238            let mut pending = std::mem::take(&mut state.file_remainder);
239            pending.extend_from_slice(&bytes[state.file_offset..]);
240            state.file_offset = bytes.len();
241
242            let mut lines = Vec::new();
243            let mut start = 0usize;
244            for index in 0..pending.len() {
245                if pending[index] == b'\n' {
246                    let mut line = pending[start..index].to_vec();
247                    if line.last() == Some(&b'\r') {
248                        line.pop();
249                    }
250                    lines.push(line);
251                    start = index + 1;
252                }
253            }
254            state.file_remainder = pending[start..].to_vec();
255            lines
256        };
257        bytes.clear();
258
259        let mut delivered = 0;
260        for line in lines {
261            if line.iter().all(u8::is_ascii_whitespace) {
262                continue;
263            }
264            self.inject_event(cx, &line)?;
265            delivered += 1;
266        }
267        Ok(delivered)
268    }
269
270    fn poll_stdin(&self, cx: &mut Cx) -> Result<u64> {
271        let receiver = match &self.stdin {
272            StdinSource::Channel(receiver) => receiver,
273            StdinSource::Unavailable => return Ok(0),
274        };
275        let mut delivered = 0;
276        loop {
277            match receiver
278                .lock()
279                .map_err(|_| Error::PoisonedLock("stdin trigger source"))?
280                .try_recv()
281            {
282                Ok(Some(mut line)) => {
283                    if line.last() == Some(&b'\n') {
284                        line.pop();
285                    }
286                    if line.last() == Some(&b'\r') {
287                        line.pop();
288                    }
289                    if line.iter().all(u8::is_ascii_whitespace) {
290                        continue;
291                    }
292                    self.inject_event(cx, &line)?;
293                    delivered += 1;
294                }
295                Ok(None) | Err(mpsc::TryRecvError::Disconnected) => {
296                    self.state
297                        .lock()
298                        .map_err(|_| Error::PoisonedLock("trigger state"))?
299                        .source_closed = true;
300                    break;
301                }
302                Err(mpsc::TryRecvError::Empty) => break,
303            }
304        }
305        Ok(delivered)
306    }
307
308    fn poll_network_source(&self, cx: &mut Cx) -> Result<u64> {
309        let Some(source) = &self.network_source else {
310            return Ok(0);
311        };
312        let mut delivered = 0;
313        loop {
314            let timeout = if delivered == 0 {
315                delivery_timeout()
316            } else {
317                Duration::from_millis(0)
318            };
319            let event = source
320                .lock()
321                .map_err(|_| Error::PoisonedLock("trigger source"))?
322                .next_event(cx, timeout)?;
323            let Some(event) = event else {
324                break;
325            };
326            self.inject_event(cx, &event)?;
327            source
328                .lock()
329                .map_err(|_| Error::PoisonedLock("trigger source"))?
330                .ack(cx)?;
331            delivered += 1;
332        }
333        Ok(delivered)
334    }
335
336    fn poll_cron(&self, cx: &mut Cx) -> Result<u64> {
337        let Some(matcher) = &self.cron else {
338            return Ok(0);
339        };
340        let Some(minute_key) = matcher.current_match(SystemTime::now()) else {
341            return Ok(0);
342        };
343        {
344            let mut state = self
345                .state
346                .lock()
347                .map_err(|_| Error::PoisonedLock("trigger state"))?;
348            if state.last_cron_minute == Some(minute_key) {
349                return Ok(0);
350            }
351            state.last_cron_minute = Some(minute_key);
352        }
353        self.inject_event(cx, b"tick")?;
354        Ok(1)
355    }
356
357    fn inject_event(&self, cx: &mut Cx, raw: &[u8]) -> Result<()> {
358        require_source_capability(cx, &self.source)?;
359        let expr = self.decode_event(cx, raw)?;
360        let source = self.source.kind_symbol();
361        let when_ms = SystemTime::now()
362            .duration_since(UNIX_EPOCH)
363            .map(|duration| duration.as_millis() as u64)
364            .unwrap_or(0);
365        let mut frame = ServerFrame::from_expr(
366            cx,
367            self.codec.clone(),
368            crate::FrameKind::Trigger {
369                source: source.clone(),
370                when_ms,
371            },
372            &expr,
373            sim_kernel::Consistency::LocalFirst,
374            Vec::new(),
375            false,
376        )?;
377        frame.envelope.role = self.role.clone();
378        frame.envelope.trigger_source = Some(source);
379        self.server.deliver_trigger_frame(cx, frame)?;
380        self.state
381            .lock()
382            .map_err(|_| Error::PoisonedLock("trigger state"))?
383            .delivered += 1;
384        Ok(())
385    }
386
387    fn decode_event(&self, cx: &mut Cx, raw: &[u8]) -> Result<Expr> {
388        match &self.decoder {
389            TriggerDecoder::Codec(codec) => {
390                decode_frame_payload(cx, codec, raw, trigger_read_policy(cx), Default::default())
391            }
392            TriggerDecoder::Callable(callable) => {
393                let text = String::from_utf8(raw.to_vec()).map_err(|_| {
394                    Error::Eval(
395                        "trigger event bytes must be valid utf-8 for callable decoders".to_owned(),
396                    )
397                })?;
398                let arg = cx.factory().string(text)?;
399                let value = cx.call_value(callable.clone(), Args::new(vec![arg]))?;
400                value.object().as_expr(cx)
401            }
402        }
403    }
404
405    fn run(self: Arc<Self>, cx: &mut Cx) {
406        while !self.stopping.load(Ordering::Relaxed) {
407            match self.poll(cx) {
408                Ok(_) => {}
409                Err(_) => break,
410            }
411            let source_closed = self
412                .state
413                .lock()
414                .map(|state| state.source_closed)
415                .unwrap_or(true);
416            if source_closed {
417                break;
418            }
419            thread::sleep(self.poll_interval());
420        }
421    }
422
423    fn poll_interval(&self) -> Duration {
424        match self.source {
425            ServerAddress::Cron { .. } => Duration::from_millis(250),
426            _ => Duration::from_millis(25),
427        }
428    }
429}