named-lock-0.3.0/.cargo_vcs_info.json0000644000000001360000000000100130460ustar { "git": { "sha1": "70f6eb93be9f69434e677232fa95b98eedd80077" }, "path_in_vcs": "" }named-lock-0.3.0/.github/workflows/ci.yaml000064400000000000000000000027461046102023000165230ustar 00000000000000name: CI on: [push, pull_request] jobs: tests: name: Tests runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] env: RUST_BACKTRACE: 1 steps: - name: Install toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable - name: Checkout sources uses: actions/checkout@v1 - name: Build uses: actions-rs/cargo@v1 with: command: build args: --tests --examples - name: Run tests uses: actions-rs/cargo@v1 with: command: test lints: name: Lints runs-on: ubuntu-latest steps: - name: Install toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true components: rustfmt, clippy - uses: actions/checkout@v1 - name: Run cargo fmt uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check - name: Run cargo clippy uses: actions-rs/cargo@v1 with: command: clippy args: -- -D clippy::all named-lock-0.3.0/.gitignore000064400000000000000000000000231046102023000136210ustar 00000000000000/target Cargo.lock named-lock-0.3.0/CHANGELOG.md000064400000000000000000000023771046102023000134600ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [unreleased] ## [0.3.0] ### Changed - `NamedLock::create` now rejects names that contain `\0` character ([#5]) - `NamedLock::create` now rejects empty names - Upgrade all dependencies ## [0.2.0] ### Added - Added `NamedLock::with_path` on UNIX ([#2], [#4]) ### Changed - `NamedLock::create` on UNIX respects `TMPDIR` environment variable ([#1], [#4]) - `NamedLock::create` now rejects names that contain `/` or `\` characters ([#2], [#4]) - `NamedLock::create` on Windows explicitly creates a global mutex - `Error::CreateFailed` now has the source of the error - Upgrade all dependencies [unreleased]: https://github.com/oblique/named-lock/compare/0.3.0...HEAD [0.2.0]: https://github.com/oblique/named-lock/compare/0.1.1...0.2.0 [0.3.0]: https://github.com/oblique/named-lock/compare/0.2.0...0.3.0 [#5]: https://github.com/oblique/named-lock/issues/5 [#4]: https://github.com/oblique/named-lock/issues/4 [#2]: https://github.com/oblique/named-lock/issues/2 [#1]: https://github.com/oblique/named-lock/issues/1 named-lock-0.3.0/Cargo.toml0000644000000026410000000000100110470ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "named-lock" version = "0.3.0" authors = ["oblique "] description = "Cross-platform implementation of cross-process named locks" readme = "README.md" keywords = [ "process", "inter-process", "cross-process", "flock", "CreateMutexW", ] categories = ["os"] license = "MIT" repository = "https://github.com/oblique/named-lock" [package.metadata.docs.rs] all-features = true rustdoc-args = [ "--cfg", "docsrs", ] [dependencies.once_cell] version = "1.14.0" [dependencies.parking_lot] version = "0.12.1" [dependencies.thiserror] version = "1.0.35" [dev-dependencies.uuid] version = "1.1.2" features = ["v4"] [target."cfg(unix)".dependencies.libc] version = "0.2.132" [target."cfg(windows)".dependencies.widestring] version = "1.0.2" [target."cfg(windows)".dependencies.winapi] version = "0.3.9" features = [ "handleapi", "synchapi", "winbase", "winnt", "winerror", ] named-lock-0.3.0/Cargo.toml.orig000064400000000000000000000014661046102023000145340ustar 00000000000000[package] name = "named-lock" version = "0.3.0" authors = ["oblique "] edition = "2018" license = "MIT" readme = "README.md" description = "Cross-platform implementation of cross-process named locks" categories = ["os"] keywords = ["process", "inter-process", "cross-process", "flock", "CreateMutexW"] repository = "https://github.com/oblique/named-lock" [dependencies] thiserror = "1.0.35" once_cell = "1.14.0" parking_lot = "0.12.1" [target.'cfg(unix)'.dependencies] libc = "0.2.132" [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = ["handleapi", "synchapi", "winbase", "winnt", "winerror"] } widestring = "1.0.2" [dev-dependencies] uuid = { version = "1.1.2", features = ["v4"] } [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] named-lock-0.3.0/LICENSE000064400000000000000000000020501046102023000126400ustar 00000000000000MIT License Copyright (c) 2020 oblique Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. named-lock-0.3.0/README.md000064400000000000000000000022421046102023000131150ustar 00000000000000# named-lock [![license][license badge]][license] [![crates.io][crate badge]][crate] [![docs][docs badge]][docs] 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(()) } ``` ## Implementation On UNIX this is implemented by using files and [`flock`]. The path of the created lock file will be `$TMPDIR/.lock`, or `/tmp/.lock` if `TMPDIR` environment variable is not set. On Windows this is implemented by creating named mutex with [`CreateMutexW`]. [license]: LICENSE [license badge]: https://img.shields.io/github/license/oblique/named-lock [crate]: https://crates.io/crates/named-lock [crate badge]: https://img.shields.io/crates/v/named-lock [docs]: https://docs.rs/named-lock [docs badge]: https://docs.rs/named-lock/badge.svg [`flock`]: https://linux.die.net/man/2/flock [`CreateMutexW`]: https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createmutexw named-lock-0.3.0/rustfmt.toml000064400000000000000000000000751046102023000142410ustar 00000000000000max_width = 80 use_small_heuristics = "off" edition = "2018" named-lock-0.3.0/src/error.rs000064400000000000000000000011111046102023000141160ustar 00000000000000use thiserror::Error; /// Type alias to `Result`. pub type Result = std::result::Result; /// Error type of this crate. #[derive(Debug, Error)] pub enum Error { #[error("Invalid character in name")] InvalidCharacter, #[error("Name must not be empty")] EmptyName, #[error("Failed to create named lock: {0}")] CreateFailed(#[source] std::io::Error), #[error("Failed to lock named lock")] LockFailed, #[error("Failed to unlock named lock")] UnlockFailed, #[error("Named lock would block")] WouldBlock, } named-lock-0.3.0/src/lib.rs000064400000000000000000000206201046102023000135410ustar 00000000000000#![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>>>, > = Lazy::new(|| Mutex::new(HashMap::new())); /// Cross-process lock that is identified by name. #[derive(Debug)] pub struct NamedLock { raw: Arc>, } 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/.lock`, or `/tmp/.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 { 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

(path: P) -> Result where P: AsRef, { NamedLock::_create(path.as_ref().to_owned()) } fn _create(name: NameType) -> Result { 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 { 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 { 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) )); } } named-lock-0.3.0/src/unix.rs000064400000000000000000000031771046102023000137660ustar 00000000000000use libc::{LOCK_EX, LOCK_NB, LOCK_UN}; use std::fs::{File, OpenOptions}; use std::io; use std::os::unix::io::{AsRawFd, RawFd}; use std::path::Path; use crate::error::*; #[derive(Debug)] pub(crate) struct RawNamedLock { lock_file: File, } impl RawNamedLock { pub(crate) fn create(lock_path: &Path) -> Result { let lock_file = OpenOptions::new() .write(true) .create_new(true) .open(&lock_path) .or_else(|_| OpenOptions::new().write(true).open(&lock_path)) .map_err(Error::CreateFailed)?; Ok(RawNamedLock { lock_file, }) } pub(crate) fn try_lock(&self) -> Result<()> { unsafe { flock(self.lock_file.as_raw_fd(), LOCK_EX | LOCK_NB) } } pub(crate) fn lock(&self) -> Result<()> { unsafe { flock(self.lock_file.as_raw_fd(), LOCK_EX) } } pub(crate) fn unlock(&self) -> Result<()> { unsafe { flock(self.lock_file.as_raw_fd(), LOCK_UN) } } } unsafe fn flock(fd: RawFd, operation: i32) -> Result<()> { loop { let rc = libc::flock(fd, operation); if rc < 0 { let err = io::Error::last_os_error(); if err.kind() == io::ErrorKind::Interrupted { continue; } else if err.kind() == io::ErrorKind::WouldBlock { return Err(Error::WouldBlock); } else if (operation & LOCK_EX) == LOCK_EX { return Err(Error::LockFailed); } else if (operation & LOCK_UN) == LOCK_UN { return Err(Error::UnlockFailed); } } break; } Ok(()) } named-lock-0.3.0/src/windows.rs000064400000000000000000000034721046102023000144730ustar 00000000000000use std::io; use std::ptr; use widestring::WideCString; use winapi::shared::winerror::WAIT_TIMEOUT; use winapi::um::handleapi::CloseHandle; use winapi::um::synchapi::{CreateMutexW, ReleaseMutex, WaitForSingleObject}; use winapi::um::winbase::{INFINITE, WAIT_ABANDONED, WAIT_OBJECT_0}; use winapi::um::winnt::HANDLE; use crate::error::*; #[derive(Debug)] pub(crate) struct RawNamedLock { handle: HANDLE, } unsafe impl Sync for RawNamedLock {} unsafe impl Send for RawNamedLock {} impl RawNamedLock { pub(crate) fn create(name: &str) -> Result { let name = WideCString::from_str(name).unwrap(); let handle = unsafe { CreateMutexW(ptr::null_mut(), 0, name.as_ptr()) }; if handle.is_null() { Err(Error::CreateFailed(io::Error::last_os_error())) } else { Ok(RawNamedLock { handle, }) } } pub(crate) fn try_lock(&self) -> Result<()> { let rc = unsafe { WaitForSingleObject(self.handle, 0) }; if rc == WAIT_OBJECT_0 || rc == WAIT_ABANDONED { Ok(()) } else if rc == WAIT_TIMEOUT { Err(Error::WouldBlock) } else { Err(Error::LockFailed) } } pub(crate) fn lock(&self) -> Result<()> { let rc = unsafe { WaitForSingleObject(self.handle, INFINITE) }; if rc == WAIT_OBJECT_0 || rc == WAIT_ABANDONED { Ok(()) } else { Err(Error::LockFailed) } } pub(crate) fn unlock(&self) -> Result<()> { let rc = unsafe { ReleaseMutex(self.handle) }; if rc == 0 { Err(Error::UnlockFailed) } else { Ok(()) } } } impl Drop for RawNamedLock { fn drop(&mut self) { unsafe { CloseHandle(self.handle); } } }