Skip to main content

zoi_core/
lock.rs

1use std::fs;
2use std::io::{Read, Seek, SeekFrom, Write};
3use std::path::PathBuf;
4
5use anyhow::{Result, anyhow};
6use colored::Colorize;
7use fs2::FileExt;
8
9thread_local! {
10    /// A per-thread counter to track the number of active lock guards in the
11    /// current thread, allowing for re-entrant locking within the same thread.
12    static LOCK_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
13}
14
15/// Returns the path to the system-wide lock file.
16fn get_lock_path() -> Result<PathBuf> {
17    let home_dir = crate::utils::get_user_home()
18        .ok_or_else(|| anyhow!("Could not find home directory."))?;
19    Ok(home_dir.join(".zoi").join("pkgs").join("lock"))
20}
21
22/// Acquires a system-wide lock to prevent concurrent modifications to the Zoi
23/// store.
24///
25/// Locking Mechanism:
26/// - Attempts to open/create `~/.zoi/pkgs/lock`.
27/// - Uses `flock` (via `fs2`) to acquire an exclusive advisory lock on the
28///   file.
29/// - If busy, reads the file to display the PID of the process currently
30///   holding the lock.
31/// - Once acquired, writes the current process PID to the lock file.
32///
33/// This ensures that operations like `install`, `uninstall`, and `update` never
34/// run simultaneously, preventing database and filesystem corruption.
35///
36/// # Errors
37///
38/// Returns an error if the lock cannot be acquired or if file operations fail.
39pub fn acquire_lock() -> Result<LockGuard> {
40    if std::env::var("ZOI_SKIP_LOCK").is_ok_and(|v| v == "1") {
41        return Ok(LockGuard::noop());
42    }
43
44    let lock_path = get_lock_path()?;
45
46    // Check if the current thread already holds a lock.
47    let count = LOCK_COUNT.with(|c| {
48        let val = c.get();
49        c.set(val + 1);
50        val
51    });
52
53    if count > 0 {
54        return Ok(LockGuard::recursive());
55    }
56
57    if let Some(parent) = lock_path.parent()
58        && let Err(e) = fs::create_dir_all(parent)
59    {
60        eprintln!(
61            "Warning: could not create lock directory {}: {}",
62            parent.display(),
63            e
64        );
65    }
66
67    let file_res = fs::OpenOptions::new()
68        .read(true)
69        .write(true)
70        .create(true)
71        .truncate(false)
72        .open(&lock_path);
73
74    let file = match file_res {
75        Ok(f) => f,
76        Err(e) => {
77            LOCK_COUNT.with(|c| c.set(c.get() - 1));
78            return Err(e.into());
79        }
80    };
81
82    if file.try_lock_exclusive().is_err() {
83        LOCK_COUNT.with(|c| c.set(c.get() - 1));
84        let mut content = String::new();
85        if let Ok(mut f) = fs::File::open(&lock_path) {
86            let _ = f.read_to_string(&mut content);
87        }
88
89        let pid_info = if content.trim().is_empty() {
90            String::new()
91        } else {
92            format!(" (held by PID {})", content.trim())
93        };
94
95        eprintln!(
96            "{}: Another Zoi process{} may be running.",
97            "Error".red().bold(),
98            pid_info
99        );
100        eprintln!(
101            "If you are absolutely sure no other Zoi process is running, you \
102             can manually remove the lock file:"
103        );
104        eprintln!("  {}", lock_path.display());
105        return Err(anyhow!("Could not acquire lock."));
106    }
107
108    let mut file = file;
109    let _ = file.set_len(0);
110    let _ = file.seek(SeekFrom::Start(0));
111    let _ = write!(file, "{}", std::process::id());
112    let _ = file.flush();
113
114    Ok(LockGuard {
115        path: Some(lock_path),
116        file: Some(file),
117        is_recursive: false
118    })
119}
120
121/// Releases the system-wide lock if it exists.
122///
123/// # Errors
124///
125/// Returns an error if the lock path cannot be determined.
126pub fn release_lock() -> Result<()> {
127    let lock_path = get_lock_path()?;
128    if lock_path.exists() {
129        let _ = fs::remove_file(lock_path);
130    }
131    LOCK_COUNT.with(|c| c.set(0));
132    Ok(())
133}
134
135/// A guard that releases the system-wide lock when dropped.
136pub struct LockGuard {
137    /// The path to the lock file.
138    path: Option<PathBuf>,
139    /// The file handle holding the lock.
140    file: Option<fs::File>,
141    /// Whether this guard is a recursive acquisition.
142    is_recursive: bool
143}
144
145impl LockGuard {
146    /// Creates a no-op lock guard that does not hold any lock.
147    pub fn noop() -> Self {
148        Self {
149            path: None,
150            file: None,
151            is_recursive: false
152        }
153    }
154
155    /// Creates a recursive lock guard.
156    pub fn recursive() -> Self {
157        Self {
158            path: None,
159            file: None,
160            is_recursive: true
161        }
162    }
163}
164
165impl Drop for LockGuard {
166    fn drop(&mut self) {
167        if self.is_recursive {
168            LOCK_COUNT.with(|c| c.set(c.get() - 1));
169            return;
170        }
171
172        if let Some(path) = self.path.take() {
173            self.file.take();
174
175            if path.exists()
176                && let Err(e) = fs::remove_file(&path)
177            {
178                debug_assert!(false, "Failed to remove lock file: {e}");
179            }
180            LOCK_COUNT.with(|c| c.set(0));
181        }
182    }
183}