use anyhow::{Context, Result};
use std::fs::{File, OpenOptions};
use std::path::{Path, PathBuf};
#[cfg(unix)]
mod sys {
use std::os::raw::c_int;
extern "C" {
pub fn flock(fd: c_int, operation: c_int) -> c_int;
}
pub const LOCK_SH: c_int = 1;
pub const LOCK_EX: c_int = 2;
pub const LOCK_NB: c_int = 4;
pub const LOCK_UN: c_int = 8;
}
#[derive(Debug)]
pub struct FileLock {
file: File,
path: PathBuf,
}
impl FileLock {
pub fn acquire_exclusive(path: &Path) -> Result<Self> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!("Failed to create parent directory for lock: {:?}", parent)
})?;
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)
.with_context(|| format!("Failed to open lock file: {:?}", path))?;
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let fd = file.as_raw_fd();
let res = unsafe { sys::flock(fd, sys::LOCK_EX) };
if res != 0 {
let err = std::io::Error::last_os_error();
anyhow::bail!("Failed to acquire exclusive lock on {:?}: {}", path, err);
}
}
Ok(Self {
file,
path: path.to_path_buf(),
})
}
pub fn try_acquire_exclusive(path: &Path) -> Result<Option<Self>> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!("Failed to create parent directory for lock: {:?}", parent)
})?;
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)
.with_context(|| format!("Failed to open lock file: {:?}", path))?;
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let fd = file.as_raw_fd();
let res = unsafe { sys::flock(fd, sys::LOCK_EX | sys::LOCK_NB) };
if res != 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc_ewouldblock()) {
return Ok(None);
}
anyhow::bail!(
"Failed to try-acquire exclusive lock on {:?}: {}",
path,
err
);
}
}
Ok(Some(Self {
file,
path: path.to_path_buf(),
}))
}
pub fn acquire_shared(path: &Path) -> Result<Self> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!("Failed to create parent directory for lock: {:?}", parent)
})?;
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)
.with_context(|| format!("Failed to open lock file: {:?}", path))?;
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let fd = file.as_raw_fd();
let res = unsafe { sys::flock(fd, sys::LOCK_SH) };
if res != 0 {
let err = std::io::Error::last_os_error();
anyhow::bail!("Failed to acquire shared lock on {:?}: {}", path, err);
}
}
Ok(Self {
file,
path: path.to_path_buf(),
})
}
pub fn downgrade_to_shared(&self) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let fd = self.file.as_raw_fd();
let res = unsafe { sys::flock(fd, sys::LOCK_SH) };
if res != 0 {
let err = std::io::Error::last_os_error();
anyhow::bail!("Failed to downgrade lock on {:?}: {}", self.path, err);
}
}
Ok(())
}
pub fn acquire_exclusive_timeout(
path: &Path,
timeout: std::time::Duration,
) -> Result<Option<Self>> {
let start = std::time::Instant::now();
let mut sleep_ms = 1u64;
loop {
if let Some(lock) = Self::try_acquire_exclusive(path)? {
return Ok(Some(lock));
}
if start.elapsed() >= timeout {
return Ok(None);
}
std::thread::sleep(std::time::Duration::from_millis(sleep_ms));
sleep_ms = (sleep_ms * 2).min(10);
}
}
pub fn path(&self) -> &Path {
&self.path
}
}
#[cfg(unix)]
fn libc_ewouldblock() -> i32 {
libc::EWOULDBLOCK
}
impl Drop for FileLock {
fn drop(&mut self) {
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let fd = self.file.as_raw_fd();
unsafe {
sys::flock(fd, sys::LOCK_UN);
}
}
}
}