1use std::{
2 sync::{
3 Arc, Mutex,
4 atomic::{AtomicBool, AtomicU64},
5 mpsc::Receiver,
6 },
7 thread::JoinHandle,
8 time::Duration,
9};
10
11use sim_citizen_derive::non_citizen;
12use sim_kernel::{
13 ClassRef, Cx, Error, Expr, Object, ReadPolicy, Result, Symbol, TrustLevel, Value,
14 read_construct_capability, read_eval_capability,
15};
16
17use crate::{Server, ServerAddress, cron::CronMatcher, ensure_installed_codec};
18
19mod runtime;
20mod source_options;
21mod sources;
22
23use source_options::source_loopback;
24use sources::{TriggerSource, build_network_source, require_source_capability, source_capability};
25#[cfg(test)]
26use sources::{loopback_smtp_messages, queued_trigger_events};
27
28static NEXT_TRIGGER_ID: AtomicU64 = AtomicU64::new(1);
29
30#[derive(Clone)]
31enum TriggerDecoder {
32 Codec(Symbol),
33 Callable(Value),
34}
35
36struct TriggerConfig {
37 source: ServerAddress,
38 source_expr: Expr,
39 role: Option<Symbol>,
40 codec: Symbol,
41 decode_expr: Expr,
42 decoder: TriggerDecoder,
43 cron: Option<CronMatcher>,
44 network_source: Option<Box<dyn TriggerSource>>,
45}
46
47#[derive(Default)]
48struct TriggerState {
49 file_offset: usize,
50 file_remainder: Vec<u8>,
51 delivered: u64,
52 source_closed: bool,
53 last_cron_minute: Option<u64>,
54}
55
56enum StdinSource {
57 Channel(Mutex<Receiver<Option<Vec<u8>>>>),
58 Unavailable,
59}
60
61#[non_citizen(
62 reason = "live trigger handle; reconstruct source through server/Address descriptor and trigger ops",
63 kind = "handle",
64 descriptor = "server/Address"
65)]
66pub struct TriggerHandle {
68 id: u64,
69 server: Arc<Server>,
70 source: ServerAddress,
71 source_expr: Expr,
72 role: Option<Symbol>,
73 codec: Symbol,
74 decode_expr: Expr,
75 decoder: TriggerDecoder,
76 cron: Option<CronMatcher>,
77 network_source: Option<Mutex<Box<dyn TriggerSource>>>,
78 stopping: AtomicBool,
79 handle: Mutex<Option<JoinHandle<()>>>,
80 stdin: StdinSource,
81 state: Mutex<TriggerState>,
82}
83
84#[cfg(test)]
85#[allow(dead_code)]
86pub(crate) fn enqueue_trigger_event(source: &ServerAddress, payload: Vec<u8>) -> Result<()> {
87 let key = match source {
88 ServerAddress::Webhook { route } => Some(format!("webhook:{route}")),
89 ServerAddress::Imap { address, mailbox } => Some(format!("imap:{address}:{mailbox}")),
90 ServerAddress::Smtp { address } => Some(format!("smtp:{address}")),
91 ServerAddress::Telegram { chat_id, bot } => Some(format!("telegram:{chat_id}:{bot}")),
92 ServerAddress::Matrix { room_id } => Some(format!("matrix:{room_id}")),
93 _ => None,
94 }
95 .ok_or_else(|| Error::Eval("source does not use the queued trigger runtime".to_owned()))?;
96 let mut queues = queued_trigger_events()
97 .lock()
98 .map_err(|_| Error::PoisonedLock("trigger queue"))?;
99 queues.entry(key).or_default().push(payload);
100 Ok(())
101}
102
103#[cfg(test)]
104pub(crate) fn loopback_smtp_messages_for(source: &ServerAddress) -> Result<Vec<Vec<u8>>> {
105 let ServerAddress::Smtp { address } = source else {
106 return Err(Error::Eval(
107 "source does not use the loopback smtp runtime".to_owned(),
108 ));
109 };
110 let key = format!("smtp:{address}");
111 let messages = loopback_smtp_messages()
112 .lock()
113 .map_err(|_| Error::PoisonedLock("loopback smtp messages"))?;
114 Ok(messages.get(&key).cloned().unwrap_or_default())
115}
116
117impl Object for TriggerHandle {
118 fn display(&self, _cx: &mut Cx) -> Result<String> {
119 Ok("#<server-trigger>".to_owned())
120 }
121
122 fn as_any(&self) -> &dyn std::any::Any {
123 self
124 }
125}
126
127impl sim_kernel::ObjectCompat for TriggerHandle {
128 fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
129 cx.factory().class_stub(
130 sim_kernel::ClassId(0),
131 Symbol::qualified("server", "Trigger"),
132 )
133 }
134 fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
135 self.reflect_value(cx)?.object().as_expr(cx)
136 }
137 fn as_table(&self, cx: &mut Cx) -> Result<Value> {
138 self.reflect_value(cx)
139 }
140}
141
142pub(crate) fn register_trigger(
143 cx: &mut Cx,
144 server: Arc<Server>,
145 source_expr: Expr,
146 decode_expr: Expr,
147 role: Option<Symbol>,
148 codec: Symbol,
149) -> Result<Arc<TriggerHandle>> {
150 let source_expr = literal_expr(&source_expr).clone();
151 let source = ServerAddress::from_expr(&source_expr)?;
152 require_source_capability(cx, &source)?;
153 let cron = match &source {
154 ServerAddress::Cron { spec } => Some(CronMatcher::parse(spec)?),
155 _ => None,
156 };
157 let network_source = build_network_source(&source, &source_expr)?;
158
159 ensure_installed_codec(cx, &codec)?;
160 let decoder = build_decoder(cx, &decode_expr)?;
161 let trigger = Arc::new(TriggerHandle::new(
162 server.clone(),
163 TriggerConfig {
164 source,
165 source_expr,
166 role,
167 codec,
168 decode_expr,
169 decoder,
170 cron,
171 network_source,
172 },
173 ));
174 server.register_trigger(trigger.clone())?;
175 trigger.start(cx)?;
176 Ok(trigger)
177}
178
179fn build_decoder(cx: &mut Cx, decode_expr: &Expr) -> Result<TriggerDecoder> {
180 let expr = literal_expr(decode_expr).clone();
181 if let Expr::Symbol(symbol) = &expr
182 && let Some(codec) = normalize_codec_symbol(cx, symbol)
183 {
184 ensure_installed_codec(cx, &codec)?;
185 return Ok(TriggerDecoder::Codec(codec));
186 }
187 let callable = cx.eval_expr(expr)?;
188 if callable.object().as_callable().is_none() {
189 return Err(Error::TypeMismatch {
190 expected: "callable or codec symbol",
191 found: "non-callable",
192 });
193 }
194 Ok(TriggerDecoder::Callable(callable))
195}
196
197fn normalize_codec_symbol(cx: &Cx, symbol: &Symbol) -> Option<Symbol> {
198 if cx.registry().codec_by_symbol(symbol).is_some() {
199 return Some(symbol.clone());
200 }
201 let qualified = Symbol::qualified("codec", symbol.name.to_string());
202 if cx.registry().codec_by_symbol(&qualified).is_some() {
203 return Some(qualified);
204 }
205 None
206}
207
208fn literal_expr(expr: &Expr) -> &Expr {
209 match expr {
210 Expr::Quote { expr, .. } => expr,
211 _ => expr,
212 }
213}
214
215fn trigger_read_policy(cx: &Cx) -> ReadPolicy {
216 let mut capabilities = sim_kernel::CapabilitySet::new();
217 if cx.require(&read_construct_capability()).is_ok() {
218 capabilities.insert(read_construct_capability());
219 }
220 if cx.require(&read_eval_capability()).is_ok() {
221 capabilities.insert(read_eval_capability());
222 }
223 ReadPolicy {
224 trust: TrustLevel::TrustedSource,
225 capabilities,
226 }
227}
228
229fn io_error_to_host(err: std::io::Error) -> Error {
230 Error::host_io(err)
231}
232
233fn delivery_timeout() -> Duration {
234 Duration::from_millis(250)
235}
236
237fn source_loopback_enabled(expr: &Expr) -> bool {
238 source_loopback(expr).unwrap_or(false)
239}