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 lock_file_path_for_dir(dir: &Path) -> PathBuf {
dir.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);
}
Ok(trimmed.parse::<u32>().ok()) }
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(())
}
fn try_create_lock_exclusive(path: &Path, pid: u32) -> std::io::Result<bool> {
use std::io::Write;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
match std::fs::OpenOptions::new()
.write(true)
.create_new(true) .open(path)
{
Ok(mut f) => {
writeln!(f, "{pid}")?;
f.sync_all()?;
Ok(true)
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(false),
Err(e) => Err(e),
}
}
pub fn acquire_lock(path: &Path) -> Result<DaemonLock> {
let me = std::process::id();
match try_create_lock_exclusive(path, me) {
Ok(true) => {
tracing::info!(
pid = me,
"wrote daemon lock at {} (exclusive create)",
path.display()
);
return Ok(DaemonLock::from_path(path.to_path_buf()));
}
Ok(false) => {} Err(e) => {
return Err(anyhow::anyhow!(
"create daemon lock {}: {e}",
path.display()
));
}
}
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");
let path = lock_file_path_for_dir(tmp.path());
assert_eq!(
path.file_name().and_then(|n| n.to_str()),
Some(LOCK_FILENAME)
);
assert!(
path.starts_with(tmp.path()),
"lock file must live under the data dir; got: {path:?}"
);
}
#[test]
fn read_lock_pid_returns_none_for_missing_file() {
let tmp = tempdir().expect("tempdir");
let result = read_lock_pid(&tmp.path().join("daemon.lock"))
.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");
assert_eq!(
read_lock_pid(&path).expect("must not error for valid file"),
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");
assert_eq!(
read_lock_pid(&path).expect("must not error for empty file"),
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");
}
#[cfg(unix)]
#[test]
fn pid_alive_returns_false_for_overflow_pid() {
assert!(!pid_alive(u32::MAX), "u32::MAX overflows i32");
assert!(!pid_alive(i32::MAX as u32 + 1), "first i32-overflow value");
}
#[test]
fn acquire_lock_writes_own_pid() {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join("daemon.lock");
assert!(!path.exists(), "pre-condition: lock file must not exist");
let _guard = acquire_lock(&path).expect("acquire 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());
}
#[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");
let dead_pid = child.id();
child.wait().expect("wait must succeed");
assert!(
!pid_alive(dead_pid),
"pid_alive({dead_pid}) must be false after child was reaped"
);
std::fs::write(&path, format!("{dead_pid}\n")).expect("write stale pid");
let _guard = acquire_lock(&path).expect("acquire 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());
}
#[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 must reclaim stale PID on non-Unix");
assert_eq!(
read_lock_pid(&path).expect("read").expect("pid"),
std::process::id()
);
}
#[test]
fn acquire_lock_refuses_live_pid() {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join("daemon.lock");
#[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(), "must refuse live lock holder PID 1");
assert!(
format!("{}", result.unwrap_err()).contains("already running"),
"error must mention 'already running'"
);
}
}
}
#[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 must succeed on empty path");
assert!(path.exists(), "lock file must exist after acquire");
drop(guard);
assert!(!path.exists(), "lock file must be removed on drop");
}
}