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