1use crate::atomic::atomic_write;
17use crate::error::{FsError, Result};
18use serde::{Deserialize, Serialize};
19#[cfg(not(unix))]
20use std::fs::TryLockError;
21use std::fs::{File, OpenOptions};
22use std::path::{Path, PathBuf};
23use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
24
25#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
26pub struct LockOwner {
27 pub pid: u32,
28 pub hostname: String,
29 pub label: String,
30 pub started_unix_ms: u64,
31}
32
33impl LockOwner {
34 pub fn current(label: &str) -> Self {
35 Self {
36 pid: std::process::id(),
37 hostname: hostname(),
38 label: label.to_string(),
39 started_unix_ms: SystemTime::now()
40 .duration_since(UNIX_EPOCH)
41 .map(|d| d.as_millis() as u64)
42 .unwrap_or(0),
43 }
44 }
45
46 pub fn describe(&self) -> String {
47 format!(
48 "pid {} on {} ({}) since unix-ms {}",
49 self.pid, self.hostname, self.label, self.started_unix_ms
50 )
51 }
52}
53
54#[derive(Clone, Debug)]
55pub struct LockOptions {
56 pub timeout: Duration,
58 pub poll: Duration,
59 pub label: String,
61}
62
63impl Default for LockOptions {
64 fn default() -> Self {
65 Self {
66 timeout: Duration::ZERO,
67 poll: Duration::from_millis(50),
68 label: String::new(),
69 }
70 }
71}
72
73impl LockOptions {
74 pub fn wait(timeout: Duration) -> Self {
75 Self {
76 timeout,
77 ..Self::default()
78 }
79 }
80
81 pub fn labeled(mut self, label: &str) -> Self {
82 self.label = label.to_string();
83 self
84 }
85}
86
87#[derive(Debug)]
89pub struct FileLock {
90 #[cfg_attr(unix, allow(dead_code))]
92 file: Handle,
93 path: PathBuf,
94 owner_path: Option<PathBuf>,
95 pub recovered_stale: Option<LockOwner>,
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101enum Mode {
102 Exclusive,
103 Shared,
104}
105
106#[cfg(unix)]
108type Handle = posix::Handle;
109#[cfg(not(unix))]
110type Handle = File;
111
112pub fn owner_path_for(lock_path: &Path) -> PathBuf {
113 let mut os = lock_path.as_os_str().to_os_string();
114 os.push(".owner");
115 PathBuf::from(os)
116}
117
118fn open_lock_file(path: &Path) -> Result<File> {
119 let mut options = OpenOptions::new();
120 options.read(true).write(true).create(true).truncate(false);
121 #[cfg(unix)]
122 {
123 use std::os::unix::fs::OpenOptionsExt;
124 options
125 .mode(0o600)
126 .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC);
127 }
128 options.open(path).map_err(|e| FsError::io(path, e))
129}
130
131impl FileLock {
132 pub fn acquire(path: &Path, options: &LockOptions) -> Result<Self> {
134 let file = lock_with_retry(path, options, Mode::Exclusive)?;
135 let owner_path = owner_path_for(path);
136 let recovered_stale = read_owner(&owner_path);
137 let record = serde_json::to_vec_pretty(&LockOwner::current(&options.label))
138 .expect("owner serializes");
139 atomic_write(&owner_path, &record)?;
140 Ok(Self {
141 file,
142 path: path.to_path_buf(),
143 owner_path: Some(owner_path),
144 recovered_stale,
145 })
146 }
147
148 pub fn try_acquire(path: &Path) -> Result<Self> {
150 Self::acquire(path, &LockOptions::default())
151 }
152
153 pub fn acquire_shared(path: &Path, options: &LockOptions) -> Result<Self> {
155 let file = lock_with_retry(path, options, Mode::Shared)?;
156 Ok(Self {
157 file,
158 path: path.to_path_buf(),
159 owner_path: None,
160 recovered_stale: None,
161 })
162 }
163
164 pub fn path(&self) -> &Path {
165 &self.path
166 }
167
168 pub fn current_owner(lock_path: &Path) -> Option<LockOwner> {
170 read_owner(&owner_path_for(lock_path))
171 }
172}
173
174#[cfg(unix)]
176fn lock_with_retry(path: &Path, options: &LockOptions, mode: Mode) -> Result<Handle> {
177 let started = Instant::now();
178 loop {
179 if let Some(handle) = posix::try_lock(path, mode)? {
180 return Ok(handle);
181 }
182 wait_or_fail(path, options, started, mode)?;
183 }
184}
185
186#[cfg(not(unix))]
188fn lock_with_retry(path: &Path, options: &LockOptions, mode: Mode) -> Result<Handle> {
189 let file = open_lock_file(path)?;
190 let started = Instant::now();
191 loop {
192 let locked = match mode {
193 Mode::Exclusive => file.try_lock(),
194 Mode::Shared => file.try_lock_shared(),
195 };
196 match locked {
197 Ok(()) => return Ok(file),
198 Err(TryLockError::WouldBlock) => wait_or_fail(path, options, started, mode)?,
199 Err(TryLockError::Error(e)) => return Err(FsError::io(path, e)),
200 }
201 }
202}
203
204fn wait_or_fail(path: &Path, options: &LockOptions, started: Instant, mode: Mode) -> Result<()> {
206 if started.elapsed() >= options.timeout {
207 let waited_ms = started.elapsed().as_millis() as u64;
208 return Err(match mode {
209 Mode::Exclusive if options.timeout.is_zero() => {
210 let owner = read_owner(&owner_path_for(path))
211 .map(|o| o.describe())
212 .unwrap_or_else(|| "an unknown owner".into());
213 FsError::Locked {
214 path: path.to_path_buf(),
215 owner,
216 }
217 }
218 _ => FsError::LockTimeout {
219 path: path.to_path_buf(),
220 waited_ms,
221 },
222 });
223 }
224 std::thread::sleep(options.poll);
225 Ok(())
226}
227
228impl Drop for FileLock {
229 fn drop(&mut self) {
230 if let Some(owner) = &self.owner_path {
232 let _ = std::fs::remove_file(owner);
233 }
234 #[cfg(not(unix))]
236 let _ = self.file.unlock();
237 }
238}
239
240#[cfg(unix)]
251mod posix {
252 use super::{open_lock_file, Mode};
253 use crate::error::{FsError, Result};
254 use nix::errno::Errno;
255 use nix::fcntl::{fcntl, FcntlArg};
256 use nix::libc;
257 use std::collections::BTreeMap;
258 use std::fs::File;
259 use std::path::{Path, PathBuf};
260 use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
261
262 struct Entry {
263 mode: Mode,
264 holders: usize,
265 file: Arc<File>,
266 }
267
268 fn registry() -> MutexGuard<'static, BTreeMap<PathBuf, Entry>> {
269 static REGISTRY: OnceLock<Mutex<BTreeMap<PathBuf, Entry>>> = OnceLock::new();
270 REGISTRY
271 .get_or_init(|| Mutex::new(BTreeMap::new()))
272 .lock()
273 .unwrap_or_else(|p| p.into_inner())
274 }
275
276 #[derive(Debug)]
278 pub struct Handle {
279 key: PathBuf,
280 file: Option<Arc<File>>,
283 }
284
285 pub fn try_lock(path: &Path, mode: Mode) -> Result<Option<Handle>> {
287 let key = registry_key(path);
288 let mut registry = registry();
289 if let Some(entry) = registry.get_mut(&key) {
290 if entry.mode == Mode::Shared && mode == Mode::Shared {
293 entry.holders += 1;
294 return Ok(Some(Handle {
295 key,
296 file: Some(entry.file.clone()),
297 }));
298 }
299 return Ok(None);
300 }
301 let file = Arc::new(open_lock_file(path)?);
302 if !set_lock(&file, mode, path)? {
303 return Ok(None);
306 }
307 registry.insert(
308 key.clone(),
309 Entry {
310 mode,
311 holders: 1,
312 file: file.clone(),
313 },
314 );
315 Ok(Some(Handle {
316 key,
317 file: Some(file),
318 }))
319 }
320
321 fn set_lock(file: &File, mode: Mode, path: &Path) -> Result<bool> {
323 let kind = match mode {
324 Mode::Exclusive => libc::F_WRLCK,
325 Mode::Shared => libc::F_RDLCK,
326 };
327 let request = libc::flock {
328 l_type: kind as _,
329 l_whence: libc::SEEK_SET as _,
330 l_start: 0,
331 l_len: 0,
332 l_pid: 0,
333 };
334 match fcntl(file, FcntlArg::F_SETLK(&request)) {
335 Ok(_) => Ok(true),
336 Err(Errno::EAGAIN) | Err(Errno::EACCES) => Ok(false),
337 Err(errno) => Err(FsError::io(path, std::io::Error::from(errno))),
338 }
339 }
340
341 fn registry_key(path: &Path) -> PathBuf {
344 let parent = path
345 .parent()
346 .filter(|p| !p.as_os_str().is_empty())
347 .unwrap_or(Path::new("."));
348 match (parent.canonicalize(), path.file_name()) {
349 (Ok(dir), Some(name)) => dir.join(name),
350 _ => path.to_path_buf(),
351 }
352 }
353
354 impl Drop for Handle {
355 fn drop(&mut self) {
356 let mut registry = registry();
357 let last = match registry.get_mut(&self.key) {
358 Some(entry) => {
359 entry.holders -= 1;
360 entry.holders == 0
361 }
362 None => false,
363 };
364 if last {
365 registry.remove(&self.key);
366 }
367 drop(self.file.take());
368 }
369 }
370}
371
372pub fn with_lock<T>(path: &Path, options: &LockOptions, f: impl FnOnce() -> T) -> Result<T> {
374 let _guard = FileLock::acquire(path, options)?;
375 Ok(f())
376}
377
378fn read_owner(path: &Path) -> Option<LockOwner> {
379 serde_json::from_slice(&std::fs::read(path).ok()?).ok()
380}
381
382pub fn hostname() -> String {
383 #[cfg(unix)]
384 {
385 let mut buf = [0u8; 256];
386 if unsafe { libc::gethostname(buf.as_mut_ptr().cast(), buf.len()) } == 0 {
388 let end = buf.iter().position(|b| *b == 0).unwrap_or(buf.len());
389 let name = String::from_utf8_lossy(&buf[..end]).trim().to_string();
390 if !name.is_empty() {
391 return name;
392 }
393 }
394 }
395 for var in ["COMPUTERNAME", "HOSTNAME"] {
396 if let Some(v) = std::env::var_os(var) {
397 let s = v.to_string_lossy().trim().to_string();
398 if !s.is_empty() {
399 return s;
400 }
401 }
402 }
403 "unknown".into()
404}