#![allow(unsafe_code)]
use std::fs::{File, OpenOptions};
use std::path::Path;
use crate::Error;
use crate::error::Result;
#[derive(Debug)]
pub struct FileLock {
file: Option<File>,
path: std::path::PathBuf,
exclusive: bool,
}
impl FileLock {
pub fn try_lock(path: impl Into<std::path::PathBuf>, exclusive: bool) -> Result<Self> {
let path = path.into();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(Error::Io)?;
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.map_err(Error::Io)?;
#[cfg(unix)]
{
let fd = std::os::unix::io::AsRawFd::as_raw_fd(&file);
let result = if exclusive {
unsafe { libc::flock(fd, 0x02 | 0x04) }
} else {
unsafe { libc::flock(fd, 0x01 | 0x04) }
};
if result != 0 {
return Err(Error::WorkspaceLocked);
}
Ok(Self {
file: Some(file),
path,
exclusive,
})
}
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
use windows_sys::Win32::Storage::FileSystem::{
LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY, LockFileEx,
};
let handle = std::os::windows::io::AsRawHandle::as_raw_handle(&file);
let mut overlapped = std::mem::MaybeUninit::zeroed();
let result = unsafe {
LockFileEx(
handle,
if exclusive {
LOCKFILE_EXCLUSIVE_LOCK
} else {
0
} | LOCKFILE_FAIL_IMMEDIATELY,
0,
0xFFFFFFFF,
0xFFFFFFFF,
overlapped.as_mut_ptr(),
)
};
if result == 0 {
return Err(Error::WorkspaceLocked);
}
Ok(Self {
file: Some(file),
path,
exclusive,
})
}
#[cfg(not(any(unix, windows)))]
{
Ok(Self {
file: Some(file),
path,
exclusive,
})
}
}
pub fn try_lock_no_wait(
path: impl Into<std::path::PathBuf>,
exclusive: bool,
) -> Result<Option<Self>> {
match Self::try_lock(&path.into(), exclusive) {
Ok(lock) => Ok(Some(lock)),
Err(Error::WorkspaceLocked) => Ok(None),
Err(e) => Err(e),
}
}
pub fn is_locked(path: impl Into<std::path::PathBuf>) -> bool {
Self::try_lock(&path.into(), false).is_err()
}
pub fn unlock(mut self) {
if let Some(file) = self.file.take() {
drop(file);
}
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn is_exclusive(&self) -> bool {
self.exclusive
}
}
impl Drop for FileLock {
fn drop(&mut self) {
if let Some(file) = self.file.take() {
drop(file);
}
}
}
pub struct ScopedLock {
lock: Option<FileLock>,
}
impl ScopedLock {
pub fn new(path: impl Into<std::path::PathBuf>, exclusive: bool) -> Result<Self> {
let lock = FileLock::try_lock(path, exclusive)?;
Ok(Self { lock: Some(lock) })
}
pub fn release(mut self) {
if let Some(lock) = self.lock.take() {
lock.unlock();
}
}
}
impl Drop for ScopedLock {
fn drop(&mut self) {
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_file_lock_acquire_release() {
let temp = TempDir::new().unwrap();
let lock_path = temp.path().join("test.lock");
let lock = FileLock::try_lock(&lock_path, true).unwrap();
assert!(lock.is_exclusive());
lock.unlock();
}
#[test]
fn test_file_lock_conflict() {
let temp = TempDir::new().unwrap();
let lock_path = temp.path().join("conflict.lock");
let _lock1 = FileLock::try_lock(&lock_path, true).unwrap();
let result = FileLock::try_lock(&lock_path, true);
assert!(matches!(result, Err(Error::WorkspaceLocked)));
}
#[test]
fn test_file_lock_shared() {
let temp = TempDir::new().unwrap();
let lock_path = temp.path().join("shared.lock");
let lock1 = FileLock::try_lock(&lock_path, false).unwrap();
assert!(!lock1.is_exclusive());
let lock2 = FileLock::try_lock(&lock_path, false).unwrap();
assert!(!lock2.is_exclusive());
let result = FileLock::try_lock(&lock_path, true);
assert!(matches!(result, Err(Error::WorkspaceLocked)));
lock1.unlock();
lock2.unlock();
}
#[test]
fn test_scoped_lock() {
let temp = TempDir::new().unwrap();
let lock_path = temp.path().join("scoped.lock");
{
let _scoped = ScopedLock::new(&lock_path, true).unwrap();
let result = FileLock::try_lock(&lock_path, true);
assert!(matches!(result, Err(Error::WorkspaceLocked)));
}
let _lock = FileLock::try_lock(&lock_path, true).unwrap();
}
}