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