named_lock/
lib.rs

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