Skip to main content

zoi_core/
lock.rs

1use anyhow::{Result, anyhow};
2use colored::*;
3use fs2::FileExt;
4use std::fs;
5use std::io::{Read, Seek, SeekFrom, Write};
6use std::path::PathBuf;
7
8fn get_lock_path() -> Result<PathBuf> {
9    let home_dir =
10        crate::utils::get_user_home().ok_or_else(|| anyhow!("Could not find home directory."))?;
11    Ok(home_dir.join(".zoi").join("pkgs").join("lock"))
12}
13
14/// Acquires a system-wide lock to prevent concurrent modifications to the Zoi store.
15///
16/// Locking Mechanism:
17/// - Attempts to open/create `~/.zoi/pkgs/lock`.
18/// - Uses `flock` (via `fs2`) to acquire an exclusive advisory lock on the file.
19/// - If busy, reads the file to display the PID of the process currently holding the lock.
20/// - Once acquired, writes the current process PID to the lock file.
21///
22/// This ensures that operations like `install`, `uninstall`, and `update` never
23/// run simultaneously, preventing database and filesystem corruption.
24pub fn acquire_lock() -> Result<LockGuard> {
25    if std::env::var("ZOI_SKIP_LOCK").is_ok_and(|v| v == "1") {
26        return Ok(LockGuard::noop());
27    }
28    let lock_path = get_lock_path()?;
29
30    if let Some(parent) = lock_path.parent()
31        && let Err(e) = fs::create_dir_all(parent)
32    {
33        eprintln!(
34            "Warning: could not create lock directory {}: {}",
35            parent.display(),
36            e
37        );
38    }
39
40    let file = fs::OpenOptions::new()
41        .read(true)
42        .write(true)
43        .create(true)
44        .truncate(false)
45        .open(&lock_path)?;
46
47    if file.try_lock_exclusive().is_err() {
48        let mut content = String::new();
49        if let Ok(mut f) = fs::File::open(&lock_path) {
50            let _ = f.read_to_string(&mut content);
51        }
52
53        let pid_info = if !content.trim().is_empty() {
54            format!(" (held by PID {})", content.trim())
55        } else {
56            String::new()
57        };
58
59        eprintln!(
60            "{}: Another Zoi process{} may be running.",
61            "Error".red().bold(),
62            pid_info
63        );
64        eprintln!(
65            "If you are absolutely sure no other Zoi process is running, you can manually remove the lock file:"
66        );
67        eprintln!("  {}", lock_path.display());
68        return Err(anyhow!("Could not acquire lock."));
69    }
70
71    let mut file = file;
72    let _ = file.set_len(0);
73    let _ = file.seek(SeekFrom::Start(0));
74    let _ = write!(file, "{}", std::process::id());
75    let _ = file.flush();
76
77    Ok(LockGuard {
78        path: Some(lock_path),
79        _file: Some(file),
80    })
81}
82
83pub fn release_lock() -> Result<()> {
84    let lock_path = get_lock_path()?;
85    if lock_path.exists() {
86        let _ = fs::remove_file(lock_path);
87    }
88    Ok(())
89}
90
91pub struct LockGuard {
92    path: Option<PathBuf>,
93    _file: Option<fs::File>,
94}
95
96impl LockGuard {
97    pub fn noop() -> Self {
98        Self {
99            path: None,
100            _file: None,
101        }
102    }
103}
104
105impl Drop for LockGuard {
106    fn drop(&mut self) {
107        if let Some(path) = self.path.take() {
108            self._file.take();
109
110            if path.exists()
111                && let Err(e) = fs::remove_file(&path)
112            {
113                debug_assert!(false, "Failed to remove lock file: {}", e);
114            }
115        }
116    }
117}