#![cfg_attr(docsrs, feature(doc_cfg))]
use std::collections::HashMap;
use std::fmt;
#[cfg(unix)]
use std::path::{Path, PathBuf};
use std::sync::{Arc, Weak};
use once_cell::sync::Lazy;
use parking_lot::lock_api::ArcMutexGuard;
use parking_lot::{Mutex, RawMutex};
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;
static OPENED_RAW_LOCKS: Lazy<
Mutex<HashMap<NameType, Weak<Mutex<RawNamedLock>>>>,
> = Lazy::new(|| Mutex::new(HashMap::new()));
#[derive(Debug)]
pub struct NamedLock {
raw: Arc<Mutex<RawNamedLock>>,
}
impl NamedLock {
pub fn create(name: &str) -> Result<NamedLock> {
if name.is_empty() {
return Err(Error::EmptyName);
}
if name.chars().any(|c| matches!(c, '\0' | '/' | '\\')) {
return Err(Error::InvalidCharacter);
}
#[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)
}
#[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,
})
}
pub fn try_lock(&self) -> Result<NamedLockGuard> {
let guard = self.raw.try_lock_arc().ok_or(Error::WouldBlock)?;
guard.try_lock()?;
Ok(NamedLockGuard {
raw: guard,
})
}
pub fn lock(&self) -> Result<NamedLockGuard> {
let guard = self.raw.lock_arc();
guard.lock()?;
Ok(NamedLockGuard {
raw: guard,
})
}
}
pub struct NamedLockGuard {
raw: ArcMutexGuard<RawMutex, RawNamedLock>,
}
impl Drop for NamedLockGuard {
fn drop(&mut self) {
let _ = self.raw.unlock();
}
}
impl fmt::Debug for NamedLockGuard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NamedLockGuard").field("raw", &*self.raw).finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use static_assertions::assert_impl_all;
use std::env;
use std::fmt::Debug;
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 owned_guard() -> 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!(lock2.try_lock(), Err(Error::WouldBlock)));
drop(lock1);
assert!(matches!(lock2.try_lock(), Err(Error::WouldBlock)));
drop(guard1);
let _guard2 = lock2.try_lock()?;
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)
));
}
#[test]
fn check_traits() {
assert_impl_all!(NamedLock: Debug, Send, Sync);
assert_impl_all!(NamedLockGuard: Debug, Send, Sync);
}
}