use std::{
fs::File,
io,
time::{Duration, Instant},
};
#[cfg(all(test, unix))]
mod tests;
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 unlock(file: &File) -> io::Result<()> {
apply_lock(file, DescriptorOperation::Unlock)
}
#[derive(Clone, Copy)]
enum DescriptorOperation {
Shared,
Exclusive,
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::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),
}
}
}