malvin 0.2.7

Non-interactive research and coding agent
use std::fs;
use std::io::Write;
use std::path::Path;

use super::local_llm_ollama::process_alive;

pub(crate) fn try_acquire_lock(path: &Path) -> Result<bool, String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).map_err(|e| format!("mkdir {}: {e}", parent.display()))?;
    }
    reclaim_stale_lock(path);
    match fs::OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(path)
    {
        Ok(mut file) => {
            writeln!(file, "{}", std::process::id()).map_err(|e| e.to_string())?;
            Ok(true)
        }
        Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(false),
        Err(e) => Err(format!("lock {}: {e}", path.display())),
    }
}

pub(crate) fn release_lock(path: &Path) {
    let _ = fs::remove_file(path);
}

pub(crate) fn reclaim_stale_lock(path: &Path) {
    let Ok(body) = fs::read_to_string(path) else {
        return;
    };
    let Some(pid) = body.trim().parse::<u32>().ok() else {
        let _ = fs::remove_file(path);
        return;
    };
    if pid != std::process::id() && !process_alive(pid) {
        let _ = fs::remove_file(path);
    }
}

pub(crate) fn lock_holder_pid(path: &Path) -> Option<u32> {
    let body = fs::read_to_string(path).ok()?;
    let pid = body.trim().parse::<u32>().ok()?;
    process_alive(pid).then_some(pid)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn acquire_release_and_stale_reclaim() {
        let tmp = tempfile::tempdir().expect("tmp");
        let lock = tmp.path().join("m.lock");
        assert!(try_acquire_lock(&lock).expect("acq"));
        assert!(!try_acquire_lock(&lock).expect("busy"));
        release_lock(&lock);
        fs::write(&lock, "999999999\n").expect("stale");
        reclaim_stale_lock(&lock);
        assert!(!lock.is_file() || lock_holder_pid(&lock).is_none());
        assert!(try_acquire_lock(&lock).expect("after stale"));
        release_lock(&lock);
    }
}