Skip to main content

sim_lib_server/
trigger.rs

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)]
66/// Live handle to a running trigger that delivers source events into a server.
67pub 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}