void_core/support/
lock.rs1use std::fs::{self, File, OpenOptions};
4use std::io::Write;
5use std::path::{Path, PathBuf};
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use super::error::{Result, VoidError};
9
10const LOCK_FILENAME: &str = "LOCK";
11
12pub struct StaleLockInfo {
14 pub pid: u32,
15 pub created: u64,
16 pub path: PathBuf,
17}
18
19pub struct RepoLock {
21 path: PathBuf,
22 file: Option<File>,
23}
24
25impl RepoLock {
26 pub fn acquire(void_dir: &Path, force: bool) -> Result<Self> {
28 let path = void_dir.join(LOCK_FILENAME);
29
30 if force && path.exists() {
31 fs::remove_file(&path)?;
32 }
33
34 let mut file = match OpenOptions::new().write(true).create_new(true).open(&path) {
35 Ok(file) => file,
36 Err(err) => {
37 if err.kind() == std::io::ErrorKind::AlreadyExists {
38 return Err(VoidError::RepoLocked(path.display().to_string()));
39 }
40 return Err(VoidError::Io(err));
41 }
42 };
43
44 let pid = std::process::id();
45 let created = SystemTime::now()
46 .duration_since(UNIX_EPOCH)
47 .unwrap_or_default()
48 .as_secs();
49 let content = format!("pid={pid}\ncreated={created}\n");
50 if let Err(err) = file.write_all(content.as_bytes()) {
51 let _ = fs::remove_file(&path);
52 return Err(VoidError::Io(err));
53 }
54
55 Ok(Self {
56 path,
57 file: Some(file),
58 })
59 }
60
61 pub fn is_stale(void_dir: &Path) -> Result<Option<StaleLockInfo>> {
63 let path = void_dir.join(LOCK_FILENAME);
64 if !path.exists() {
65 return Ok(None);
66 }
67
68 let content = fs::read_to_string(&path)?;
69 let pid = parse_pid(&content)?;
70 let created = parse_created(&content)?;
71
72 if !is_process_running(pid) {
73 return Ok(Some(StaleLockInfo { pid, created, path }));
74 }
75
76 Ok(None)
77 }
78
79 pub fn acquire_auto(void_dir: &Path) -> Result<Self> {
81 if let Some(stale) = Self::is_stale(void_dir)? {
82 let _ = fs::remove_file(&stale.path);
84 }
85 Self::acquire(void_dir, false)
86 }
87}
88
89fn parse_pid(content: &str) -> Result<u32> {
90 content
91 .lines()
92 .find(|l| l.starts_with("pid="))
93 .and_then(|l| l.strip_prefix("pid="))
94 .and_then(|s| s.parse().ok())
95 .ok_or_else(|| VoidError::Lock("invalid lock file format".into()))
96}
97
98fn parse_created(content: &str) -> Result<u64> {
99 content
100 .lines()
101 .find(|l| l.starts_with("created="))
102 .and_then(|l| l.strip_prefix("created="))
103 .and_then(|s| s.parse().ok())
104 .ok_or_else(|| VoidError::Lock("invalid lock file format".into()))
105}
106
107#[cfg(unix)]
108fn is_process_running(pid: u32) -> bool {
109 unsafe { libc::kill(pid as i32, 0) == 0 }
112}
113
114#[cfg(windows)]
115fn is_process_running(pid: u32) -> bool {
116 const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
119 extern "system" {
120 fn OpenProcess(access: u32, inherit: i32, pid: u32) -> isize;
121 fn CloseHandle(handle: isize) -> i32;
122 }
123 let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
124 if handle == 0 {
125 false
126 } else {
127 unsafe { CloseHandle(handle) };
128 true
129 }
130}
131
132#[cfg(not(any(unix, windows)))]
133fn is_process_running(_pid: u32) -> bool {
134 true
136}
137
138impl Drop for RepoLock {
139 fn drop(&mut self) {
140 let _ = self.file.take();
141 let _ = fs::remove_file(&self.path);
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn stale_lock_detected_when_pid_not_running() {
151 let temp = tempfile::TempDir::new().unwrap();
152 let void_dir = temp.path();
153
154 let lock_path = void_dir.join("LOCK");
156 fs::write(&lock_path, "pid=999999999\ncreated=1234567890\n").unwrap();
157
158 let stale = RepoLock::is_stale(void_dir).unwrap();
160 assert!(stale.is_some());
161 assert_eq!(stale.unwrap().pid, 999999999);
162 }
163
164 #[test]
165 fn active_lock_not_considered_stale() {
166 let temp = tempfile::TempDir::new().unwrap();
167 let void_dir = temp.path();
168
169 let lock_path = void_dir.join("LOCK");
171 let content = format!("pid={}\ncreated=1234567890\n", std::process::id());
172 fs::write(&lock_path, content).unwrap();
173
174 let stale = RepoLock::is_stale(void_dir).unwrap();
176 assert!(stale.is_none());
177 }
178
179 #[test]
180 fn acquire_auto_removes_stale_lock() {
181 let temp = tempfile::TempDir::new().unwrap();
182 let void_dir = temp.path();
183
184 let lock_path = void_dir.join("LOCK");
186 fs::write(&lock_path, "pid=999999999\ncreated=1234567890\n").unwrap();
187
188 let lock = RepoLock::acquire_auto(void_dir).unwrap();
190
191 assert!(lock_path.exists());
193
194 drop(lock);
195 }
196
197 #[test]
198 fn no_lock_returns_none() {
199 let temp = tempfile::TempDir::new().unwrap();
200 let void_dir = temp.path();
201
202 let stale = RepoLock::is_stale(void_dir).unwrap();
204 assert!(stale.is_none());
205 }
206
207 #[test]
208 fn parse_pid_works() {
209 let content = "pid=12345\ncreated=1234567890\n";
210 assert_eq!(parse_pid(content).unwrap(), 12345);
211 }
212
213 #[test]
214 fn parse_created_works() {
215 let content = "pid=12345\ncreated=1234567890\n";
216 assert_eq!(parse_created(content).unwrap(), 1234567890);
217 }
218
219 #[test]
220 fn invalid_lock_format_returns_error() {
221 let content = "invalid format";
222 assert!(parse_pid(content).is_err());
223 assert!(parse_created(content).is_err());
224 }
225}