use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process;
use std::thread;
use std::time::Duration;
use tracing::{debug, warn};
use crate::storage::get_project_storage_path;
pub struct IndexLock {
lock_file: PathBuf,
acquired: bool,
}
impl IndexLock {
pub fn new(project_dir: &Path) -> io::Result<Self> {
let project_storage = get_project_storage_path(project_dir).map_err(io::Error::other)?;
let lock_file = project_storage.join("index.lock");
Ok(Self {
lock_file,
acquired: false,
})
}
pub fn acquire(&mut self) -> io::Result<()> {
if let Some(parent) = self.lock_file.parent() {
fs::create_dir_all(parent)?;
}
let my_pid = process::id();
loop {
match fs::read_to_string(&self.lock_file) {
Ok(contents) => {
if let Ok(pid) = contents.trim().parse::<u32>() {
if pid == my_pid {
self.acquired = true;
return Ok(());
}
if Self::is_process_alive(pid) {
debug!("Waiting for indexing lock held by PID {}", pid);
thread::sleep(Duration::from_millis(500));
continue;
} else {
debug!("Cleaning stale lock from dead PID {}", pid);
let _ = fs::remove_file(&self.lock_file);
}
} else {
warn!("Invalid lock file content, removing");
let _ = fs::remove_file(&self.lock_file);
}
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {
}
Err(e) => {
return Err(e);
}
}
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&self.lock_file)
{
Ok(mut file) => {
file.write_all(my_pid.to_string().as_bytes())?;
file.sync_all()?;
self.acquired = true;
debug!("Acquired indexing lock (PID {})", my_pid);
return Ok(());
}
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
thread::sleep(Duration::from_millis(100));
}
Err(e) => {
return Err(e);
}
}
}
}
pub fn release(&mut self) -> io::Result<()> {
if self.acquired {
match fs::remove_file(&self.lock_file) {
Ok(_) => {
self.acquired = false;
debug!("Released indexing lock");
Ok(())
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {
self.acquired = false;
Ok(())
}
Err(e) => Err(e),
}
} else {
Ok(())
}
}
#[cfg(unix)]
fn is_process_alive(pid: u32) -> bool {
use std::process::Command;
Command::new("kill")
.args(["-0", &pid.to_string()])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
#[cfg(windows)]
fn is_process_alive(pid: u32) -> bool {
use std::process::Command;
Command::new("tasklist")
.args(&["/FI", &format!("PID eq {}", pid)])
.output()
.map(|output| {
let output_str = String::from_utf8_lossy(&output.stdout);
output_str.contains(&pid.to_string())
})
.unwrap_or(false)
}
#[cfg(not(any(unix, windows)))]
fn is_process_alive(_pid: u32) -> bool {
true
}
}
impl Drop for IndexLock {
fn drop(&mut self) {
let _ = self.release();
}
}
pub fn with_index_lock<F, R>(project_dir: &Path, f: F) -> io::Result<R>
where
F: FnOnce() -> R,
{
let mut lock = IndexLock::new(project_dir)?;
lock.acquire()?;
let result = f();
lock.release()?;
Ok(result)
}