use std::path::Path;
use std::time::Duration;
const DEFAULT: Duration = Duration::from_secs(120);
#[must_use]
pub fn drop_after() -> Option<Duration> {
if let Ok(raw) = std::env::var("LJOS_MCP_IDLE_DROP") {
if !raw.trim().is_empty() {
return parse(&raw);
}
}
let mut window = Some(DEFAULT);
for path in crate::converge::config_paths(None) {
if let Some(set) = configured(&path) {
window = set;
}
}
window
}
fn configured(path: &Path) -> Option<Option<Duration>> {
let text = std::fs::read_to_string(path).ok()?;
let value = toml::from_str::<toml::Value>(&text).ok()?;
match value.get("mcp")?.get("idle_drop")? {
toml::Value::Integer(secs) => Some(u64::try_from(*secs).ok().and_then(window)),
toml::Value::String(raw) => Some(parse(raw)),
toml::Value::Boolean(false) => Some(None),
_ => None,
}
}
fn window(secs: u64) -> Option<Duration> {
(secs > 0).then(|| Duration::from_secs(secs))
}
fn parse(raw: &str) -> Option<Duration> {
let raw = raw.trim();
if matches!(
raw.to_ascii_lowercase().as_str(),
"off" | "false" | "no" | "never"
) {
return None;
}
if let Ok(secs) = raw.parse::<u64>() {
return window(secs);
}
match crate::reap::parse_idle(raw) {
Ok(found) if found.is_zero() => None,
Ok(found) => Some(found),
Err(_) => Some(DEFAULT),
}
}
pub fn release_memory() {
crate::drop_listing_cache();
#[cfg(all(target_os = "linux", target_env = "gnu"))]
unsafe {
libc::malloc_trim(0);
}
}
pub struct CallActivity(());
impl Drop for CallActivity {
fn drop(&mut self) {
crate::bookkeep::touch_activity();
}
}
#[must_use]
pub fn call_started() -> CallActivity {
crate::bookkeep::touch_activity();
CallActivity(())
}
pub fn spawn_idle_drop() {
let Some(limit) = drop_after() else {
return;
};
crate::bookkeep::touch_activity();
let tick = (limit / 2).clamp(Duration::from_secs(1), Duration::from_secs(10));
let _ = std::thread::Builder::new()
.name("ljos-mcp-drop".into())
.stack_size(64 * 1024)
.spawn(move || {
let mut dropped_at = 0;
loop {
std::thread::sleep(tick);
let last = crate::bookkeep::last_activity();
if last != dropped_at
&& crate::bookkeep::idle_due(crate::bookkeep::now_secs(), last, limit)
{
release_memory();
dropped_at = last;
}
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_bare_number_is_seconds_and_off_keeps_everything() {
assert_eq!(parse("120"), Some(Duration::from_secs(120)));
assert_eq!(parse("5m"), Some(Duration::from_secs(300)));
assert_eq!(parse("0"), None);
assert_eq!(parse("off"), None);
assert_eq!(parse("soon"), Some(DEFAULT));
}
#[test]
fn ljos_toml_sets_the_window_under_mcp() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("ljos.toml");
std::fs::write(&file, "[mcp]\nidle_drop = 30\n").unwrap();
assert_eq!(configured(&file), Some(Some(Duration::from_secs(30))));
std::fs::write(&file, "[mcp]\nidle_drop = \"off\"\n").unwrap();
assert_eq!(configured(&file), Some(None));
std::fs::write(&file, "[git_hooks]\nskip = []\n").unwrap();
assert_eq!(configured(&file), None);
}
}