use std::path::{Path, PathBuf};
pub struct LockClaim {
#[allow(dead_code)] file: std::fs::File,
}
pub fn try_acquire(path: &Path) -> Option<LockClaim> {
open_claim_file(path).map(|file| LockClaim { file })
}
pub fn is_held(path: &Path) -> bool {
#[cfg(windows)]
{
path.exists()
}
#[cfg(unix)]
{
use nix::fcntl::{FlockArg, flock};
use std::os::unix::io::AsRawFd;
let Ok(file) = std::fs::OpenOptions::new().read(true).open(path) else {
return false;
};
if flock(file.as_raw_fd(), FlockArg::LockSharedNonblock).is_ok() {
let _ = flock(file.as_raw_fd(), FlockArg::Unlock);
false
} else {
true
}
}
}
pub async fn wait_release(path: &Path) -> std::io::Result<()> {
#[cfg(windows)]
{
wait_release_windows(path.to_path_buf()).await
}
#[cfg(unix)]
{
wait_release_unix(path.to_path_buf()).await
}
}
pub async fn wait_acquire(path: &Path) -> std::io::Result<LockClaim> {
#[cfg(windows)]
{
wait_acquire_windows(path.to_path_buf()).await
}
#[cfg(unix)]
{
wait_acquire_unix(path.to_path_buf()).await
}
}
#[cfg(windows)]
fn open_claim_file(path: &Path) -> Option<std::fs::File> {
use std::os::windows::ffi::OsStrExt;
use std::os::windows::io::FromRawHandle;
use windows_sys::Win32::Foundation::{
GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE,
};
use windows_sys::Win32::Storage::FileSystem::{
CREATE_NEW, CreateFileW, FILE_ATTRIBUTE_NORMAL,
FILE_FLAG_DELETE_ON_CLOSE, FILE_SHARE_READ,
};
let wide: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let handle = unsafe {
CreateFileW(
wide.as_ptr(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
std::ptr::null(),
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE,
std::ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE {
return None;
}
Some(unsafe { std::fs::File::from_raw_handle(handle as _) })
}
#[cfg(windows)]
async fn wait_release_windows(path: PathBuf) -> std::io::Result<()> {
tokio::task::spawn_blocking(move || windows_wait_for_file_gone(&path))
.await
.map_err(|e| std::io::Error::other(format!("join: {e}")))?
}
#[cfg(windows)]
async fn wait_acquire_windows(path: PathBuf) -> std::io::Result<LockClaim> {
loop {
if let Some(claim) = try_acquire(&path) {
return Ok(claim);
}
wait_release_windows(path.clone()).await?;
}
}
#[cfg(windows)]
fn windows_wait_for_file_gone(path: &Path) -> std::io::Result<()> {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Foundation::{
INVALID_HANDLE_VALUE, WAIT_FAILED, WAIT_OBJECT_0,
};
use windows_sys::Win32::Storage::FileSystem::{
FILE_NOTIFY_CHANGE_FILE_NAME, FindCloseChangeNotification,
FindFirstChangeNotificationW, FindNextChangeNotification,
};
use windows_sys::Win32::System::Threading::{INFINITE, WaitForSingleObject};
let parent = match path.parent() {
Some(p) => p,
None => return Ok(()),
};
if !path.exists() {
return Ok(());
}
let parent_wide: Vec<u16> = parent
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let handle = unsafe {
FindFirstChangeNotificationW(
parent_wide.as_ptr(),
0,
FILE_NOTIFY_CHANGE_FILE_NAME,
)
};
if handle == INVALID_HANDLE_VALUE {
return Err(std::io::Error::last_os_error());
}
struct Guard(isize);
impl Drop for Guard {
fn drop(&mut self) {
unsafe {
FindCloseChangeNotification(self.0 as _);
}
}
}
let _guard = Guard(handle as isize);
loop {
if !path.exists() {
return Ok(());
}
let rc = unsafe { WaitForSingleObject(handle as _, INFINITE) };
if rc == WAIT_FAILED {
return Err(std::io::Error::last_os_error());
}
if rc != WAIT_OBJECT_0 {
return Err(std::io::Error::other(format!(
"unexpected WaitForSingleObject result: {rc}"
)));
}
unsafe { FindNextChangeNotification(handle as _) };
}
}
#[cfg(unix)]
fn open_claim_file(path: &Path) -> Option<std::fs::File> {
match try_create_locked(path) {
Ok(file) => return Some(file),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(_) => return None,
}
take_existing_lock(path)
}
#[cfg(unix)]
fn try_create_locked(path: &Path) -> std::io::Result<std::fs::File> {
use nix::fcntl::{FlockArg, flock};
use std::os::unix::io::AsRawFd;
use std::os::unix::fs::OpenOptionsExt;
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.mode(0o644)
.open(path)?;
if flock(file.as_raw_fd(), FlockArg::LockExclusiveNonblock).is_err() {
drop(file);
let _ = std::fs::remove_file(path);
return Err(std::io::Error::other("flock failed"));
}
Ok(file)
}
#[cfg(unix)]
fn take_existing_lock(path: &Path) -> Option<std::fs::File> {
use nix::fcntl::{FlockArg, flock};
use std::os::unix::io::AsRawFd;
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.ok()?;
if flock(file.as_raw_fd(), FlockArg::LockExclusiveNonblock).is_err() {
return None;
}
Some(file)
}
#[cfg(unix)]
async fn wait_release_unix(path: PathBuf) -> std::io::Result<()> {
tokio::task::spawn_blocking(move || unix_wait_for_release(&path))
.await
.map_err(|e| std::io::Error::other(format!("join: {e}")))?
}
#[cfg(unix)]
async fn wait_acquire_unix(path: PathBuf) -> std::io::Result<LockClaim> {
tokio::task::spawn_blocking(move || unix_wait_for_acquire(&path))
.await
.map_err(|e| std::io::Error::other(format!("join: {e}")))?
.map(|file| LockClaim { file })
}
#[cfg(unix)]
fn unix_wait_for_release(path: &Path) -> std::io::Result<()> {
use nix::fcntl::{FlockArg, flock};
use std::os::unix::io::AsRawFd;
let file = match std::fs::OpenOptions::new().read(true).open(path) {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e),
};
flock(file.as_raw_fd(), FlockArg::LockShared)
.map_err(|e| std::io::Error::other(format!("flock LOCK_SH: {e}")))?;
let _ = flock(file.as_raw_fd(), FlockArg::Unlock);
Ok(())
}
#[cfg(unix)]
fn unix_wait_for_acquire(path: &Path) -> std::io::Result<std::fs::File> {
use nix::fcntl::{FlockArg, flock};
use std::os::unix::io::AsRawFd;
use std::os::unix::fs::OpenOptionsExt;
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.mode(0o644)
.open(path)?;
flock(file.as_raw_fd(), FlockArg::LockExclusive)
.map_err(|e| std::io::Error::other(format!("flock LOCK_EX: {e}")))?;
Ok(file)
}