diff options
Diffstat (limited to 'src/lib.rs')
-rw-r--r-- | src/lib.rs | 294 |
1 files changed, 294 insertions, 0 deletions
diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..15e5032 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,294 @@ +#![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) + )); + } +} |