| use std::collections::hash_map::Entry; | |
| use std::fs::File; | |
| use std::os::unix::prelude::*; | |
| use std::path::{Path, PathBuf}; | |
| use std::sync::{Arc, LazyLock, Mutex}; | |
| use std::{io, mem}; | |
| use rustc_hash::FxHashMap; | |
| static LOCK_REGISTRY: LazyLock<Mutex<FxHashMap<PathBuf, LockState>>> = | |
| LazyLock::new(|| Mutex::new(FxHashMap::default())); | |
| enum LockState { | |
| /// Lock exclusively held. `Lock.file` contains an `Arc<UnlockGuard>` with a single reference. | |
| /// | |
| /// The `extra_files` fields contains files we opened while the lock was held. We have to | |
| /// persist them until we actually want to unlock the file to prevent unlocking on close. | |
| Exclusive { extra_files: Vec<File> }, | |
| /// Lock can be shared. When there are N lock holders, the `Arc<UnlockGuard>` has N+1 references | |
| /// with the last one being held by `LockState` and getting removed in the drop impl of `Lock` | |
| /// if it is the remaining reference. | |
| Shared(Arc<UnlockGuard>), | |
| } | |
| #[derive(Debug)] | |
| pub struct Lock { | |
| path: PathBuf, | |
| file: Option<Arc<UnlockGuard>>, | |
| } | |
| impl Lock { | |
| pub fn try_lock(p: &Path, file: File, exclusive: bool) -> io::Result<Lock> { | |
| let mut locks = LOCK_REGISTRY.lock().unwrap(); | |
| let file = match locks.entry(p.to_owned()) { | |
| Entry::Occupied(mut state) => { | |
| // We must not open the file again if there is an existing lock to prevent the close | |
| // from unlocking the file even when another `Lock` already had the lock held before | |
| // this `Lock::try_lock` call. | |
| match state.get_mut() { | |
| LockState::Exclusive { extra_files } => { | |
| // Retain file to prevent unlock on close | |
| extra_files.push(file); | |
| return Err(io::ErrorKind::WouldBlock.into()); | |
| } | |
| LockState::Shared(file) => { | |
| if exclusive { | |
| return Err(io::ErrorKind::WouldBlock.into()); | |
| } else { | |
| Arc::clone(file) | |
| } | |
| } | |
| } | |
| } | |
| Entry::Vacant(vacant) => { | |
| let file = Arc::new(UnlockGuard::try_lock(file, exclusive)?); | |
| if exclusive { | |
| vacant.insert(LockState::Exclusive { extra_files: vec![] }); | |
| } else { | |
| vacant.insert(LockState::Shared(Arc::clone(&file))); | |
| } | |
| file | |
| } | |
| }; | |
| Ok(Lock { path: p.to_owned(), file: Some(file) }) | |
| } | |
| } | |
| impl Drop for Lock { | |
| fn drop(&mut self) { | |
| let mut locks = LOCK_REGISTRY.lock().unwrap(); | |
| self.file.take().unwrap(); | |
| match locks.get_mut(&self.path).unwrap() { | |
| LockState::Exclusive { extra_files: _ } => { | |
| locks.remove(&self.path); | |
| } | |
| LockState::Shared(file) => { | |
| if Arc::strong_count(file) == 1 { | |
| locks.remove(&self.path); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /// A file guard which will unlock the file when dropped. | |
| #[derive(Debug)] | |
| struct UnlockGuard { | |
| file: File, | |
| } | |
| impl UnlockGuard { | |
| fn try_lock(file: File, exclusive: bool) -> io::Result<Self> { | |
| let lock_type = if exclusive { libc::F_WRLCK } else { libc::F_RDLCK }; | |
| let mut flock: libc::flock = unsafe { mem::zeroed() }; | |
| #[cfg(not(all(target_os = "hurd", target_arch = "x86")))] | |
| { | |
| flock.l_type = lock_type as libc::c_short; | |
| flock.l_whence = libc::SEEK_SET as libc::c_short; | |
| } | |
| #[cfg(all(target_os = "hurd", target_arch = "x86"))] | |
| { | |
| flock.l_type = lock_type as libc::c_int; | |
| flock.l_whence = libc::SEEK_SET as libc::c_int; | |
| } | |
| flock.l_start = 0; | |
| flock.l_len = 0; | |
| let ret = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_SETLK, &flock) }; | |
| if ret == -1 { Err(io::Error::last_os_error()) } else { Ok(Self { file }) } | |
| } | |
| } | |
| impl Drop for UnlockGuard { | |
| fn drop(&mut self) { | |
| let mut flock: libc::flock = unsafe { mem::zeroed() }; | |
| #[cfg(not(all(target_os = "hurd", target_arch = "x86")))] | |
| { | |
| flock.l_type = libc::F_UNLCK as libc::c_short; | |
| flock.l_whence = libc::SEEK_SET as libc::c_short; | |
| } | |
| #[cfg(all(target_os = "hurd", target_arch = "x86"))] | |
| { | |
| flock.l_type = libc::F_UNLCK as libc::c_int; | |
| flock.l_whence = libc::SEEK_SET as libc::c_int; | |
| } | |
| flock.l_start = 0; | |
| flock.l_len = 0; | |
| unsafe { | |
| libc::fcntl(self.file.as_raw_fd(), libc::F_SETLK, &flock); | |
| } | |
| } | |
| } |