#![allow(unsafe_code)]
use crate::error::GpkgError;
use std::fs::OpenOptions;
use std::os::unix::io::{IntoRawFd, RawFd};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LockMode {
Shared,
Exclusive,
}
impl LockMode {
fn fcntl_type(self) -> libc::c_short {
match self {
LockMode::Shared => libc::F_RDLCK as libc::c_short,
LockMode::Exclusive => libc::F_WRLCK as libc::c_short,
}
}
}
#[derive(Debug)]
pub struct GpkgFileLock {
fd: RawFd,
path: PathBuf,
mode: LockMode,
locked: bool,
}
impl GpkgFileLock {
pub fn acquire<P: AsRef<Path>>(path: P, mode: LockMode) -> Result<Self, GpkgError> {
let path = path.as_ref();
let fd = open_fd(path, mode)?;
let mut lock = build_flock(mode.fcntl_type());
let rc = unsafe { libc::fcntl(fd, libc::F_SETLKW, &mut lock as *mut libc::flock) };
if rc == -1 {
let err = std::io::Error::last_os_error();
close_fd(fd);
return Err(GpkgError::LockingError(format!(
"failed to acquire {mode:?} lock on {}: {err}",
path.display()
)));
}
Ok(Self {
fd,
path: path.to_path_buf(),
mode,
locked: true,
})
}
pub fn try_acquire<P: AsRef<Path>>(path: P, mode: LockMode) -> Result<Option<Self>, GpkgError> {
let path = path.as_ref();
let fd = open_fd(path, mode)?;
let mut lock = build_flock(mode.fcntl_type());
let rc = unsafe { libc::fcntl(fd, libc::F_SETLK, &mut lock as *mut libc::flock) };
if rc == -1 {
let err = std::io::Error::last_os_error();
let raw = err.raw_os_error();
close_fd(fd);
if raw == Some(libc::EAGAIN) || raw == Some(libc::EACCES) {
return Ok(None);
}
return Err(GpkgError::LockingError(format!(
"failed to try-acquire {mode:?} lock on {}: {err}",
path.display()
)));
}
Ok(Some(Self {
fd,
path: path.to_path_buf(),
mode,
locked: true,
}))
}
pub fn release(&mut self) -> Result<(), GpkgError> {
if !self.locked {
return Ok(());
}
let mut lock = build_flock(libc::F_UNLCK as libc::c_short);
let rc = unsafe { libc::fcntl(self.fd, libc::F_SETLK, &mut lock as *mut libc::flock) };
if rc == -1 {
let err = std::io::Error::last_os_error();
return Err(GpkgError::LockingError(format!(
"failed to release lock on {}: {err}",
self.path.display()
)));
}
self.locked = false;
Ok(())
}
#[must_use]
pub fn mode(&self) -> LockMode {
self.mode
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn is_locked(&self) -> bool {
self.locked
}
}
impl Drop for GpkgFileLock {
fn drop(&mut self) {
let _ = self.release();
close_fd(self.fd);
self.locked = false;
}
}
pub fn lock_for_read<P: AsRef<Path>>(path: P) -> Result<GpkgFileLock, GpkgError> {
GpkgFileLock::acquire(path, LockMode::Shared)
}
pub fn lock_for_write<P: AsRef<Path>>(path: P) -> Result<GpkgFileLock, GpkgError> {
GpkgFileLock::acquire(path, LockMode::Exclusive)
}
fn open_fd(path: &Path, mode: LockMode) -> Result<RawFd, GpkgError> {
let mut options = OpenOptions::new();
match mode {
LockMode::Shared => {
options.read(true);
}
LockMode::Exclusive => {
options.read(true).write(true);
}
}
let file = options.open(path).map_err(|err| {
GpkgError::LockingError(format!(
"failed to open {} for {mode:?} locking: {err}",
path.display()
))
})?;
Ok(file.into_raw_fd())
}
fn build_flock(l_type: libc::c_short) -> libc::flock {
let mut lock: libc::flock = unsafe { std::mem::zeroed() };
lock.l_type = l_type;
lock.l_whence = libc::SEEK_SET as libc::c_short;
lock.l_start = 0;
lock.l_len = 0;
lock
}
fn close_fd(fd: RawFd) {
unsafe {
libc::close(fd);
}
}