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    Ok(crate::utils::get_user_state_dir()?
18        .join("pkgs")
19        .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 the user state lock file.
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        file: Some(file),
116        is_recursive: false
117    })
118}
119
120/// Returns an error because only dropping the owning [`LockGuard`] can release
121/// the advisory lock. The lock file itself is intentionally never removed
122/// because unlinking it can let a waiting process and a newly-created inode be
123/// locked concurrently.
124///
125/// # Errors
126///
127/// Returns an error if the lock path cannot be determined.
128pub fn release_lock() -> Result<()> {
129    Err(anyhow!("drop the LockGuard to release the system lock"))
130}
131
132/// A guard that releases the system-wide lock when dropped.
133pub struct LockGuard {
134    /// The file handle holding the lock.
135    file: Option<fs::File>,
136    /// Whether this guard is a recursive acquisition.
137    is_recursive: bool
138}
139
140impl LockGuard {
141    /// Creates a no-op lock guard that does not hold any lock.
142    pub fn noop() -> Self {
143        Self {
144            file: None,
145            is_recursive: false
146        }
147    }
148
149    /// Creates a recursive lock guard.
150    pub fn recursive() -> Self {
151        Self {
152            file: None,
153            is_recursive: true
154        }
155    }
156}
157
158impl Drop for LockGuard {
159    fn drop(&mut self) {
160        if self.is_recursive {
161            LOCK_COUNT.with(|c| c.set(c.get() - 1));
162            return;
163        }
164
165        if self.file.take().is_some() {
166            LOCK_COUNT.with(|c| c.set(0));
167        }
168    }
169}