use std::fs::{File, OpenOptions};
use std::io;
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
use std::time::{Duration, Instant};
extern crate libc;
const LOCK_FILE_PATH: &str = "/tmp/tacet-kperf.lock";
const DEFAULT_LOCK_TIMEOUT: Duration = Duration::from_millis(200);
#[derive(Debug)]
pub enum LockResult {
Acquired(LockGuard),
Timeout,
IoError(io::Error),
}
pub struct LockGuard {
file: File,
}
impl Drop for LockGuard {
fn drop(&mut self) {
unsafe {
libc::flock(self.file.as_raw_fd(), libc::LOCK_UN);
}
}
}
impl std::fmt::Debug for LockGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LockGuard").field("locked", &true).finish()
}
}
pub fn try_acquire(timeout: Duration) -> LockResult {
let file = match OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.mode(0o666)
.open(LOCK_FILE_PATH)
{
Ok(f) => f,
Err(e) => return LockResult::IoError(e),
};
let fd = file.as_raw_fd();
let start = Instant::now();
loop {
let result = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
if result == 0 {
return LockResult::Acquired(LockGuard { file });
}
let errno = io::Error::last_os_error();
if errno.kind() != io::ErrorKind::WouldBlock {
return LockResult::IoError(errno);
}
if start.elapsed() >= timeout {
return LockResult::Timeout;
}
std::thread::sleep(Duration::from_millis(10));
}
}
pub fn try_acquire_default() -> LockResult {
try_acquire(DEFAULT_LOCK_TIMEOUT)
}
#[cfg(test)]
mod tests {
use super::*;
fn can_acquire_lock() -> bool {
match try_acquire_default() {
LockResult::Acquired(_) => true,
LockResult::IoError(e) if e.kind() == io::ErrorKind::PermissionDenied => {
eprintln!(
"Skipping kperf lock test: permission denied (lock file may be owned by root)"
);
false
}
LockResult::Timeout => {
eprintln!("Skipping kperf lock test: lock held by another process");
false
}
LockResult::IoError(e) => {
eprintln!("Skipping kperf lock test: I/O error: {}", e);
false
}
}
}
#[test]
fn test_lock_acquire_release() {
if !can_acquire_lock() {
return;
}
let result = try_acquire_default();
assert!(
matches!(result, LockResult::Acquired(_)),
"Should acquire lock: {:?}",
result
);
}
#[test]
fn test_lock_contention_same_process() {
let guard1 = match try_acquire_default() {
LockResult::Acquired(g) => g,
LockResult::IoError(e) if e.kind() == io::ErrorKind::PermissionDenied => {
tracing::debug!("Skipping: permission denied");
return;
}
LockResult::Timeout => {
tracing::debug!("Skipping: lock held by another process");
return;
}
other => panic!("Unexpected result: {:?}", other),
};
let result = try_acquire(Duration::from_millis(50));
assert!(
matches!(result, LockResult::Timeout),
"Expected timeout, got: {:?}",
result
);
drop(guard1);
let guard3 = try_acquire_default();
assert!(matches!(guard3, LockResult::Acquired(_)));
}
}