use std::sync::{Arc, Once, OnceLock};
use std::time::Duration;
use ikigai_core::{Fallback, Request, Resolution, Scope, Space};
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(300);
pub const DOORS: &[&str] = &["urn:lisp:eval", "urn:lisp:run"];
pub const KEYS: &[&str] = &[
"lisp.workers",
"lisp.worker_stack_bytes",
"lisp.max_program_bytes",
"lisp.max_input_bytes",
"lisp.max_nesting",
"lisp.timeout",
];
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Settings {
pub limits: ikigai_lisp::Limits,
pub timeout: Option<Duration>,
}
pub fn settings_from(entries: &[(String, String)]) -> Result<Settings, String> {
let mut limits = ikigai_lisp::Limits::default();
let mut timeout = None;
let mut seen: Vec<&str> = Vec::new();
for (key, value) in entries {
if seen.contains(&key.as_str()) {
return Err(format!("`{key}` is set twice; set it once"));
}
seen.push(key);
let number = whole_number(key, value)?;
let size =
|| usize::try_from(number).map_err(|_| format!("`{key} = {value}` is too large here"));
match key.as_str() {
"lisp.workers" => limits = limits.workers(size()?),
"lisp.worker_stack_bytes" => limits = limits.worker_stack_bytes(size()?),
"lisp.max_program_bytes" => limits = limits.max_program_bytes(size()?),
"lisp.max_input_bytes" => limits = limits.max_input_bytes(size()?),
"lisp.max_nesting" => limits = limits.max_nesting(size()?),
"lisp.timeout" => timeout = Some(Duration::from_secs(number)),
_ => {
return Err(format!(
"`{key}` is not a Lisp setting; the keys are {}",
KEYS.join(", ")
))
}
}
}
Ok(Settings { limits, timeout })
}
fn whole_number(key: &str, value: &str) -> Result<u64, String> {
let digits: String = value.chars().filter(|c| *c != '_').collect();
if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
return Err(format!(
"`{key} = {value}` is not a whole number (the value is a count of {})",
if key == "lisp.timeout" {
"seconds"
} else if key.ends_with("bytes") {
"bytes"
} else if key == "lisp.workers" {
"threads"
} else {
"levels"
}
));
}
match digits.parse::<u64>() {
Ok(0) => Err(format!("`{key} = {value}`: the least it can be is 1")),
Ok(n) => Ok(n),
Err(_) => Err(format!("`{key} = {value}` is too large")),
}
}
fn settings() -> &'static Result<Settings, String> {
static SETTINGS: OnceLock<Result<Settings, String>> = OnceLock::new();
SETTINGS.get_or_init(|| {
settings_from(&crate::config::entries_under("lisp.")).map_err(|e| {
format!(
"{e} (in {}; see the `lisp.*` keys in ikigai-cli's README)",
crate::config::config_path().display()
)
})
})
}
pub fn configure() -> Result<(), String> {
let settings = settings().as_ref().map_err(Clone::clone)?;
match ikigai_lisp::set_limits(settings.limits.clone()) {
Ok(()) => Ok(()),
Err(fixed) if fixed == settings.limits => Ok(()),
Err(fixed) => Err(format!(
"the Lisp bounds were fixed before the config home was read ({fixed:?}), so the \
`lisp.*` keys cannot take effect; configure them before the first evaluation"
)),
}
}
pub fn configured_timeout() -> Option<Duration> {
settings().as_ref().ok().and_then(|s| s.timeout)
}
pub fn host_timeout() -> Duration {
if let Err(e) = configure() {
static WARNED: Once = Once::new();
WARNED.call_once(|| eprintln!("ikigai: {e}; the Lisp defaults stand"));
}
configured_timeout().unwrap_or(DEFAULT_TIMEOUT)
}
pub fn govern(root: Arc<dyn Space>, budget: Duration, doors: Vec<String>) -> Arc<dyn Space> {
let only = Only {
inner: Arc::clone(&root),
doors,
};
Arc::new(Fallback::new(vec![
Arc::new(ikigai_throttle::Timeout::new(only, budget)) as Arc<dyn Space>,
root,
]))
}
struct Only {
inner: Arc<dyn Space>,
doors: Vec<String>,
}
impl Space for Only {
fn resolve(&self, request: &Request, scope: &Scope) -> Resolution {
let target = request.target.as_str();
let name = target.split(['?', '#']).next().unwrap_or(target);
if self.doors.iter().any(|door| door == name) {
self.inner.resolve(request, scope)
} else {
Resolution::Miss
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ikigai_core::{ArgRef, Capability, EndpointSpace, Error, Exact, Iri, Kernel, Verb};
fn entries(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn every_key_maps_to_its_bound() {
let settings = settings_from(&entries(&[
("lisp.workers", "3"),
("lisp.worker_stack_bytes", "67_108_864"),
("lisp.max_program_bytes", "1024"),
("lisp.max_input_bytes", "2048"),
("lisp.max_nesting", "200"),
("lisp.timeout", "45"),
]))
.expect("valid settings");
let expected = ikigai_lisp::Limits::default()
.workers(3)
.worker_stack_bytes(64 * 1024 * 1024)
.max_program_bytes(1024)
.max_input_bytes(2048)
.max_nesting(200);
assert_eq!(settings.limits, expected);
assert_eq!(settings.timeout, Some(Duration::from_secs(45)));
}
#[test]
fn no_keys_means_ikigai_lisps_defaults_and_no_timeout() {
let settings = settings_from(&[]).expect("empty is valid");
assert_eq!(settings.limits, ikigai_lisp::Limits::default());
assert_eq!(settings.timeout, None);
}
#[test]
fn a_bad_line_is_refused_naming_the_key() {
for (key, value, says) in [
("lisp.workers", "0", "least it can be is 1"),
("lisp.timeout", "0", "least it can be is 1"),
("lisp.timeout", "10s", "seconds"),
("lisp.max_nesting", "-5", "not a whole number"),
("lisp.max_input_bytes", "1MiB", "bytes"),
("lisp.worker_stack_bytes", "", "not a whole number"),
(
"lisp.max_program_bytes",
"99999999999999999999999",
"too large",
),
("lisp.worker_stack", "1024", "not a Lisp setting"),
] {
let error = settings_from(&entries(&[(key, value)]))
.expect_err(&format!("`{key} = {value}` is refused"));
assert!(error.contains(key), "{error}");
assert!(error.contains(says), "`{key} = {value}`: {error}");
}
let twice = settings_from(&entries(&[("lisp.timeout", "5"), ("lisp.timeout", "6")]))
.expect_err("a key given twice is refused");
assert!(twice.contains("set twice"), "{twice}");
}
#[test]
fn the_config_scanner_hands_over_every_lisp_line_and_nothing_else() {
let text = "lisp.timeout = 300\n# lisp.workers = 2\nmail.from = \"a@b\"\n\
lisp.max_nesting = \"500\"\nbug.lisp.timeout = 9\n";
assert_eq!(
crate::config::entries_under_in(text, "lisp."),
entries(&[("lisp.timeout", "300"), ("lisp.max_nesting", "500")])
);
}
fn eval(kernel: &Kernel, program: &str) -> Result<String, Error> {
let request = Request::new(Verb::Source, Iri::parse("urn:lisp:eval").expect("an IRI"))
.with_arg("in", ArgRef::Inline(program.as_bytes().to_vec()));
futures::executor::block_on(kernel.issue(request, &Capability::root()))
.map(|r| String::from_utf8_lossy(&r.bytes).into_owned())
}
#[test]
#[allow(clippy::disallowed_methods)]
fn the_governor_interrupts_a_runaway_and_the_evaluator_survives_it() {
let root: Arc<dyn Space> =
Arc::new(EndpointSpace::new().bind(Exact::new("urn:lisp:eval"), ikigai_lisp::eval()));
let budget = Duration::from_secs(3);
let kernel = Kernel::new(govern(root, budget, vec!["urn:lisp:eval".to_string()]));
let started = std::time::Instant::now();
let runaway = eval(&kernel, "(define (spin n) (spin (+ n 1))) (spin 0)");
let took = started.elapsed();
match runaway {
Err(Error::Timeout(detail)) => assert!(detail.contains("urn:lisp:eval"), "{detail}"),
other => panic!("a runaway is stopped with a Timeout, got {other:?}"),
}
assert!(
took < Duration::from_secs(20),
"stopped near its {budget:?} budget, not by something else: {took:?}"
);
let plain = Kernel::new(Arc::new(
EndpointSpace::new().bind(Exact::new("urn:lisp:eval"), ikigai_lisp::eval()),
));
for _ in 0..3 {
assert_eq!(
eval(&plain, "(+ 1 2)").expect("the evaluator survives"),
"3"
);
}
}
#[test]
fn only_the_named_doors_are_governed() {
let root: Arc<dyn Space> = Arc::new(
EndpointSpace::new()
.bind(Exact::new("urn:lisp:eval"), ikigai_lisp::eval())
.bind(
Exact::new("urn:other"),
ikigai_lisp::program("other", "(+ 40 2)"),
),
);
let only = Only {
inner: Arc::clone(&root),
doors: vec!["urn:lisp:eval".to_string(), "urn:absent".to_string()],
};
let resolve = |iri: &str| {
let request = Request::new(Verb::Source, Iri::parse(iri).expect("an IRI"));
matches!(only.resolve(&request, &Scope::empty()), Resolution::Hit(_))
};
assert!(resolve("urn:lisp:eval"));
assert!(!resolve("urn:other"), "not a governed door");
assert!(
!resolve("urn:absent"),
"a governed door the root does not bind"
);
let kernel = Kernel::new(govern(
root,
Duration::from_secs(5),
vec!["urn:lisp:eval".to_string()],
));
let request = Request::new(Verb::Source, Iri::parse("urn:other").expect("an IRI"));
let answer = futures::executor::block_on(kernel.issue(request, &Capability::root()))
.expect("the root still answers");
assert_eq!(String::from_utf8_lossy(&answer.bytes), "42");
}
}