use std::{
fs::File,
io,
time::{Duration, Instant},
};
#[cfg(all(test, unix))]
mod tests;
#[derive(Debug)]
pub struct SharedFileLock(File);
impl SharedFileLock {
pub fn acquire(file: File) -> io::Result<Self> {
lock_shared(&file)?;
Ok(Self(file))
}
#[must_use]
pub const fn file(&self) -> &File {
&self.0
}
}
impl Drop for SharedFileLock {
fn drop(&mut self) {
let _ = unlock(&self.0);
}
}
pub fn lock_shared(file: &File) -> io::Result<()> {
apply_lock(file, DescriptorOperation::Shared)
}
pub fn lock_exclusive(file: &File) -> io::Result<()> {
apply_lock(file, DescriptorOperation::Exclusive)
}
pub fn try_lock_exclusive(file: &File) -> io::Result<()> {
apply_lock(file, DescriptorOperation::TryExclusive)
}
pub fn unlock(file: &File) -> io::Result<()> {
apply_lock(file, DescriptorOperation::Unlock)
}
#[derive(Clone, Copy)]
enum DescriptorOperation {
Shared,
Exclusive,
TryExclusive,
Unlock,
}
fn apply_lock(file: &File, operation: DescriptorOperation) -> io::Result<()> {
require_regular_descriptor(file)?;
#[cfg(unix)]
{
use rustix::fs::FlockOperation;
let operation = match operation {
DescriptorOperation::Shared => FlockOperation::LockShared,
DescriptorOperation::Exclusive => FlockOperation::LockExclusive,
DescriptorOperation::TryExclusive => FlockOperation::NonBlockingLockExclusive,
DescriptorOperation::Unlock => FlockOperation::Unlock,
};
rustix::fs::flock(file, operation).map_err(io::Error::from)
}
#[cfg(not(unix))]
{
let _ = operation;
Err(io::Error::from(io::ErrorKind::Unsupported))
}
}
fn require_regular_descriptor(file: &File) -> io::Result<()> {
if file.metadata()?.is_file() {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"lock requires a regular file",
))
}
}
pub fn lock_exclusive_with_wait(
file: &File,
poll_interval: Duration,
waiting: impl FnMut(Duration) -> io::Result<()>,
) -> io::Result<Duration> {
if poll_interval.is_zero() || !file.metadata()?.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"lock requires a regular file and nonzero polling interval",
));
}
#[cfg(unix)]
{
wait_for_lock(poll_interval, waiting, || {
rustix::fs::flock(file, rustix::fs::FlockOperation::NonBlockingLockExclusive)
.map_err(|error| io::Error::from_raw_os_error(error.raw_os_error()))
})
}
#[cfg(not(unix))]
{
let _ = waiting;
Err(io::Error::new(
io::ErrorKind::Unsupported,
"descriptor lock waiting is unsupported",
))
}
}
#[cfg(unix)]
fn wait_for_lock(
poll_interval: Duration,
mut waiting: impl FnMut(Duration) -> io::Result<()>,
mut acquire: impl FnMut() -> io::Result<()>,
) -> io::Result<Duration> {
let started = Instant::now();
loop {
match acquire() {
Ok(()) => return Ok(started.elapsed()),
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
waiting(started.elapsed())?;
std::thread::sleep(poll_interval);
}
Err(error) => return Err(error),
}
}
}