use std::path::PathBuf;
use std::time::Duration;
use tokio::fs;
use tokio::io::AsyncWriteExt;
pub struct RuntimeLock {
path: PathBuf,
}
impl Drop for RuntimeLock {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
fn is_process_alive(pid: u32) -> bool {
unsafe { libc::kill(pid as i32, 0) == 0 }
}
fn lock_path(name: &str) -> Result<PathBuf, color_eyre::Report> {
let home = std::env::var("HOME")?;
Ok(PathBuf::from(home)
.join(".cache/muthr")
.join(format!("{}.lock", name)))
}
pub async fn acquire(name: &str, timeout: Duration) -> Result<RuntimeLock, color_eyre::Report> {
let path = lock_path(name)?;
let parent = path
.parent()
.ok_or_else(|| color_eyre::eyre::eyre!("invalid lock path"))?;
fs::create_dir_all(parent).await?;
let pid = std::process::id();
let started = std::time::Instant::now();
let mut first_wait_log = true;
loop {
let open_result = fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&path)
.await;
match open_result {
Ok(mut file) => {
file.write_all(pid.to_string().as_bytes()).await?;
file.flush().await?;
return Ok(RuntimeLock { path });
}
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
let stale = match fs::read_to_string(&path).await {
Ok(s) => match s.trim().parse::<u32>() {
Ok(holder_pid) => !is_process_alive(holder_pid),
Err(_) => true,
},
Err(_) => true,
};
if stale {
fs::remove_file(&path).await.ok();
continue;
}
if started.elapsed() >= timeout {
return Err(color_eyre::eyre::eyre!(
"timed out waiting for lifecycle lock '{}'",
name
));
}
if first_wait_log {
eprintln!("info: waiting for lifecycle lock '{}'", name);
first_wait_log = false;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
Err(err) => return Err(err.into()),
}
}
}