use anyhow::{Context, Result};
use std::path::Path;
pub(crate) struct ProjectWriteLock {
_file: std::fs::File,
}
impl ProjectWriteLock {
pub(crate) fn acquire(storage_path: &Path) -> Result<Self> {
let lock_path = storage_path.join("index.lock");
let file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(&lock_path)
.with_context(|| {
format!("Failed to open write-lock file at {}", lock_path.display())
})?;
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) };
if rc != 0 {
let err = std::io::Error::last_os_error();
anyhow::bail!(
"Failed to acquire cross-process write lock at {}: {err}",
lock_path.display()
);
}
}
#[cfg(windows)]
{
windows_lock::lock(&file, true).map_err(|err| {
anyhow::anyhow!(
"Failed to acquire cross-process write lock at {}: {err}",
lock_path.display()
)
})?;
}
Ok(Self { _file: file })
}
pub(crate) fn try_acquire(storage_path: &Path) -> Result<Option<Self>> {
let lock_path = storage_path.join("index.lock");
let file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(&lock_path)
.with_context(|| {
format!("Failed to open write-lock file at {}", lock_path.display())
})?;
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if rc != 0 {
let err = std::io::Error::last_os_error();
let raw = err.raw_os_error();
if raw == Some(libc::EWOULDBLOCK) || raw == Some(libc::EAGAIN) {
return Ok(None);
}
anyhow::bail!(
"Failed to probe write lock at {}: {err}",
lock_path.display()
);
}
}
#[cfg(windows)]
{
match windows_lock::lock(&file, false) {
Ok(()) => {}
Err(err)
if matches!(
err.raw_os_error(),
Some(33) | Some(32) ) =>
{
return Ok(None);
}
Err(err) => {
anyhow::bail!(
"Failed to probe write lock at {}: {err}",
lock_path.display()
);
}
}
}
Ok(Some(Self { _file: file }))
}
}
impl Drop for ProjectWriteLock {
fn drop(&mut self) {
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let _ = unsafe { libc::flock(self._file.as_raw_fd(), libc::LOCK_UN) };
}
#[cfg(windows)]
{
windows_lock::unlock(&self._file);
}
}
}
#[cfg(windows)]
mod windows_lock {
use std::fs::File;
use std::os::windows::io::AsRawHandle;
#[repr(C)]
#[derive(Default)]
struct Overlapped {
internal: usize,
internal_high: usize,
offset_low: u32,
offset_high: u32,
event: usize,
}
const LOCKFILE_EXCLUSIVE_LOCK: u32 = 0x0000_0002;
const LOCKFILE_FAIL_IMMEDIATELY: u32 = 0x0000_0001;
unsafe extern "system" {
fn LockFileEx(
handle: usize,
flags: u32,
reserved: u32,
len_low: u32,
len_high: u32,
overlapped: *mut Overlapped,
) -> i32;
fn UnlockFileEx(
handle: usize,
reserved: u32,
len_low: u32,
len_high: u32,
overlapped: *mut Overlapped,
) -> i32;
}
pub(super) fn lock(file: &File, blocking: bool) -> std::io::Result<()> {
let handle = file.as_raw_handle() as usize;
let mut overlapped = Overlapped::default();
let mut flags = LOCKFILE_EXCLUSIVE_LOCK;
if !blocking {
flags |= LOCKFILE_FAIL_IMMEDIATELY;
}
let ok = unsafe { LockFileEx(handle, flags, 0, 1, 0, &mut overlapped) };
if ok == 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(())
}
}
pub(super) fn unlock(file: &File) {
let handle = file.as_raw_handle() as usize;
let mut overlapped = Overlapped::default();
let _ = unsafe { UnlockFileEx(handle, 0, 1, 0, &mut overlapped) };
}
}