use std::path::{Path, PathBuf};
use std::time::Instant;
use crate::error::Error;
const LOCK_FILE_NAME: &str = ".session.lock";
const POST_WAIT_REPORT_MS: u128 = 50;
pub(crate) struct SessionLock {
_file: std::fs::File,
}
impl SessionLock {
pub(crate) fn acquire(workspace_target: &Path) -> Result<SessionLock, Error> {
let lock_dir = workspace_target.join("lihaaf");
std::fs::create_dir_all(&lock_dir).map_err(|e| {
Error::io(
e,
"creating session lock parent dir",
Some(lock_dir.clone()),
)
})?;
let lock_path: PathBuf = lock_dir.join(LOCK_FILE_NAME);
let file = open_lock_file(&lock_path)?;
match try_lock_exclusive(&file) {
Ok(true) => return Ok(SessionLock { _file: file }),
Ok(false) => {
eprintln!(
"lihaaf: waiting for another lihaaf session to release {} ...",
lock_path.display(),
);
}
Err(e) => {
return Err(Error::io(
e,
"non-blocking session lock acquire",
Some(lock_path.clone()),
));
}
}
let start = Instant::now();
lock_exclusive_blocking(&file)
.map_err(|e| Error::io(e, "blocking session lock acquire", Some(lock_path.clone())))?;
let elapsed = start.elapsed();
if elapsed.as_millis() >= POST_WAIT_REPORT_MS {
eprintln!(
"lihaaf: acquired session lock after {} ms",
elapsed.as_millis(),
);
}
Ok(SessionLock { _file: file })
}
}
fn open_lock_file(lock_path: &Path) -> Result<std::fs::File, Error> {
let mut opts = std::fs::OpenOptions::new();
opts.create(true).write(true).truncate(false);
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
opts.share_mode(3);
}
opts.open(lock_path).map_err(|e| {
Error::io(
e,
"opening session lock file",
Some(lock_path.to_path_buf()),
)
})
}
#[cfg(unix)]
fn try_lock_exclusive(file: &std::fs::File) -> std::io::Result<bool> {
use std::os::unix::io::AsRawFd;
let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if rc == 0 {
return Ok(true);
}
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EWOULDBLOCK) {
return Ok(false);
}
Err(err)
}
#[cfg(unix)]
fn lock_exclusive_blocking(file: &std::fs::File) -> std::io::Result<()> {
use std::os::unix::io::AsRawFd;
loop {
let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) };
if rc == 0 {
return Ok(());
}
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return Err(err);
}
}
#[cfg(windows)]
fn try_lock_exclusive(file: &std::fs::File) -> std::io::Result<bool> {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Foundation::ERROR_LOCK_VIOLATION;
use windows_sys::Win32::Storage::FileSystem::{
LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY, LockFileEx,
};
use windows_sys::Win32::System::IO::OVERLAPPED;
let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
let ok = unsafe {
LockFileEx(
file.as_raw_handle() as _,
LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
0,
u32::MAX,
u32::MAX,
&mut overlapped,
)
};
if ok != 0 {
return Ok(true);
}
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(ERROR_LOCK_VIOLATION as i32) {
return Ok(false);
}
Err(err)
}
#[cfg(windows)]
fn lock_exclusive_blocking(file: &std::fs::File) -> std::io::Result<()> {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Storage::FileSystem::{LOCKFILE_EXCLUSIVE_LOCK, LockFileEx};
use windows_sys::Win32::System::IO::OVERLAPPED;
let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
let ok = unsafe {
LockFileEx(
file.as_raw_handle() as _,
LOCKFILE_EXCLUSIVE_LOCK,
0,
u32::MAX,
u32::MAX,
&mut overlapped,
)
};
if ok != 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
#[cfg(windows)]
impl Drop for SessionLock {
fn drop(&mut self) {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Storage::FileSystem::UnlockFileEx;
use windows_sys::Win32::System::IO::OVERLAPPED;
let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
let _ = unsafe {
UnlockFileEx(
self._file.as_raw_handle() as _,
0,
u32::MAX,
u32::MAX,
&mut overlapped,
)
};
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn acquire_creates_lockfile_under_target_lihaaf() {
let tmp = tempfile::tempdir().unwrap();
let workspace_target = tmp.path().join("target");
let _guard = SessionLock::acquire(&workspace_target).unwrap();
let lock_path = workspace_target.join("lihaaf").join(LOCK_FILE_NAME);
assert!(
lock_path.is_file(),
"expected lockfile at {}",
lock_path.display(),
);
}
#[test]
fn drop_releases_lock_for_same_process_reacquire() {
let tmp = tempfile::tempdir().unwrap();
let workspace_target = tmp.path().join("target");
{
let _guard = SessionLock::acquire(&workspace_target).unwrap();
}
let start = std::time::Instant::now();
let _guard2 = SessionLock::acquire(&workspace_target).unwrap();
let elapsed = start.elapsed();
assert!(
elapsed.as_millis() < POST_WAIT_REPORT_MS,
"re-acquire after drop should be fast (was {} ms)",
elapsed.as_millis(),
);
}
#[test]
fn lockfile_path_is_deterministic() {
let tmp = tempfile::tempdir().unwrap();
let workspace_target = tmp.path().join("target");
let expected = workspace_target.join("lihaaf").join(LOCK_FILE_NAME);
{
let _g1 = SessionLock::acquire(&workspace_target).unwrap();
assert!(expected.is_file());
}
{
let _g2 = SessionLock::acquire(&workspace_target).unwrap();
assert!(expected.is_file());
}
}
}