diff options
author | Elie Kheirallah <khei@google.com> | 2023-04-28 22:30:11 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2023-04-28 22:30:11 +0000 |
commit | f11391f865539df4f20c657d5ec2aede7dadfe23 (patch) | |
tree | fe7bf0f76ec4187fddac28beafd69348da03ac13 | |
parent | f827f0ac19518ff016b1ba880e25f7c6153bfe0d (diff) | |
parent | c443e836d5c56b83f7b2e87c0876bda29cb910c0 (diff) | |
download | named-lock-f11391f865539df4f20c657d5ec2aede7dadfe23.tar.gz |
Import named-lock am: 10099b967d am: a251f02d3c am: 2918246d30 am: 8c455a7a24 am: 37ff3bdb97 am: c443e836d5
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/named-lock/+/2566511
Change-Id: Ib5a5f26f82295c6153d3048f8a341a48b698175c
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r-- | CHANGELOG.md | 40 | ||||
-rw-r--r-- | Cargo.toml | 64 | ||||
-rw-r--r-- | Cargo.toml.orig | 31 | ||||
-rw-r--r-- | LICENSE | 21 | ||||
-rw-r--r-- | METADATA | 19 | ||||
-rw-r--r-- | MODULE_LICENSE_MIT | 0 | ||||
-rw-r--r-- | OWNERS | 5 | ||||
-rw-r--r-- | README.md | 43 | ||||
-rw-r--r-- | rustfmt.toml | 3 | ||||
-rw-r--r-- | src/error.rs | 26 | ||||
-rw-r--r-- | src/lib.rs | 294 | ||||
-rw-r--r-- | src/unix.rs | 63 | ||||
-rw-r--r-- | src/windows.rs | 73 |
13 files changed, 682 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bb403ab --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,40 @@ +# 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 diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e939ba4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,64 @@ +# 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 <psyberbits@gmail.com>"] +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", +] diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..3e75c37 --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,31 @@ +[package] +name = "named-lock" +version = "0.3.0" +authors = ["oblique <psyberbits@gmail.com>"] +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"] @@ -0,0 +1,21 @@ +MIT 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. diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..4083bb6 --- /dev/null +++ b/METADATA @@ -0,0 +1,19 @@ +name: "named-lock" +description: "Cross-platform implementation of cross-process named locks" +third_party { + url { + type: HOMEPAGE + value: "https://crates.io/crates/named-lock" + } + url { + type: ARCHIVE + value: "https://static.crates.io/crates/named-lock/named-lock-0.3.0.crate" + } + version: "0.3.0" + license_type: NOTICE + last_upgrade_date { + year: 2023 + month: 4 + day: 19 + } +} diff --git a/MODULE_LICENSE_MIT b/MODULE_LICENSE_MIT new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MODULE_LICENSE_MIT @@ -0,0 +1,5 @@ +include platform/prebuilts/rust:master:/OWNERS +devinmoore@google.com +fmayle@google.com +khei@google.com +smoreland@google.com diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d3dd2e --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# 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/<name>.lock`, or `/tmp/<name>.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 diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..6a7d0cd --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +max_width = 80 +use_small_heuristics = "off" +edition = "2018" diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..7a1e2d4 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,26 @@ +use thiserror::Error; + +/// Type alias to `Result<T, Error>`. +pub type Result<T, E = Error> = std::result::Result<T, E>; + +/// 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, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..15e5032 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,294 @@ +#![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<HashMap<NameType, Weak<Mutex<RawNamedLock>>>>, +> = Lazy::new(|| Mutex::new(HashMap::new())); + +/// Cross-process lock that is identified by name. +#[derive(Debug)] +pub struct NamedLock { + raw: Arc<Mutex<RawNamedLock>>, +} + +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/<name>.lock`, or `/tmp/<name>.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<NamedLock> { + 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<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, + }) + } + + /// Try to lock named lock. + /// + /// If it is already locked, `Error::WouldBlock` will be returned. + pub fn try_lock(&self) -> Result<NamedLockGuard> { + 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<NamedLockGuard> { + 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) + )); + } +} diff --git a/src/unix.rs b/src/unix.rs new file mode 100644 index 0000000..58b90ce --- /dev/null +++ b/src/unix.rs @@ -0,0 +1,63 @@ +use 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<RawNamedLock> { + 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(()) +} diff --git a/src/windows.rs b/src/windows.rs new file mode 100644 index 0000000..8f65877 --- /dev/null +++ b/src/windows.rs @@ -0,0 +1,73 @@ +use 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<RawNamedLock> { + 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); + } + } +} |