zoi-core 1.24.6

Advanced Package Manager & Environment Orchestrator
Documentation
use anyhow::{Result, anyhow};
use colored::*;
use fs2::FileExt;
use std::fs;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::PathBuf;

fn get_lock_path() -> Result<PathBuf> {
    let home_dir =
        crate::utils::get_user_home().ok_or_else(|| anyhow!("Could not find home directory."))?;
    Ok(home_dir.join(".zoi").join("pkgs").join("lock"))
}

/// Acquires a system-wide lock to prevent concurrent modifications to the Zoi store.
///
/// Locking Mechanism:
/// - Attempts to open/create `~/.zoi/pkgs/lock`.
/// - Uses `flock` (via `fs2`) to acquire an exclusive advisory lock on the file.
/// - If busy, reads the file to display the PID of the process currently holding the lock.
/// - Once acquired, writes the current process PID to the lock file.
///
/// This ensures that operations like `install`, `uninstall`, and `update` never
/// run simultaneously, preventing database and filesystem corruption.
pub fn acquire_lock() -> Result<LockGuard> {
    if std::env::var("ZOI_SKIP_LOCK").is_ok_and(|v| v == "1") {
        return Ok(LockGuard::noop());
    }
    let lock_path = get_lock_path()?;

    if let Some(parent) = lock_path.parent()
        && let Err(e) = fs::create_dir_all(parent)
    {
        eprintln!(
            "Warning: could not create lock directory {}: {}",
            parent.display(),
            e
        );
    }

    let file = fs::OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .truncate(false)
        .open(&lock_path)?;

    if file.try_lock_exclusive().is_err() {
        let mut content = String::new();
        if let Ok(mut f) = fs::File::open(&lock_path) {
            let _ = f.read_to_string(&mut content);
        }

        let pid_info = if !content.trim().is_empty() {
            format!(" (held by PID {})", content.trim())
        } else {
            String::new()
        };

        eprintln!(
            "{}: Another Zoi process{} may be running.",
            "Error".red().bold(),
            pid_info
        );
        eprintln!(
            "If you are absolutely sure no other Zoi process is running, you can manually remove the lock file:"
        );
        eprintln!("  {}", lock_path.display());
        return Err(anyhow!("Could not acquire lock."));
    }

    let mut file = file;
    let _ = file.set_len(0);
    let _ = file.seek(SeekFrom::Start(0));
    let _ = write!(file, "{}", std::process::id());
    let _ = file.flush();

    Ok(LockGuard {
        path: Some(lock_path),
        _file: Some(file),
    })
}

pub fn release_lock() -> Result<()> {
    let lock_path = get_lock_path()?;
    if lock_path.exists() {
        let _ = fs::remove_file(lock_path);
    }
    Ok(())
}

pub struct LockGuard {
    path: Option<PathBuf>,
    _file: Option<fs::File>,
}

impl LockGuard {
    pub fn noop() -> Self {
        Self {
            path: None,
            _file: None,
        }
    }
}

impl Drop for LockGuard {
    fn drop(&mut self) {
        if let Some(path) = self.path.take() {
            self._file.take();

            if path.exists()
                && let Err(e) = fs::remove_file(&path)
            {
                debug_assert!(false, "Failed to remove lock file: {}", e);
            }
        }
    }
}