1use bstr::ByteSlice;
2use config::{
3 any_err, decorate_callback_name, from_lua_value, get_or_create_module, load_config,
4 serialize_options, CallbackSignature,
5};
6use kumo_server_runtime::available_parallelism;
7use mlua::{Function, Lua, LuaSerdeExt, Value, Variadic};
8use mod_redis::RedisConnKey;
9use serde::{Deserialize, Serialize};
10use std::sync::atomic::AtomicUsize;
11
12pub mod acct;
13pub mod authn_authz;
14pub mod config_handle;
15pub mod diagnostic_logging;
16pub mod disk_space;
17pub mod hashable_weak;
18pub mod http_server;
19pub mod log;
20pub mod log_backlog;
21pub mod nodeid;
22pub mod panic;
23pub mod start;
24pub mod tls_helpers;
25
26pub fn register(lua: &Lua) -> anyhow::Result<()> {
27 for func in [
28 mod_redis::register,
29 data_loader::register,
30 mod_digest::register,
31 mod_encode::register,
32 mod_aws_sigv4::register,
33 cidr_map::register,
34 domain_map::register,
35 mod_amqp::register,
36 mod_filesystem::register,
37 mod_file_type::register,
38 mod_http::register,
39 mod_regex::register,
40 mod_serde::register,
41 mod_sqlite::register,
42 mod_crypto::register,
43 mod_smtp_response_normalize::register,
44 kumo_jsonl::lua::register,
45 mod_counter_series::register,
46 mod_string::register,
47 mod_time::register,
48 mod_dns_resolver::register,
49 mod_kafka::register,
50 mod_memoize::register,
51 mod_mimepart::register,
52 mod_mpsc::register,
53 mod_nats::register,
54 mod_uuid::register,
55 kumo_api_types::shaping::register,
56 regex_set_map::register,
57 crate::authn_authz::register,
58 ] {
59 func(lua)?;
60 }
61
62 let kumo_mod = get_or_create_module(lua, "kumo")?;
63 kumo_mod.set("version", version_info::kumo_version())?;
64
65 fn event_registrar_name(name: &str) -> String {
66 format!("kumomta-event-registrars-{name}")
67 }
68
69 fn register_event_caller(lua: &Lua, name: &str) -> mlua::Result<()> {
72 let decorated_name = event_registrar_name(name);
73 let mut call_stack = vec![];
74 for n in 1.. {
75 match lua.inspect_stack(n, |info| {
76 let source = info.source();
77 format!(
78 "{}:{}",
79 source
80 .short_src
81 .as_ref()
82 .map(|b| b.to_string())
83 .unwrap_or_else(String::new),
84 info.current_line().unwrap_or(0)
85 )
86 }) {
87 Some(info) => {
88 call_stack.push(info);
89 }
90 None => break,
91 }
92 }
93
94 let tbl: Value = lua.named_registry_value(&decorated_name)?;
95 match tbl {
96 Value::Nil => {
97 let tbl = lua.create_table()?;
98 tbl.set(1, call_stack)?;
99 lua.set_named_registry_value(&decorated_name, tbl)?;
100 Ok(())
101 }
102 Value::Table(tbl) => {
103 let len = tbl.raw_len();
104 tbl.set(len + 1, call_stack)?;
105 Ok(())
106 }
107 _ => Err(mlua::Error::external(format!(
108 "registry key for {decorated_name} has invalid type",
109 ))),
110 }
111 }
112
113 kumo_mod.set(
116 "get_event_registrars",
117 lua.create_function(move |lua, name: String| {
118 let decorated_name = event_registrar_name(&name);
119 let value: Value = lua.named_registry_value(&decorated_name)?;
120 Ok(value)
121 })?,
122 )?;
123
124 kumo_mod.set(
125 "on",
126 lua.create_function(move |lua, (name, func): (String, Function)| {
127 let decorated_name = decorate_callback_name(&name);
128
129 if let Ok(current_event) = lua.globals().get::<String>("_KUMO_CURRENT_EVENT") {
130 if current_event != "main" {
131 return Err(mlua::Error::external(format!(
132 "Attempting to register an event handler via \
133 `kumo.on('{name}', ...)` from within the event handler \
134 '{current_event}'. You must move your event handler registration \
135 so that it is setup directly when the policy is loaded \
136 in order for it to consistently trigger and handle events."
137 )));
138 }
139 }
140
141 register_event_caller(lua, &name)?;
142
143 if config::does_callback_allow_multiple(&name) {
144 let tbl: Value = lua.named_registry_value(&decorated_name)?;
145 return match tbl {
146 Value::Nil => {
147 let tbl = lua.create_table()?;
148 tbl.set(1, func)?;
149 lua.set_named_registry_value(&decorated_name, tbl)?;
150 Ok(())
151 }
152 Value::Table(tbl) => {
153 let len = tbl.raw_len();
154 tbl.set(len + 1, func)?;
155 Ok(())
156 }
157 _ => Err(mlua::Error::external(format!(
158 "registry key for {decorated_name} has invalid type",
159 ))),
160 };
161 }
162
163 let existing: Value = lua.named_registry_value(&decorated_name)?;
164 match existing {
165 Value::Nil => {}
166 Value::Function(func) => {
167 let info = func.info();
168 let src = info.source.unwrap_or_else(|| "?".into());
169 let line = info.line_defined.unwrap_or(0);
170 return Err(mlua::Error::external(format!(
171 "{name} event already has a handler defined at {src}:{line}"
172 )));
173 }
174 _ => {
175 return Err(mlua::Error::external(format!(
176 "{name} event already has a handler"
177 )));
178 }
179 }
180
181 lua.set_named_registry_value(&decorated_name, func)?;
182 Ok(())
183 })?,
184 )?;
185
186 kumo_mod.set(
187 "set_diagnostic_log_filter",
188 lua.create_function(move |_, filter: String| {
189 diagnostic_logging::set_diagnostic_log_filter(&filter).map_err(any_err)
190 })?,
191 )?;
192
193 fn variadic_to_string(args: Variadic<Value>) -> String {
194 let mut output = String::new();
195 for (idx, item) in args.into_iter().enumerate() {
196 if idx > 0 {
197 output.push(' ');
198 }
199
200 match item {
201 Value::String(s) => {
202 let bytes = s.as_bytes();
203 for (start, end, c) in bytes.char_indices() {
204 if c == std::char::REPLACEMENT_CHARACTER {
205 let c_slice = &bytes[start..end];
206 for &b in c_slice.iter() {
207 output.push_str(&format!("\\x{b:02X}"));
208 }
209 } else {
210 output.push(c);
211 }
212 }
213 }
214 item => match item.to_string() {
215 Ok(s) => output.push_str(&s),
216 Err(_) => output.push_str(&format!("{item:?}")),
217 },
218 }
219 }
220 output
221 }
222
223 fn get_caller(lua: &Lua) -> String {
224 match lua.inspect_stack(1, |info| {
225 let source = info.source();
226 let file_name = source
227 .short_src
228 .as_ref()
229 .map(|b| b.to_string())
230 .unwrap_or_else(String::new);
231 let file_name = match file_name.strip_prefix("[string \"") {
234 Some(name) => name.strip_suffix("\"]").unwrap_or(name),
235 None => &file_name,
236 };
237
238 format!("{file_name}:{}", info.current_line().unwrap_or(0))
239 }) {
240 Some(info) => info,
241 None => "?".to_string(),
242 }
243 }
244
245 kumo_mod.set(
246 "log_error",
247 lua.create_function(move |lua, args: Variadic<Value>| {
248 if tracing::event_enabled!(target: "lua", tracing::Level::ERROR) {
249 let src = get_caller(lua);
250 tracing::error!(target: "lua", "{src}: {}", variadic_to_string(args));
251 }
252 Ok(())
253 })?,
254 )?;
255 kumo_mod.set(
256 "log_info",
257 lua.create_function(move |lua, args: Variadic<Value>| {
258 if tracing::event_enabled!(target: "lua", tracing::Level::INFO) {
259 let src = get_caller(lua);
260 tracing::info!(target: "lua", "{src}: {}", variadic_to_string(args));
261 }
262 Ok(())
263 })?,
264 )?;
265 kumo_mod.set(
266 "log_warn",
267 lua.create_function(move |lua, args: Variadic<Value>| {
268 if tracing::event_enabled!(target: "lua", tracing::Level::WARN) {
269 let src = get_caller(lua);
270 tracing::warn!(target: "lua", "{src}: {}", variadic_to_string(args));
271 }
272 Ok(())
273 })?,
274 )?;
275 kumo_mod.set(
276 "log_debug",
277 lua.create_function(move |lua, args: Variadic<Value>| {
278 if tracing::event_enabled!(target: "lua", tracing::Level::DEBUG) {
279 let src = get_caller(lua);
280 tracing::debug!(target: "lua", "{src}: {}", variadic_to_string(args));
281 }
282 Ok(())
283 })?,
284 )?;
285
286 kumo_mod.set(
287 "set_max_spare_lua_contexts",
288 lua.create_function(move |_, limit: usize| {
289 config::set_max_spare(limit);
290 Ok(())
291 })?,
292 )?;
293
294 kumo_mod.set(
295 "set_max_lua_context_use_count",
296 lua.create_function(move |_, limit: usize| {
297 config::set_max_use(limit);
298 Ok(())
299 })?,
300 )?;
301
302 kumo_mod.set(
303 "set_max_lua_context_age",
304 lua.create_function(move |_, limit: usize| {
305 config::set_max_age(limit);
306 Ok(())
307 })?,
308 )?;
309
310 kumo_mod.set(
311 "set_lua_gc_on_put",
312 lua.create_function(move |_, enable: u8| {
313 config::set_gc_on_put(enable);
314 Ok(())
315 })?,
316 )?;
317
318 kumo_mod.set(
319 "set_lruttl_cache_capacity",
320 lua.create_function(move |_, (name, capacity): (String, usize)| {
321 if lruttl::set_cache_capacity(&name, capacity) {
322 Ok(())
323 } else {
324 Err(mlua::Error::external(format!(
325 "could not set capacity for cache {name} \
326 as that is not a pre-defined lruttl cache name"
327 )))
328 }
329 })?,
330 )?;
331
332 kumo_mod.set(
333 "set_config_monitor_globs",
334 lua.create_function(move |_, globs: Vec<String>| {
335 config::epoch::set_globs(globs).map_err(any_err)?;
336 Ok(())
337 })?,
338 )?;
339 kumo_mod.set(
340 "eval_config_monitor_globs",
341 lua.create_async_function(|_, _: ()| async move {
342 config::epoch::eval_globs().await.map_err(any_err)
343 })?,
344 )?;
345 kumo_mod.set(
346 "bump_config_epoch",
347 lua.create_function(move |_, _: ()| {
348 config::epoch::bump_current_epoch();
349 Ok(())
350 })?,
351 )?;
352
353 kumo_mod.set(
354 "available_parallelism",
355 lua.create_function(move |_, _: ()| available_parallelism().map_err(any_err))?,
356 )?;
357
358 kumo_mod.set(
359 "set_memory_hard_limit",
360 lua.create_function(move |_, limit: usize| {
361 kumo_server_memory::set_hard_limit(limit);
362 Ok(())
363 })?,
364 )?;
365
366 kumo_mod.set(
367 "set_memory_low_thresh",
368 lua.create_function(move |_, limit: usize| {
369 kumo_server_memory::set_low_memory_thresh(limit);
370 Ok(())
371 })?,
372 )?;
373
374 kumo_mod.set(
375 "set_memory_soft_limit",
376 lua.create_function(move |_, limit: usize| {
377 kumo_server_memory::set_soft_limit(limit);
378 Ok(())
379 })?,
380 )?;
381
382 kumo_mod.set(
383 "get_memory_hard_limit",
384 lua.create_function(move |_, _: ()| Ok(kumo_server_memory::get_hard_limit()))?,
385 )?;
386
387 kumo_mod.set(
388 "get_memory_soft_limit",
389 lua.create_function(move |_, _: ()| Ok(kumo_server_memory::get_soft_limit()))?,
390 )?;
391
392 kumo_mod.set(
393 "get_memory_low_thresh",
394 lua.create_function(move |_, _: ()| Ok(kumo_server_memory::get_low_memory_thresh()))?,
395 )?;
396
397 kumo_mod.set(
398 "configure_redis_throttles",
399 lua.create_async_function(|lua, params: Value| async move {
400 let key: RedisConnKey = from_lua_value(&lua, params)?;
401 let conn = key.open().map_err(any_err)?;
402 conn.ping().await.map_err(any_err)?;
403 throttle::use_redis(conn).await.map_err(any_err)
404 })?,
405 )?;
406
407 kumo_mod.set(
408 "traceback",
409 lua.create_function(move |lua: &Lua, level: usize| {
410 #[derive(Debug, Serialize)]
411 struct Frame {
412 event: String,
413 name: Option<String>,
414 name_what: Option<String>,
415 source: Option<String>,
416 short_src: Option<String>,
417 line_defined: Option<usize>,
418 last_line_defined: Option<usize>,
419 what: &'static str,
420 curr_line: Option<usize>,
421 is_tail_call: bool,
422 }
423
424 let mut frames = vec![];
425 for n in level.. {
426 match lua.inspect_stack(n, |info| {
427 let source = info.source();
428 let names = info.names();
429 Frame {
430 curr_line: info.current_line(),
431 is_tail_call: info.is_tail_call(),
432 event: format!("{:?}", info.event()),
433 last_line_defined: source.last_line_defined,
434 line_defined: source.line_defined,
435 name: names.name.as_ref().map(|b| b.to_string()),
436 name_what: names.name_what.as_ref().map(|b| b.to_string()),
437 source: source.source.as_ref().map(|b| b.to_string()),
438 short_src: source.short_src.as_ref().map(|b| b.to_string()),
439 what: source.what,
440 }
441 }) {
442 Some(frame) => {
443 frames.push(frame);
444 }
445 None => break,
446 }
447 }
448
449 lua.to_value(&frames)
450 })?,
451 )?;
452
453 #[derive(Deserialize, Debug)]
456 struct TaskParams {
457 event_name: String,
458 args: Vec<serde_json::Value>,
459 }
460
461 impl TaskParams {
462 async fn run(&self) -> anyhow::Result<()> {
463 let mut config = load_config().await?;
464
465 let sig = CallbackSignature::<Value, ()>::new(self.event_name.to_string());
466
467 config
468 .convert_args_and_call_callback(&sig, &self.args)
469 .await?;
470
471 config.put();
472
473 Ok(())
474 }
475 }
476
477 kumo_mod.set(
478 "spawn_task",
479 lua.create_function(|lua, params: Value| {
480 let params: TaskParams = lua.from_value(params)?;
481
482 if !config::is_validating() {
483 std::thread::Builder::new()
484 .name(format!("spawned-task-{}", params.event_name))
485 .spawn(move || {
486 let runtime = tokio::runtime::Builder::new_current_thread()
487 .enable_io()
488 .enable_time()
489 .on_thread_park(kumo_server_memory::purge_thread_cache)
490 .build()
491 .unwrap();
492 let event_name = params.event_name.clone();
493
494 let result = runtime.block_on(async move { params.run().await });
495 if let Err(err) = result {
496 tracing::error!("Error while dispatching {event_name}: {err:#}");
497 }
498 })?;
499 }
500
501 Ok(())
502 })?,
503 )?;
504
505 kumo_mod.set(
506 "spawn_thread_pool",
507 lua.create_function(|lua, params: Value| {
508 #[derive(Deserialize, Debug)]
509 struct ThreadPoolParams {
510 name: String,
511 num_threads: usize,
512 }
513
514 let params: ThreadPoolParams = lua.from_value(params)?;
515 let num_threads = AtomicUsize::new(params.num_threads);
516
517 if !config::is_validating() {
518 let _runtime = kumo_server_runtime::Runtime::new(
522 ¶ms.name,
523 |_| params.num_threads,
524 &num_threads,
525 )
526 .map_err(any_err)?;
527 }
528
529 Ok(())
530 })?,
531 )?;
532
533 kumo_mod.set(
534 "validation_failed",
535 lua.create_function(|_, ()| {
536 config::set_validation_failed();
537 Ok(())
538 })?,
539 )?;
540
541 kumo_mod.set(
542 "enable_memory_callstack_tracking",
543 lua.create_function(|_, enable: bool| {
544 kumo_server_memory::set_tracking_callstacks(enable);
545 Ok(())
546 })?,
547 )?;
548
549 kumo_mod.set(
553 "prometheus_metrics",
554 lua.create_async_function(|lua, ()| async move {
555 use tokio_stream::StreamExt;
556 let mut json_text = String::new();
557 let mut stream = kumo_prometheus::registry::Registry::stream_json();
558 while let Some(text) = stream.next().await {
559 json_text.push_str(&text);
560 }
561 let value: serde_json::Value = serde_json::from_str(&json_text).map_err(any_err)?;
562 lua.to_value_with(&value, serialize_options())
563 })?,
564 )?;
565
566 Ok(())
567}