Skip to main content

krait/daemon/
lifecycle.rs

1use std::path::{Path, PathBuf};
2
3use thiserror::Error;
4
5#[derive(Debug, Error)]
6pub enum LifecycleError {
7    #[error("Daemon already running (pid {pid})")]
8    AlreadyRunning { pid: u32 },
9    #[error("IO error: {0}")]
10    Io(#[from] std::io::Error),
11}
12
13/// PID file path derived from socket path: `/tmp/krait-<hash>.pid`
14#[must_use]
15pub fn pid_path(socket_path: &Path) -> PathBuf {
16    socket_path.with_extension("pid")
17}
18
19/// Write current process PID to file. Checks for an existing live process first.
20///
21/// # Errors
22/// Returns `LifecycleError::AlreadyRunning` if a daemon is already running,
23/// or `LifecycleError::Io` on filesystem errors.
24pub fn acquire_pid_file(pid_path: &Path) -> Result<(), LifecycleError> {
25    if let Ok(contents) = std::fs::read_to_string(pid_path) {
26        if let Ok(pid) = contents.trim().parse::<u32>() {
27            if is_process_alive(pid) {
28                return Err(LifecycleError::AlreadyRunning { pid });
29            }
30        }
31        // Stale PID file — remove it
32        let _ = std::fs::remove_file(pid_path);
33    }
34
35    let pid = std::process::id();
36    std::fs::write(pid_path, pid.to_string())?;
37    Ok(())
38}
39
40/// Remove PID file and socket file.
41pub fn cleanup(socket_path: &Path, pid_path: &Path) {
42    let _ = std::fs::remove_file(socket_path);
43    let _ = std::fs::remove_file(pid_path);
44}
45
46fn is_process_alive(pid: u32) -> bool {
47    // SAFETY: kill(pid, 0) checks if process exists without sending a signal
48    unsafe { libc::kill(pid.cast_signed(), 0) == 0 }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn pid_path_from_socket_path() {
57        let sock = PathBuf::from("/tmp/krait-abc123.sock");
58        assert_eq!(pid_path(&sock), PathBuf::from("/tmp/krait-abc123.pid"));
59    }
60
61    #[test]
62    fn acquire_pid_file_creates_file() {
63        let dir = tempfile::tempdir().unwrap();
64        let path = dir.path().join("test.pid");
65
66        acquire_pid_file(&path).unwrap();
67
68        let contents = std::fs::read_to_string(&path).unwrap();
69        assert_eq!(contents, std::process::id().to_string());
70    }
71
72    #[test]
73    fn acquire_pid_file_rejects_live_process() {
74        let dir = tempfile::tempdir().unwrap();
75        let path = dir.path().join("test.pid");
76
77        // Write our own PID (which is alive)
78        std::fs::write(&path, std::process::id().to_string()).unwrap();
79
80        let result = acquire_pid_file(&path);
81        assert!(result.is_err());
82        assert!(result.unwrap_err().to_string().contains("already running"));
83    }
84
85    #[test]
86    fn acquire_pid_file_cleans_stale() {
87        let dir = tempfile::tempdir().unwrap();
88        let path = dir.path().join("test.pid");
89
90        // Write a PID that definitely doesn't exist
91        std::fs::write(&path, "999999999").unwrap();
92
93        acquire_pid_file(&path).unwrap();
94
95        let contents = std::fs::read_to_string(&path).unwrap();
96        assert_eq!(contents, std::process::id().to_string());
97    }
98
99    #[test]
100    fn cleanup_removes_files() {
101        let dir = tempfile::tempdir().unwrap();
102        let sock = dir.path().join("test.sock");
103        let pid = dir.path().join("test.pid");
104
105        std::fs::write(&sock, "").unwrap();
106        std::fs::write(&pid, "").unwrap();
107
108        cleanup(&sock, &pid);
109
110        assert!(!sock.exists());
111        assert!(!pid.exists());
112    }
113}