use crate::error::TsunaguError;
use crate::socket::SocketPath;
use std::path::PathBuf;
pub struct DaemonProcess {
app_name: String,
pid_path: PathBuf,
socket_path: PathBuf,
}
impl DaemonProcess {
#[must_use]
pub fn new(app_name: &str) -> Self {
Self {
app_name: app_name.to_string(),
pid_path: SocketPath::pid_file(app_name),
socket_path: SocketPath::for_app(app_name),
}
}
#[must_use]
pub fn with_paths(app_name: &str, pid_path: PathBuf, socket_path: PathBuf) -> Self {
Self {
app_name: app_name.to_string(),
pid_path,
socket_path,
}
}
#[must_use]
pub fn is_running(&self) -> bool {
self.read_pid().is_some_and(process_alive)
}
#[must_use]
pub fn read_pid(&self) -> Option<u32> {
let contents = std::fs::read_to_string(&self.pid_path).ok()?;
contents.trim().parse::<u32>().ok()
}
pub fn write_pid(&self) -> Result<(), TsunaguError> {
if let Some(parent) = self.pid_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&self.pid_path, std::process::id().to_string())?;
Ok(())
}
pub fn acquire(&self) -> Result<(), TsunaguError> {
if let Some(pid) = self.read_pid() {
if process_alive(pid) {
return Err(TsunaguError::DaemonAlreadyRunning { pid });
}
tracing::warn!(pid, "removing stale PID file");
let _ = std::fs::remove_file(&self.pid_path);
}
self.write_pid()
}
pub fn cleanup(&self) {
let _ = std::fs::remove_file(&self.pid_path);
let _ = std::fs::remove_file(&self.socket_path);
}
#[must_use]
pub fn socket_path(&self) -> &PathBuf {
&self.socket_path
}
#[must_use]
pub fn pid_path(&self) -> &PathBuf {
&self.pid_path
}
#[must_use]
pub fn app_name(&self) -> &str {
&self.app_name
}
}
impl Drop for DaemonProcess {
fn drop(&mut self) {
self.cleanup();
}
}
fn process_alive(pid: u32) -> bool {
let proc_path = std::path::PathBuf::from(format!("/proc/{pid}"));
if proc_path.exists() {
return true;
}
std::process::Command::new("ps")
.args(["-p", &pid.to_string()])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn test_daemon(dir: &TempDir) -> DaemonProcess {
DaemonProcess::with_paths(
"test-app",
dir.path().join("test.pid"),
dir.path().join("test.sock"),
)
}
#[test]
fn new_daemon_not_running() {
let dir = TempDir::new().unwrap();
let d = test_daemon(&dir);
assert!(!d.is_running());
}
#[test]
fn write_pid_creates_file() {
let dir = TempDir::new().unwrap();
let d = test_daemon(&dir);
d.write_pid().unwrap();
assert!(d.pid_path().exists());
let contents = std::fs::read_to_string(d.pid_path()).unwrap();
assert_eq!(contents, std::process::id().to_string());
}
#[test]
fn read_pid_returns_written_pid() {
let dir = TempDir::new().unwrap();
let d = test_daemon(&dir);
d.write_pid().unwrap();
assert_eq!(d.read_pid(), Some(std::process::id()));
}
#[test]
fn read_pid_returns_none_when_missing() {
let dir = TempDir::new().unwrap();
let d = test_daemon(&dir);
assert_eq!(d.read_pid(), None);
}
#[test]
fn is_running_detects_current_process() {
let dir = TempDir::new().unwrap();
let d = test_daemon(&dir);
d.write_pid().unwrap();
assert!(d.is_running());
}
#[test]
fn is_running_false_for_stale_pid() {
let dir = TempDir::new().unwrap();
let d = test_daemon(&dir);
std::fs::write(d.pid_path(), "99999999").unwrap();
assert!(!d.is_running());
}
#[test]
fn cleanup_removes_files() {
let dir = TempDir::new().unwrap();
let d = test_daemon(&dir);
d.write_pid().unwrap();
std::fs::write(d.socket_path(), "").unwrap();
assert!(d.pid_path().exists());
assert!(d.socket_path().exists());
d.cleanup();
assert!(!d.pid_path().exists());
assert!(!d.socket_path().exists());
}
#[test]
fn acquire_succeeds_when_not_running() {
let dir = TempDir::new().unwrap();
let d = test_daemon(&dir);
d.acquire().unwrap();
assert!(d.pid_path().exists());
}
#[test]
fn acquire_removes_stale_pid() {
let dir = TempDir::new().unwrap();
let d = test_daemon(&dir);
std::fs::write(d.pid_path(), "99999999").unwrap();
d.acquire().unwrap();
assert_eq!(d.read_pid(), Some(std::process::id()));
}
#[test]
fn acquire_fails_when_already_running() {
let dir = TempDir::new().unwrap();
let pid_path = dir.path().join("test.pid");
std::fs::write(&pid_path, std::process::id().to_string()).unwrap();
let d = DaemonProcess::with_paths(
"test-app",
pid_path,
dir.path().join("test.sock"),
);
let err = d.acquire().unwrap_err();
assert!(err.to_string().contains("already running"));
}
#[test]
fn app_name_is_stored() {
let dir = TempDir::new().unwrap();
let d = test_daemon(&dir);
assert_eq!(d.app_name(), "test-app");
}
#[test]
fn drop_cleans_up() {
let dir = TempDir::new().unwrap();
let pid_path = dir.path().join("test.pid");
let sock_path = dir.path().join("test.sock");
{
let d = DaemonProcess::with_paths("test-app", pid_path.clone(), sock_path.clone());
d.write_pid().unwrap();
std::fs::write(&sock_path, "").unwrap();
assert!(pid_path.exists());
}
assert!(!pid_path.exists());
assert!(!sock_path.exists());
}
}