1 #![cfg_attr(docsrs, feature(doc_cfg))] 2 3 //! This crate provides a simple and cross-platform implementation of named locks. 4 //! You can use this to lock sections between processes. 5 //! 6 //! ## Example 7 //! 8 //! ```rust 9 //! use named_lock::NamedLock; 10 //! use named_lock::Result; 11 //! 12 //! fn main() -> Result<()> { 13 //! let lock = NamedLock::create("foobar")?; 14 //! let _guard = lock.lock()?; 15 //! 16 //! // Do something... 17 //! 18 //! Ok(()) 19 //! } 20 //! ``` 21 22 use once_cell::sync::Lazy; 23 use parking_lot::{Mutex, MutexGuard}; 24 use std::collections::HashMap; 25 #[cfg(unix)] 26 use std::path::{Path, PathBuf}; 27 use std::sync::{Arc, Weak}; 28 29 mod error; 30 #[cfg(unix)] 31 mod unix; 32 #[cfg(windows)] 33 mod windows; 34 35 pub use crate::error::*; 36 #[cfg(unix)] 37 use crate::unix::RawNamedLock; 38 #[cfg(windows)] 39 use crate::windows::RawNamedLock; 40 41 #[cfg(unix)] 42 type NameType = PathBuf; 43 #[cfg(windows)] 44 type NameType = String; 45 46 // We handle two edge cases: 47 // 48 // On UNIX systems, after locking a file descriptor you can lock it again 49 // as many times you want. However OS does not keep a counter, so only one 50 // unlock must be performed. To avoid re-locking, we guard it with real mutex. 51 // 52 // On Windows, after locking a `HANDLE` you can create another `HANDLE` for 53 // the same named lock and the same process and Windows will allow you to 54 // re-lock it. To avoid this, we ensure that one `HANDLE` exists in each 55 // process for each name. 56 static OPENED_RAW_LOCKS: Lazy< 57 Mutex<HashMap<NameType, Weak<Mutex<RawNamedLock>>>>, 58 > = Lazy::new(|| Mutex::new(HashMap::new())); 59 60 /// Cross-process lock that is identified by name. 61 #[derive(Debug)] 62 pub struct NamedLock { 63 raw: Arc<Mutex<RawNamedLock>>, 64 } 65 66 impl NamedLock { 67 /// Create/open a named lock. 68 /// 69 /// # UNIX 70 /// 71 /// This will create/open a file and use [`flock`] on it. The path of 72 /// the lock file will be `$TMPDIR/<name>.lock`, or `/tmp/<name>.lock` 73 /// if `TMPDIR` environment variable is not set. 74 /// 75 /// If you want to specify the exact path, then use [NamedLock::with_path]. 76 /// 77 /// # Windows 78 /// 79 /// This will create/open a [global] mutex with [`CreateMutexW`]. 80 /// 81 /// # Notes 82 /// 83 /// * `name` must not be empty, otherwise an error is returned. 84 /// * `name` must not contain `\0`, `/`, nor `\`, otherwise an error is returned. 85 /// 86 /// [`flock`]: https://linux.die.net/man/2/flock 87 /// [global]: https://docs.microsoft.com/en-us/windows/win32/termserv/kernel-object-namespaces 88 /// [`CreateMutexW`]: https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createmutexw create(name: &str) -> Result<NamedLock>89 pub fn create(name: &str) -> Result<NamedLock> { 90 if name.is_empty() { 91 return Err(Error::EmptyName); 92 } 93 94 // On UNIX we want to restrict the user on `/tmp` directory, 95 // so we block the `/` character. 96 // 97 // On Windows `\` character is invalid. 98 // 99 // Both platforms expect null-terminated strings, 100 // so we block null-bytes. 101 if name.chars().any(|c| matches!(c, '\0' | '/' | '\\')) { 102 return Err(Error::InvalidCharacter); 103 } 104 105 // If `TMPDIR` environment variable is set then use it as the 106 // temporary directory, otherwise use `/tmp`. 107 #[cfg(unix)] 108 let name = std::env::var_os("TMPDIR") 109 .map(PathBuf::from) 110 .unwrap_or_else(|| PathBuf::from("/tmp")) 111 .join(format!("{}.lock", name)); 112 113 #[cfg(windows)] 114 let name = format!("Global\\{}", name); 115 116 NamedLock::_create(name) 117 } 118 119 /// Create/open a named lock on specified path. 120 /// 121 /// # Notes 122 /// 123 /// * This function does not append `.lock` on the path. 124 /// * Parent directories must exist. 125 #[cfg(unix)] 126 #[cfg_attr(docsrs, doc(cfg(unix)))] with_path<P>(path: P) -> Result<NamedLock> where P: AsRef<Path>,127 pub fn with_path<P>(path: P) -> Result<NamedLock> 128 where 129 P: AsRef<Path>, 130 { 131 NamedLock::_create(path.as_ref().to_owned()) 132 } 133 _create(name: NameType) -> Result<NamedLock>134 fn _create(name: NameType) -> Result<NamedLock> { 135 let mut opened_locks = OPENED_RAW_LOCKS.lock(); 136 137 let lock = match opened_locks.get(&name).and_then(|x| x.upgrade()) { 138 Some(lock) => lock, 139 None => { 140 let lock = Arc::new(Mutex::new(RawNamedLock::create(&name)?)); 141 opened_locks.insert(name, Arc::downgrade(&lock)); 142 lock 143 } 144 }; 145 146 Ok(NamedLock { 147 raw: lock, 148 }) 149 } 150 151 /// Try to lock named lock. 152 /// 153 /// If it is already locked, `Error::WouldBlock` will be returned. try_lock(&self) -> Result<NamedLockGuard>154 pub fn try_lock(&self) -> Result<NamedLockGuard> { 155 let guard = self.raw.try_lock().ok_or(Error::WouldBlock)?; 156 157 guard.try_lock()?; 158 159 Ok(NamedLockGuard { 160 raw: guard, 161 }) 162 } 163 164 /// Lock named lock. lock(&self) -> Result<NamedLockGuard>165 pub fn lock(&self) -> Result<NamedLockGuard> { 166 let guard = self.raw.lock(); 167 168 guard.lock()?; 169 170 Ok(NamedLockGuard { 171 raw: guard, 172 }) 173 } 174 } 175 176 /// Scoped guard that unlocks NamedLock. 177 #[derive(Debug)] 178 pub struct NamedLockGuard<'r> { 179 raw: MutexGuard<'r, RawNamedLock>, 180 } 181 182 impl<'r> Drop for NamedLockGuard<'r> { drop(&mut self)183 fn drop(&mut self) { 184 let _ = self.raw.unlock(); 185 } 186 } 187 188 #[cfg(test)] 189 mod tests { 190 use super::*; 191 use std::env; 192 use std::process::{Child, Command}; 193 use std::thread::sleep; 194 use std::time::Duration; 195 use uuid::Uuid; 196 call_proc_num(num: u32, uuid: &str) -> Child197 fn call_proc_num(num: u32, uuid: &str) -> Child { 198 let exe = env::current_exe().expect("no exe"); 199 let mut cmd = Command::new(exe); 200 201 cmd.env("TEST_CROSS_PROCESS_LOCK_PROC_NUM", num.to_string()) 202 .env("TEST_CROSS_PROCESS_LOCK_UUID", uuid) 203 .arg("tests::cross_process_lock") 204 .spawn() 205 .unwrap() 206 } 207 208 #[test] cross_process_lock() -> Result<()>209 fn cross_process_lock() -> Result<()> { 210 let proc_num = env::var("TEST_CROSS_PROCESS_LOCK_PROC_NUM") 211 .ok() 212 .and_then(|v| v.parse().ok()) 213 .unwrap_or(0); 214 let uuid = env::var("TEST_CROSS_PROCESS_LOCK_UUID") 215 .unwrap_or_else(|_| Uuid::new_v4().as_hyphenated().to_string()); 216 217 match proc_num { 218 0 => { 219 let mut handle1 = call_proc_num(1, &uuid); 220 sleep(Duration::from_millis(100)); 221 222 let mut handle2 = call_proc_num(2, &uuid); 223 sleep(Duration::from_millis(200)); 224 225 let lock = NamedLock::create(&uuid)?; 226 assert!(matches!(lock.try_lock(), Err(Error::WouldBlock))); 227 lock.lock().expect("failed to lock"); 228 229 assert!(handle2.wait().unwrap().success()); 230 assert!(handle1.wait().unwrap().success()); 231 } 232 1 => { 233 let lock = 234 NamedLock::create(&uuid).expect("failed to create lock"); 235 236 let _guard = lock.lock().expect("failed to lock"); 237 assert!(matches!(lock.try_lock(), Err(Error::WouldBlock))); 238 sleep(Duration::from_millis(200)); 239 } 240 2 => { 241 let lock = 242 NamedLock::create(&uuid).expect("failed to create lock"); 243 244 assert!(matches!(lock.try_lock(), Err(Error::WouldBlock))); 245 let _guard = lock.lock().expect("failed to lock"); 246 sleep(Duration::from_millis(300)); 247 } 248 _ => unreachable!(), 249 } 250 251 Ok(()) 252 } 253 254 #[test] edge_cases() -> Result<()>255 fn edge_cases() -> Result<()> { 256 let uuid = Uuid::new_v4().as_hyphenated().to_string(); 257 let lock1 = NamedLock::create(&uuid)?; 258 let lock2 = NamedLock::create(&uuid)?; 259 260 { 261 let _guard1 = lock1.try_lock()?; 262 assert!(matches!(lock1.try_lock(), Err(Error::WouldBlock))); 263 assert!(matches!(lock2.try_lock(), Err(Error::WouldBlock))); 264 } 265 266 { 267 let _guard2 = lock2.try_lock()?; 268 assert!(matches!(lock1.try_lock(), Err(Error::WouldBlock))); 269 assert!(matches!(lock2.try_lock(), Err(Error::WouldBlock))); 270 } 271 272 Ok(()) 273 } 274 275 #[test] invalid_names()276 fn invalid_names() { 277 assert!(matches!(NamedLock::create(""), Err(Error::EmptyName))); 278 279 assert!(matches!( 280 NamedLock::create("abc/"), 281 Err(Error::InvalidCharacter) 282 )); 283 284 assert!(matches!( 285 NamedLock::create("abc\\"), 286 Err(Error::InvalidCharacter) 287 )); 288 289 assert!(matches!( 290 NamedLock::create("abc\0"), 291 Err(Error::InvalidCharacter) 292 )); 293 } 294 } 295