use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use nix::sys::signal::{self, Signal};
use nix::unistd::Pid;
pub const LOCK_FILENAME: &str = "daemon.lock";
#[derive(Debug, thiserror::Error)]
#[error(
"another errand daemon is already running as process {pid}. Two daemons on one bot token \
both act on every message. Stop that one first, or remove {path} if you are certain it is \
gone."
)]
pub struct AlreadyRunningError {
pub pid: i32,
pub path: String,
}
pub fn is_running(pid: i32) -> bool {
if pid <= 0 {
return false;
}
match signal::kill(Pid::from_raw(pid), Signal::SIGURG) {
Err(nix::errno::Errno::EPERM) | Ok(()) => true,
Err(_) => false,
}
}
#[derive(Debug)]
pub struct DaemonLock {
path: PathBuf,
released: bool,
}
impl DaemonLock {
#[allow(
dead_code,
reason = "read by this module's tests, which assert on state the daemon never asks for"
)]
pub fn path(&self) -> &Path {
&self.path
}
pub fn release(&mut self) {
if self.released {
return;
}
self.released = true;
let _ = std::fs::remove_file(&self.path);
}
}
pub fn acquire_lock(state_dir: &str, pid: i32) -> Result<DaemonLock, AlreadyRunningError> {
let path = Path::new(state_dir).join(LOCK_FILENAME);
for _ in 0..2 {
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(&path)
{
Ok(mut file) => {
use std::io::Write;
let _ = file.write_all(format!("{pid}\n").as_bytes());
break;
}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
let held = std::fs::read_to_string(&path)
.ok()
.and_then(|text| text.trim().parse::<i32>().ok())
.unwrap_or(0);
if is_running(held) {
return Err(AlreadyRunningError {
pid: held,
path: path.display().to_string(),
});
}
let _ = std::fs::remove_file(&path);
}
Err(error) => {
return Err(AlreadyRunningError {
pid: 0,
path: format!("{}: {error}", path.display()),
});
}
}
}
Ok(DaemonLock {
path,
released: false,
})
}
#[cfg(test)]
mod tests;