use std::fs::File;
use std::os::unix::io::AsRawFd;
use crate::core::{LuciError, Result};
const PENDING_BYTE: u64 = 49;
const RESERVED_BYTE: u64 = 50;
const SHARED_FIRST: u64 = 51;
const SHARED_SIZE: u64 = 510;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LockLevel {
Unlocked = 0,
Shared = 1,
Reserved = 2,
Pending = 3,
Exclusive = 4,
}
pub struct FileLock {
fd: i32,
level: LockLevel,
}
impl FileLock {
pub fn new(file: &File) -> Self {
Self {
fd: file.as_raw_fd(),
level: LockLevel::Unlocked,
}
}
pub fn level(&self) -> LockLevel {
self.level
}
pub fn lock_shared(&mut self) -> Result<()> {
assert_eq!(
self.level,
LockLevel::Unlocked,
"lock_shared requires UNLOCKED state"
);
fcntl_lock(self.fd, libc::F_RDLCK, PENDING_BYTE, 1)?;
let result = fcntl_lock(self.fd, libc::F_RDLCK, SHARED_FIRST, SHARED_SIZE);
let _ = fcntl_lock(self.fd, libc::F_UNLCK, PENDING_BYTE, 1);
result?;
self.level = LockLevel::Shared;
Ok(())
}
pub fn lock_reserved(&mut self, timeout: std::time::Duration) -> Result<()> {
assert_eq!(
self.level,
LockLevel::Shared,
"lock_reserved requires SHARED state"
);
let deadline = std::time::Instant::now() + timeout;
let mut backoff = std::time::Duration::from_millis(1);
let max_backoff = std::time::Duration::from_millis(100);
loop {
match fcntl_try_lock(self.fd, libc::F_WRLCK, RESERVED_BYTE, 1) {
Ok(()) => break,
Err(LuciError::WriterLocked) => {
if std::time::Instant::now() >= deadline {
return Err(LuciError::WriterLocked);
}
std::thread::sleep(backoff);
backoff = (backoff * 2).min(max_backoff);
}
Err(e) => return Err(e),
}
}
self.level = LockLevel::Reserved;
Ok(())
}
pub fn lock_exclusive(&mut self) -> Result<()> {
assert!(
self.level == LockLevel::Reserved || self.level == LockLevel::Pending,
"lock_exclusive requires RESERVED or PENDING state"
);
if self.level == LockLevel::Reserved {
fcntl_lock(self.fd, libc::F_WRLCK, PENDING_BYTE, 1)?;
self.level = LockLevel::Pending;
}
fcntl_lock(self.fd, libc::F_WRLCK, SHARED_FIRST, SHARED_SIZE)?;
self.level = LockLevel::Exclusive;
Ok(())
}
pub fn downgrade_to_shared(&mut self) -> Result<()> {
assert!(
self.level >= LockLevel::Reserved,
"downgrade_to_shared requires RESERVED or higher"
);
fcntl_lock(self.fd, libc::F_RDLCK, SHARED_FIRST, SHARED_SIZE)?;
fcntl_lock(self.fd, libc::F_UNLCK, PENDING_BYTE, 2)?;
self.level = LockLevel::Shared;
Ok(())
}
pub fn unlock(&mut self) -> Result<()> {
if self.level == LockLevel::Unlocked {
return Ok(());
}
fcntl_lock(self.fd, libc::F_UNLCK, 0, 0)?;
self.level = LockLevel::Unlocked;
Ok(())
}
}
impl Drop for FileLock {
fn drop(&mut self) {
let _ = self.unlock();
}
}
fn fcntl_lock(fd: i32, lock_type: i16, start: u64, len: u64) -> Result<()> {
let fl = libc::flock {
l_type: lock_type,
l_whence: libc::SEEK_SET as i16,
l_start: start as i64,
l_len: len as i64,
l_pid: 0,
};
let ret = unsafe { libc::fcntl(fd, libc::F_SETLKW, &fl) };
if ret == -1 {
let err = std::io::Error::last_os_error();
return Err(LuciError::Io(err));
}
Ok(())
}
fn fcntl_try_lock(fd: i32, lock_type: i16, start: u64, len: u64) -> Result<()> {
let fl = libc::flock {
l_type: lock_type,
l_whence: libc::SEEK_SET as i16,
l_start: start as i64,
l_len: len as i64,
l_pid: 0,
};
let ret = unsafe { libc::fcntl(fd, libc::F_SETLK, &fl) };
if ret == -1 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EAGAIN) || err.raw_os_error() == Some(libc::EACCES) {
return Err(LuciError::WriterLocked);
}
return Err(LuciError::Io(err));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::OpenOptions;
fn test_file(name: &str) -> (std::path::PathBuf, File) {
let path =
std::env::temp_dir().join(format!("luci_lock_test_{}_{name}", std::process::id()));
let _ = std::fs::remove_file(&path);
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&path)
.unwrap();
file.set_len(4096).unwrap();
(path, file)
}
#[test]
fn shared_lock_roundtrip() {
let (path, file) = test_file("shared");
let mut lock = FileLock::new(&file);
assert_eq!(lock.level(), LockLevel::Unlocked);
lock.lock_shared().unwrap();
assert_eq!(lock.level(), LockLevel::Shared);
lock.unlock().unwrap();
assert_eq!(lock.level(), LockLevel::Unlocked);
std::fs::remove_file(path).ok();
}
#[test]
fn full_escalation_and_downgrade() {
let (path, file) = test_file("escalation");
let mut lock = FileLock::new(&file);
lock.lock_shared().unwrap();
lock.lock_reserved(std::time::Duration::from_secs(5))
.unwrap();
assert_eq!(lock.level(), LockLevel::Reserved);
lock.lock_exclusive().unwrap();
assert_eq!(lock.level(), LockLevel::Exclusive);
lock.downgrade_to_shared().unwrap();
assert_eq!(lock.level(), LockLevel::Shared);
lock.unlock().unwrap();
std::fs::remove_file(path).ok();
}
#[test]
fn same_process_fds_share_locks() {
let (path, file1) = test_file("same_process");
let file2 = OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.unwrap();
let mut lock1 = FileLock::new(&file1);
let mut lock2 = FileLock::new(&file2);
lock1.lock_shared().unwrap();
lock1
.lock_reserved(std::time::Duration::from_secs(5))
.unwrap();
lock2.lock_shared().unwrap();
lock2
.lock_reserved(std::time::Duration::from_secs(5))
.unwrap();
lock1.downgrade_to_shared().unwrap();
lock1.unlock().unwrap();
lock2.downgrade_to_shared().unwrap();
lock2.unlock().unwrap();
std::fs::remove_file(path).ok();
}
#[test]
fn drop_releases_locks() {
let (path, file1) = test_file("drop_release");
let file2 = OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.unwrap();
{
let mut lock1 = FileLock::new(&file1);
lock1.lock_shared().unwrap();
lock1
.lock_reserved(std::time::Duration::from_secs(5))
.unwrap();
}
let mut lock2 = FileLock::new(&file2);
lock2.lock_shared().unwrap();
lock2
.lock_reserved(std::time::Duration::from_secs(5))
.unwrap();
lock2.downgrade_to_shared().unwrap();
lock2.unlock().unwrap();
std::fs::remove_file(path).ok();
}
}