use anyhow::{bail, Result};
use std::path::{Path, PathBuf};
pub const LOCK_FILENAME: &str = "daemon.lock";
#[derive(Debug)]
pub struct DaemonLock {
path: PathBuf,
}
impl DaemonLock {
pub(crate) fn from_path(path: PathBuf) -> Self {
Self { path }
}
}
impl Drop for DaemonLock {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
pub fn lock_file_path() -> Option<PathBuf> {
trusty_common::resolve_data_dir("trusty-memory")
.ok()
.map(|d| d.join(LOCK_FILENAME))
}
pub fn pid_alive(pid: u32) -> bool {
if pid == 0 || pid > i32::MAX as u32 {
return false;
}
#[cfg(unix)]
{
let rc = unsafe { libc::kill(pid as libc::pid_t, 0) };
if rc == 0 {
return true; }
let err = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
err != libc::ESRCH
}
#[cfg(not(unix))]
{
false
}
}
pub fn read_lock_pid(path: &Path) -> Result<Option<u32>> {
let raw = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => {
return Err(anyhow::Error::new(e).context(format!("read lock file {}", path.display())));
}
};
let trimmed = raw.trim();
if trimmed.is_empty() {
return Ok(None);
}
match trimmed.parse::<u32>() {
Ok(pid) => Ok(Some(pid)),
Err(_) => Ok(None), }
}
fn write_lock_file(path: &Path, pid: u32) -> std::io::Result<()> {
use std::io::Write;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let tmp = path.with_extension("lock.tmp");
{
let mut f = std::fs::File::create(&tmp)?;
writeln!(f, "{pid}")?;
f.sync_all()?;
}
std::fs::rename(&tmp, path)?;
Ok(())
}
pub fn acquire_lock(path: &Path) -> Result<DaemonLock> {
let me = std::process::id();
if let Some(existing_pid) = read_lock_pid(path)? {
if existing_pid != me && pid_alive(existing_pid) {
bail!(
"trusty-memory daemon is already running as PID {existing_pid} \
(lock file: {}). \
If you believe this is a stale lock, remove it manually: \
rm {:?}",
path.display(),
path
);
}
tracing::info!(
stale_pid = existing_pid,
"reclaiming stale daemon lock file at {}",
path.display()
);
}
write_lock_file(path, me)
.map_err(|e| anyhow::anyhow!("write daemon lock {}: {e}", path.display()))?;
tracing::info!(pid = me, "wrote daemon lock at {}", path.display());
Ok(DaemonLock::from_path(path.to_path_buf()))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn lock_file_path_uses_data_dir() {
let tmp = tempdir().expect("tempdir");
unsafe {
std::env::set_var("TRUSTY_DATA_DIR_OVERRIDE", tmp.path());
}
let path = lock_file_path();
unsafe {
std::env::remove_var("TRUSTY_DATA_DIR_OVERRIDE");
}
let p = path.expect("lock_file_path must return Some under TRUSTY_DATA_DIR_OVERRIDE");
assert_eq!(p.file_name().and_then(|n| n.to_str()), Some(LOCK_FILENAME));
assert!(
p.starts_with(tmp.path()),
"lock file must live under the data dir override; got: {p:?}"
);
}
#[test]
fn read_lock_pid_returns_none_for_missing_file() {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join("daemon.lock");
let result = read_lock_pid(&path).expect("must not error for missing file");
assert_eq!(result, None);
}
#[test]
fn read_lock_pid_returns_pid_for_valid_file() {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join("daemon.lock");
std::fs::write(&path, "12345\n").expect("write");
let result = read_lock_pid(&path).expect("must not error for valid file");
assert_eq!(result, Some(12345));
}
#[test]
fn read_lock_pid_returns_none_for_empty_file() {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join("daemon.lock");
std::fs::write(&path, "").expect("write");
let result = read_lock_pid(&path).expect("must not error for empty file");
assert_eq!(result, None);
}
#[cfg(unix)]
#[test]
fn pid_alive_returns_true_for_current_pid() {
assert!(
pid_alive(std::process::id()),
"current process must be alive"
);
}
#[cfg(unix)]
#[test]
fn pid_alive_returns_false_for_pid_zero() {
assert!(
!pid_alive(0),
"pid 0 has process-group semantics, not single-process"
);
}
#[cfg(unix)]
#[test]
fn pid_alive_returns_false_for_overflow_pid() {
assert!(
!pid_alive(u32::MAX),
"u32::MAX overflows i32 → broadcast semantics"
);
assert!(
!pid_alive(i32::MAX as u32 + 1),
"first value that overflows i32"
);
}
#[test]
fn acquire_lock_writes_own_pid() {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join("daemon.lock");
let _guard = acquire_lock(&path).expect("acquire_lock must succeed on empty path");
let written = read_lock_pid(&path)
.expect("read after write must not error")
.expect("lock file must contain a PID after acquire");
assert_eq!(
written,
std::process::id(),
"lock file must contain the current process PID"
);
}
#[cfg(unix)]
#[test]
fn acquire_lock_reclaims_stale_pid() {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join("daemon.lock");
let mut child = std::process::Command::new("true")
.spawn()
.expect("spawn 'true' must succeed on any Unix CI machine");
let dead_pid = child.id();
child.wait().expect("wait must succeed");
assert!(
!pid_alive(dead_pid),
"pid_alive({dead_pid}) must be false after the child was reaped"
);
std::fs::write(&path, format!("{dead_pid}\n")).expect("write stale pid");
let _guard = acquire_lock(&path).expect("acquire_lock must reclaim stale PID");
let written = read_lock_pid(&path)
.expect("read after reclaim must not error")
.expect("lock file must contain a PID after reclaim");
assert_eq!(
written,
std::process::id(),
"lock file must be overwritten with current PID after stale reclaim"
);
}
#[cfg(not(unix))]
#[test]
fn acquire_lock_reclaims_stale_pid() {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join("daemon.lock");
std::fs::write(&path, "99999\n").expect("write stale pid");
let _guard = acquire_lock(&path).expect("acquire_lock must reclaim stale PID on non-Unix");
let written = read_lock_pid(&path)
.expect("read after reclaim must not error")
.expect("lock file must contain a PID after reclaim");
assert_eq!(written, std::process::id());
}
#[test]
fn acquire_lock_refuses_live_pid() {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join("daemon.lock");
std::fs::write(&path, format!("{}\n", std::process::id())).expect("write live pid");
#[cfg(unix)]
{
if pid_alive(1) {
std::fs::write(&path, "1\n").expect("write pid 1");
let result = acquire_lock(&path);
assert!(
result.is_err(),
"acquire_lock must refuse when lock holder PID 1 is alive"
);
let msg = format!("{}", result.unwrap_err());
assert!(
msg.contains("already running"),
"error must mention 'already running'; got: {msg}"
);
}
}
}
#[test]
fn daemon_lock_drops_removes_file() {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join("daemon.lock");
let guard = acquire_lock(&path).expect("acquire_lock must succeed on empty path");
assert!(path.exists(), "lock file must exist after acquire");
drop(guard);
assert!(
!path.exists(),
"lock file must be removed when DaemonLock is dropped"
);
}
}