| #![cfg_attr(docsrs, feature(doc_cfg))] |
| |
| //! This crate provides a simple and cross-platform implementation of named locks. |
| //! You can use this to lock sections between processes. |
| //! |
| //! ## Example |
| //! |
| //! ```rust |
| //! use named_lock::NamedLock; |
| //! use named_lock::Result; |
| //! |
| //! fn main() -> Result<()> { |
| //! let lock = NamedLock::create("foobar")?; |
| //! let _guard = lock.lock()?; |
| //! |
| //! // Do something... |
| //! |
| //! Ok(()) |
| //! } |
| //! ``` |
| |
| use once_cell::sync::Lazy; |
| use parking_lot::{Mutex, MutexGuard}; |
| use std::collections::HashMap; |
| #[cfg(unix)] |
| use std::path::{Path, PathBuf}; |
| use std::sync::{Arc, Weak}; |
| |
| mod error; |
| #[cfg(unix)] |
| mod unix; |
| #[cfg(windows)] |
| mod windows; |
| |
| pub use crate::error::*; |
| #[cfg(unix)] |
| use crate::unix::RawNamedLock; |
| #[cfg(windows)] |
| use crate::windows::RawNamedLock; |
| |
| #[cfg(unix)] |
| type NameType = PathBuf; |
| #[cfg(windows)] |
| type NameType = String; |
| |
| // We handle two edge cases: |
| // |
| // On UNIX systems, after locking a file descriptor you can lock it again |
| // as many times you want. However OS does not keep a counter, so only one |
| // unlock must be performed. To avoid re-locking, we guard it with real mutex. |
| // |
| // On Windows, after locking a `HANDLE` you can create another `HANDLE` for |
| // the same named lock and the same process and Windows will allow you to |
| // re-lock it. To avoid this, we ensure that one `HANDLE` exists in each |
| // process for each name. |
| static OPENED_RAW_LOCKS: Lazy< |
| Mutex<HashMap<NameType, Weak<Mutex<RawNamedLock>>>>, |
| > = Lazy::new(|| Mutex::new(HashMap::new())); |
| |
| /// Cross-process lock that is identified by name. |
| #[derive(Debug)] |
| pub struct NamedLock { |
| raw: Arc<Mutex<RawNamedLock>>, |
| } |
| |
| impl NamedLock { |
| /// Create/open a named lock. |
| /// |
| /// # UNIX |
| /// |
| /// This will create/open a file and use [`flock`] on it. The path of |
| /// the lock file will be `$TMPDIR/<name>.lock`, or `/tmp/<name>.lock` |
| /// if `TMPDIR` environment variable is not set. |
| /// |
| /// If you want to specify the exact path, then use [NamedLock::with_path]. |
| /// |
| /// # Windows |
| /// |
| /// This will create/open a [global] mutex with [`CreateMutexW`]. |
| /// |
| /// # Notes |
| /// |
| /// * `name` must not be empty, otherwise an error is returned. |
| /// * `name` must not contain `\0`, `/`, nor `\`, otherwise an error is returned. |
| /// |
| /// [`flock`]: https://linux.die.net/man/2/flock |
| /// [global]: https://docs.microsoft.com/en-us/windows/win32/termserv/kernel-object-namespaces |
| /// [`CreateMutexW`]: https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createmutexw |
| pub fn create(name: &str) -> Result<NamedLock> { |
| if name.is_empty() { |
| return Err(Error::EmptyName); |
| } |
| |
| // On UNIX we want to restrict the user on `/tmp` directory, |
| // so we block the `/` character. |
| // |
| // On Windows `\` character is invalid. |
| // |
| // Both platforms expect null-terminated strings, |
| // so we block null-bytes. |
| if name.chars().any(|c| matches!(c, '\0' | '/' | '\\')) { |
| return Err(Error::InvalidCharacter); |
| } |
| |
| // If `TMPDIR` environment variable is set then use it as the |
| // temporary directory, otherwise use `/tmp`. |
| #[cfg(unix)] |
| let name = std::env::var_os("TMPDIR") |
| .map(PathBuf::from) |
| .unwrap_or_else(|| PathBuf::from("/tmp")) |
| .join(format!("{}.lock", name)); |
| |
| #[cfg(windows)] |
| let name = format!("Global\\{}", name); |
| |
| NamedLock::_create(name) |
| } |
| |
| /// Create/open a named lock on specified path. |
| /// |
| /// # Notes |
| /// |
| /// * This function does not append `.lock` on the path. |
| /// * Parent directories must exist. |
| #[cfg(unix)] |
| #[cfg_attr(docsrs, doc(cfg(unix)))] |
| pub fn with_path<P>(path: P) -> Result<NamedLock> |
| where |
| P: AsRef<Path>, |
| { |
| NamedLock::_create(path.as_ref().to_owned()) |
| } |
| |
| fn _create(name: NameType) -> Result<NamedLock> { |
| let mut opened_locks = OPENED_RAW_LOCKS.lock(); |
| |
| let lock = match opened_locks.get(&name).and_then(|x| x.upgrade()) { |
| Some(lock) => lock, |
| None => { |
| let lock = Arc::new(Mutex::new(RawNamedLock::create(&name)?)); |
| opened_locks.insert(name, Arc::downgrade(&lock)); |
| lock |
| } |
| }; |
| |
| Ok(NamedLock { |
| raw: lock, |
| }) |
| } |
| |
| /// Try to lock named lock. |
| /// |
| /// If it is already locked, `Error::WouldBlock` will be returned. |
| pub fn try_lock(&self) -> Result<NamedLockGuard> { |
| let guard = self.raw.try_lock().ok_or(Error::WouldBlock)?; |
| |
| guard.try_lock()?; |
| |
| Ok(NamedLockGuard { |
| raw: guard, |
| }) |
| } |
| |
| /// Lock named lock. |
| pub fn lock(&self) -> Result<NamedLockGuard> { |
| let guard = self.raw.lock(); |
| |
| guard.lock()?; |
| |
| Ok(NamedLockGuard { |
| raw: guard, |
| }) |
| } |
| } |
| |
| /// Scoped guard that unlocks NamedLock. |
| #[derive(Debug)] |
| pub struct NamedLockGuard<'r> { |
| raw: MutexGuard<'r, RawNamedLock>, |
| } |
| |
| impl<'r> Drop for NamedLockGuard<'r> { |
| fn drop(&mut self) { |
| let _ = self.raw.unlock(); |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use std::env; |
| use std::process::{Child, Command}; |
| use std::thread::sleep; |
| use std::time::Duration; |
| use uuid::Uuid; |
| |
| fn call_proc_num(num: u32, uuid: &str) -> Child { |
| let exe = env::current_exe().expect("no exe"); |
| let mut cmd = Command::new(exe); |
| |
| cmd.env("TEST_CROSS_PROCESS_LOCK_PROC_NUM", num.to_string()) |
| .env("TEST_CROSS_PROCESS_LOCK_UUID", uuid) |
| .arg("tests::cross_process_lock") |
| .spawn() |
| .unwrap() |
| } |
| |
| #[test] |
| fn cross_process_lock() -> Result<()> { |
| let proc_num = env::var("TEST_CROSS_PROCESS_LOCK_PROC_NUM") |
| .ok() |
| .and_then(|v| v.parse().ok()) |
| .unwrap_or(0); |
| let uuid = env::var("TEST_CROSS_PROCESS_LOCK_UUID") |
| .unwrap_or_else(|_| Uuid::new_v4().as_hyphenated().to_string()); |
| |
| match proc_num { |
| 0 => { |
| let mut handle1 = call_proc_num(1, &uuid); |
| sleep(Duration::from_millis(100)); |
| |
| let mut handle2 = call_proc_num(2, &uuid); |
| sleep(Duration::from_millis(200)); |
| |
| let lock = NamedLock::create(&uuid)?; |
| assert!(matches!(lock.try_lock(), Err(Error::WouldBlock))); |
| lock.lock().expect("failed to lock"); |
| |
| assert!(handle2.wait().unwrap().success()); |
| assert!(handle1.wait().unwrap().success()); |
| } |
| 1 => { |
| let lock = |
| NamedLock::create(&uuid).expect("failed to create lock"); |
| |
| let _guard = lock.lock().expect("failed to lock"); |
| assert!(matches!(lock.try_lock(), Err(Error::WouldBlock))); |
| sleep(Duration::from_millis(200)); |
| } |
| 2 => { |
| let lock = |
| NamedLock::create(&uuid).expect("failed to create lock"); |
| |
| assert!(matches!(lock.try_lock(), Err(Error::WouldBlock))); |
| let _guard = lock.lock().expect("failed to lock"); |
| sleep(Duration::from_millis(300)); |
| } |
| _ => unreachable!(), |
| } |
| |
| Ok(()) |
| } |
| |
| #[test] |
| fn edge_cases() -> Result<()> { |
| let uuid = Uuid::new_v4().as_hyphenated().to_string(); |
| let lock1 = NamedLock::create(&uuid)?; |
| let lock2 = NamedLock::create(&uuid)?; |
| |
| { |
| let _guard1 = lock1.try_lock()?; |
| assert!(matches!(lock1.try_lock(), Err(Error::WouldBlock))); |
| assert!(matches!(lock2.try_lock(), Err(Error::WouldBlock))); |
| } |
| |
| { |
| let _guard2 = lock2.try_lock()?; |
| assert!(matches!(lock1.try_lock(), Err(Error::WouldBlock))); |
| assert!(matches!(lock2.try_lock(), Err(Error::WouldBlock))); |
| } |
| |
| Ok(()) |
| } |
| |
| #[test] |
| fn invalid_names() { |
| assert!(matches!(NamedLock::create(""), Err(Error::EmptyName))); |
| |
| assert!(matches!( |
| NamedLock::create("abc/"), |
| Err(Error::InvalidCharacter) |
| )); |
| |
| assert!(matches!( |
| NamedLock::create("abc\\"), |
| Err(Error::InvalidCharacter) |
| )); |
| |
| assert!(matches!( |
| NamedLock::create("abc\0"), |
| Err(Error::InvalidCharacter) |
| )); |
| } |
| } |