codebase-graph 1.2.2

Native codebaseGraph CLI and MCP server for local code knowledge graphs.
use crate::error::NativeError;
use fs2::{lock_contended_error, FileExt};
use std::fs::{self, File, OpenOptions};
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LockMode {
    Shared,
    Exclusive,
}

#[derive(Debug)]
pub(crate) struct LockedFile {
    path: PathBuf,
    file: Option<File>,
    mode: LockMode,
}

impl LockedFile {
    pub(crate) fn relock_shared(mut self) -> Result<Self, NativeError> {
        let file = self.file.as_ref().ok_or_else(|| {
            NativeError::InvalidInput(format!(
                "lock {} has already been released",
                self.path.display()
            ))
        })?;
        FileExt::lock_shared(file)?;
        self.mode = LockMode::Shared;
        Ok(self)
    }
}

impl Drop for LockedFile {
    fn drop(&mut self) {
        if let Some(file) = self.file.take() {
            let _ = FileExt::unlock(&file);
        }
    }
}

pub(crate) type WriterLease = LockedFile;
pub(crate) type StateLease = LockedFile;
pub(crate) type RunLease = LockedFile;

pub(crate) fn open_locked(
    path: impl AsRef<Path>,
    mode: LockMode,
) -> Result<LockedFile, NativeError> {
    open_locked_inner(path.as_ref(), mode, false)
}

pub(crate) fn try_open_locked(
    path: impl AsRef<Path>,
    mode: LockMode,
) -> Result<Option<LockedFile>, NativeError> {
    match open_locked_inner(path.as_ref(), mode, true) {
        Ok(locked) => Ok(Some(locked)),
        Err(NativeError::Io(error)) if is_lock_contended(&error) => Ok(None),
        Err(other) => Err(other),
    }
}

fn is_lock_contended(error: &std::io::Error) -> bool {
    let expected = lock_contended_error();
    error.kind() == std::io::ErrorKind::WouldBlock
        || (error.raw_os_error().is_some() && error.raw_os_error() == expected.raw_os_error())
}

fn open_locked_inner(
    path: &Path,
    mode: LockMode,
    non_blocking: bool,
) -> Result<LockedFile, NativeError> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
        let parent_metadata = fs::symlink_metadata(parent)?;
        if parent_metadata.file_type().is_symlink() || !parent_metadata.is_dir() {
            return Err(NativeError::InvalidInput(format!(
                "lock parent must be a real directory: {}",
                parent.display()
            )));
        }
    }
    if path.exists() {
        let metadata = fs::symlink_metadata(path)?;
        if metadata.file_type().is_symlink() || !metadata.is_file() {
            return Err(NativeError::InvalidInput(format!(
                "lock path must be a real file: {}",
                path.display()
            )));
        }
    }
    let file = OpenOptions::new()
        .create(true)
        .read(true)
        .write(true)
        .truncate(false)
        .open(path)?;
    match (mode, non_blocking) {
        (LockMode::Shared, false) => FileExt::lock_shared(&file)?,
        (LockMode::Exclusive, false) => FileExt::lock_exclusive(&file)?,
        (LockMode::Shared, true) => FileExt::try_lock_shared(&file)?,
        (LockMode::Exclusive, true) => FileExt::try_lock_exclusive(&file)?,
    }
    Ok(LockedFile {
        path: path.to_path_buf(),
        file: Some(file),
        mode,
    })
}