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 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}