lucy-cli 1.14.5

A small local JSONL agent harness
Documentation
use std::fs::{File, OpenOptions};
use std::path::Path;
#[cfg(any(not(unix), test))]
use std::path::PathBuf;

use crate::config::{ensure_not_symlink, ensure_private_dir, lucy_dir};

#[cfg(unix)]
use std::os::fd::AsRawFd;
#[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};

#[derive(Debug)]
pub(super) struct SessionLease {
    file: File,
    #[cfg(not(unix))]
    path: PathBuf,
}

impl SessionLease {
    pub(super) fn acquire(home: &Path, session_id: &str) -> Result<Self, String> {
        let lucy = lucy_dir(home);
        ensure_not_symlink(&lucy)
            .map_err(|_| "session writer lease directory is unsafe".to_owned())?;
        ensure_private_dir(&lucy)
            .map_err(|_| "unable to secure session writer lease directory".to_owned())?;
        let directory = lucy.join("locks");
        ensure_not_symlink(&directory)
            .map_err(|_| "session writer lease directory is unsafe".to_owned())?;
        ensure_private_dir(&directory)
            .map_err(|_| "unable to secure session writer lease directory".to_owned())?;
        let path = directory.join(format!("{session_id}.lock"));
        ensure_not_symlink(&path).map_err(|_| "session writer lease is unsafe".to_owned())?;

        #[cfg(unix)]
        {
            let mut options = OpenOptions::new();
            options.read(true).write(true).create(true);
            options.mode(0o600);
            options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
            let file = options
                .open(&path)
                .map_err(|_| "unable to open session writer lease".to_owned())?;
            let metadata = file
                .metadata()
                .map_err(|_| "unable to inspect session writer lease".to_owned())?;
            if !metadata.is_file() || metadata.permissions().mode() & 0o777 != 0o600 {
                return Err("session writer lease is not a private regular file".to_owned());
            }
            let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
            if result != 0 {
                let error = std::io::Error::last_os_error();
                if error.raw_os_error() == Some(libc::EWOULDBLOCK)
                    || error.raw_os_error() == Some(libc::EAGAIN)
                {
                    return Err("session is already open for writing".to_owned());
                }
                return Err("unable to acquire session writer lease".to_owned());
            }
            file.sync_data()
                .map_err(|_| "unable to checkpoint session writer lease".to_owned())?;
            Ok(Self { file })
        }

        #[cfg(not(unix))]
        {
            let file = OpenOptions::new()
                .read(true)
                .write(true)
                .create_new(true)
                .open(&path)
                .map_err(|error| {
                    if error.kind() == std::io::ErrorKind::AlreadyExists {
                        "session is already open for writing".to_owned()
                    } else {
                        "unable to acquire session writer lease".to_owned()
                    }
                })?;
            Ok(Self { file, path })
        }
    }
}

impl Drop for SessionLease {
    fn drop(&mut self) {
        #[cfg(unix)]
        unsafe {
            let _ = libc::flock(self.file.as_raw_fd(), libc::LOCK_UN);
        }
        #[cfg(not(unix))]
        {
            let _ = std::fs::remove_file(&self.path);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::atomic::{AtomicU64, Ordering};

    static COUNTER: AtomicU64 = AtomicU64::new(0);

    fn home() -> PathBuf {
        let path = std::env::temp_dir().join(format!(
            "lucy-session-lease-{}-{}",
            std::process::id(),
            COUNTER.fetch_add(1, Ordering::Relaxed)
        ));
        std::fs::create_dir(&path).expect("home");
        path
    }

    #[test]
    fn a_second_writer_fails_without_waiting() {
        let home = home();
        let first = SessionLease::acquire(&home, "session").expect("first lease");
        let error = SessionLease::acquire(&home, "session").expect_err("second writer must fail");
        assert_eq!(error, "session is already open for writing");
        drop(first);
        SessionLease::acquire(&home, "session").expect("lease after release");
        std::fs::remove_dir_all(home).expect("cleanup");
    }
}