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 static LOCK_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
13}
14
15fn 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
22pub 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 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
121pub 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
135pub struct LockGuard {
137 path: Option<PathBuf>,
139 file: Option<fs::File>,
141 is_recursive: bool
143}
144
145impl LockGuard {
146 pub fn noop() -> Self {
148 Self {
149 path: None,
150 file: None,
151 is_recursive: false
152 }
153 }
154
155 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}