1use std::{
2 fs,
3 path::PathBuf,
4 sync::{
5 Arc, Mutex,
6 atomic::{AtomicBool, Ordering},
7 mpsc,
8 },
9 thread,
10 time::Duration,
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 wall_clock = server.wall_clock().clone();
44 let stdin = match &source {
45 ServerAddress::Stdin => {
46 let (tx, rx) = mpsc::channel();
47 #[cfg(not(test))]
48 spawn_stdin_reader(tx);
49 #[cfg(test)]
50 {
51 test_stdin_senders()
52 .lock()
53 .expect("test stdin sender registry poisoned")
54 .insert(id, tx);
55 }
56 StdinSource::Channel(Mutex::new(rx))
57 }
58 _ => StdinSource::Unavailable,
59 };
60 Self {
61 id,
62 server,
63 source,
64 source_expr,
65 role,
66 codec,
67 decode_expr,
68 decoder,
69 cron,
70 network_source: network_source.map(Mutex::new),
71 wall_clock,
72 stopping: AtomicBool::new(false),
73 handle: Mutex::new(None),
74 stdin,
75 state: Mutex::new(TriggerState::default()),
76 }
77 }
78
79 pub(crate) fn start(self: &Arc<Self>, seed: &Cx) -> Result<()> {
80 let mut handle = self
81 .handle
82 .lock()
83 .map_err(|_| Error::PoisonedLock("trigger handle"))?;
84 if handle.is_some() {
85 return Ok(());
86 }
87 let mut cx = clone_server_cx(seed);
88 let trigger = Arc::clone(self);
89 *handle = Some(thread::spawn(move || {
90 trigger.run(&mut cx);
91 }));
92 Ok(())
93 }
94
95 pub(crate) fn stop(&self) -> Result<()> {
96 self.stopping.store(true, Ordering::Relaxed);
97 #[cfg(test)]
98 remove_test_stdin_sender(self.id);
99 let join = self
100 .handle
101 .lock()
102 .map_err(|_| Error::PoisonedLock("trigger handle"))?
103 .take();
104 if let Some(join) = join {
105 join.join()
106 .map_err(|_| Error::HostError("trigger thread panicked".to_owned()))?;
107 }
108 Ok(())
109 }
110
111 pub(crate) fn poll(&self, cx: &mut Cx) -> Result<u64> {
112 match &self.source {
113 ServerAddress::Stdin => self.poll_stdin(cx),
114 ServerAddress::FileTail { path } => self.poll_file_tail(cx, path),
115 ServerAddress::Cron { .. } => self.poll_cron(cx),
116 ServerAddress::Webhook { .. }
117 | ServerAddress::Imap { .. }
118 | ServerAddress::Smtp { .. }
119 | ServerAddress::Telegram { .. }
120 | ServerAddress::Matrix { .. } => self.poll_network_source(cx),
121 other => Err(Error::Eval(format!(
122 "server/trigger does not support source {}",
123 other.kind_symbol()
124 ))),
125 }
126 }
127
128 #[cfg(test)]
129 pub(crate) fn inject_text(&self, cx: &mut Cx, text: &str) -> Result<u64> {
130 let mut delivered = 0;
131 for line in text.lines() {
132 if line.trim().is_empty() {
133 continue;
134 }
135 self.inject_event(cx, line.as_bytes())?;
136 delivered += 1;
137 }
138 Ok(delivered)
139 }
140
141 #[cfg(test)]
142 pub(crate) fn feed_stdin(&self, text: &str) -> Result<()> {
143 let sender = test_stdin_sender(self.id)?;
144 for line in text.lines() {
145 sender
146 .send(Some(format!("{line}\n").into_bytes()))
147 .map_err(|_| Error::HostError("stdin trigger source closed".to_owned()))?;
148 }
149 Ok(())
150 }
151
152 #[cfg(test)]
153 pub(crate) fn finish_stdin(&self) -> Result<()> {
154 test_stdin_sender(self.id)?
155 .send(None)
156 .map_err(|_| Error::HostError("stdin trigger source closed".to_owned()))
157 }
158
159 #[cfg(test)]
160 pub(crate) fn is_source_closed(&self) -> bool {
161 self.state
162 .lock()
163 .map(|state| state.source_closed)
164 .unwrap_or(true)
165 }
166
167 #[cfg(all(test, feature = "trigger-webhook"))]
168 pub(crate) fn webhook_port(&self) -> Result<Option<u16>> {
169 let Some(source) = &self.network_source else {
170 return Ok(None);
171 };
172 let source = source
173 .lock()
174 .map_err(|_| Error::PoisonedLock("trigger source"))?;
175 let Some(source) = source
176 .as_any()
177 .downcast_ref::<super::sources::network::WebhookSource>()
178 else {
179 return Ok(None);
180 };
181 source.local_port().map(Some)
182 }
183
184 pub fn reflect_value(&self, cx: &mut Cx) -> Result<Value> {
189 let role = match &self.role {
190 Some(role) => cx.factory().symbol(role.clone())?,
191 None => cx.factory().nil()?,
192 };
193 let delivered = self
194 .state
195 .lock()
196 .map_err(|_| Error::PoisonedLock("trigger state"))?
197 .delivered;
198 let capabilities = match source_capability(&self.source) {
199 Some(capability) => symbol_list_value(cx, &[capability.as_symbol()])?,
200 None => cx.factory().list(Vec::new())?,
201 };
202 cx.factory().table(vec![
203 (
204 Symbol::new("kind"),
205 cx.factory().symbol(Symbol::new("trigger"))?,
206 ),
207 (Symbol::new("id"), cx.factory().string(self.id.to_string())?),
208 (
209 Symbol::new("source"),
210 cx.factory().expr(self.source_expr.clone())?,
211 ),
212 (Symbol::new("role"), role),
213 (
214 Symbol::new("codec"),
215 cx.factory().symbol(self.codec.clone())?,
216 ),
217 (
218 Symbol::new("decode"),
219 cx.factory().expr(self.decode_expr.clone())?,
220 ),
221 (
222 Symbol::new("delivered"),
223 cx.factory().string(delivered.to_string())?,
224 ),
225 (Symbol::new("requires"), capabilities),
226 ])
227 }
228
229 fn poll_file_tail(&self, cx: &mut Cx, path: &PathBuf) -> Result<u64> {
230 let mut bytes = fs::read(path).map_err(io_error_to_host)?;
231 let lines = {
232 let mut state = self
233 .state
234 .lock()
235 .map_err(|_| Error::PoisonedLock("trigger state"))?;
236 if state.file_offset > bytes.len() {
237 state.file_offset = 0;
238 state.file_remainder.clear();
239 }
240 let mut pending = std::mem::take(&mut state.file_remainder);
241 pending.extend_from_slice(&bytes[state.file_offset..]);
242 state.file_offset = bytes.len();
243
244 let mut lines = Vec::new();
245 let mut start = 0usize;
246 for index in 0..pending.len() {
247 if pending[index] == b'\n' {
248 let mut line = pending[start..index].to_vec();
249 if line.last() == Some(&b'\r') {
250 line.pop();
251 }
252 lines.push(line);
253 start = index + 1;
254 }
255 }
256 state.file_remainder = pending[start..].to_vec();
257 lines
258 };
259 bytes.clear();
260
261 let mut delivered = 0;
262 for line in lines {
263 if line.iter().all(u8::is_ascii_whitespace) {
264 continue;
265 }
266 self.inject_event(cx, &line)?;
267 delivered += 1;
268 }
269 Ok(delivered)
270 }
271
272 fn poll_stdin(&self, cx: &mut Cx) -> Result<u64> {
273 let receiver = match &self.stdin {
274 StdinSource::Channel(receiver) => receiver,
275 StdinSource::Unavailable => return Ok(0),
276 };
277 let mut delivered = 0;
278 loop {
279 match receiver
280 .lock()
281 .map_err(|_| Error::PoisonedLock("stdin trigger source"))?
282 .try_recv()
283 {
284 Ok(Some(mut line)) => {
285 if line.last() == Some(&b'\n') {
286 line.pop();
287 }
288 if line.last() == Some(&b'\r') {
289 line.pop();
290 }
291 if line.iter().all(u8::is_ascii_whitespace) {
292 continue;
293 }
294 self.inject_event(cx, &line)?;
295 delivered += 1;
296 }
297 Ok(None) | Err(mpsc::TryRecvError::Disconnected) => {
298 self.state
299 .lock()
300 .map_err(|_| Error::PoisonedLock("trigger state"))?
301 .source_closed = true;
302 break;
303 }
304 Err(mpsc::TryRecvError::Empty) => break,
305 }
306 }
307 Ok(delivered)
308 }
309
310 fn poll_network_source(&self, cx: &mut Cx) -> Result<u64> {
311 let Some(source) = &self.network_source else {
312 return Ok(0);
313 };
314 let mut delivered = 0;
315 loop {
316 let timeout = if delivered == 0 {
317 delivery_timeout()
318 } else {
319 Duration::from_millis(0)
320 };
321 let event = source
322 .lock()
323 .map_err(|_| Error::PoisonedLock("trigger source"))?
324 .next_event(cx, timeout)?;
325 let Some(event) = event else {
326 break;
327 };
328 self.inject_event(cx, &event)?;
329 source
330 .lock()
331 .map_err(|_| Error::PoisonedLock("trigger source"))?
332 .ack(cx)?;
333 delivered += 1;
334 }
335 Ok(delivered)
336 }
337
338 fn poll_cron(&self, cx: &mut Cx) -> Result<u64> {
339 let Some(matcher) = &self.cron else {
340 return Ok(0);
341 };
342 let now = self.wall_clock.now()?;
343 let current_minute = now.unix_millis() / 60_000;
344 {
345 let mut state = self
346 .state
347 .lock()
348 .map_err(|_| Error::PoisonedLock("trigger state"))?;
349 if !state.advance_cron_high_watermark(current_minute) {
350 return Ok(0);
351 }
352 }
353 let Some(_minute_key) = matcher.current_match(now) else {
354 return Ok(0);
355 };
356 self.inject_event(cx, b"tick")?;
357 Ok(1)
358 }
359
360 fn inject_event(&self, cx: &mut Cx, raw: &[u8]) -> Result<()> {
361 require_source_capability(cx, &self.source)?;
362 let expr = self.decode_event(cx, raw)?;
363 let source = self.source.kind_symbol();
364 let when_ms = self.wall_clock.now()?.unix_millis();
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}