use anyhow::{Context, Result};
use std::fs;
use std::io::{Read, Write};
use std::path::PathBuf;
pub struct DaemonState {
pub pid_file: PathBuf,
pub socket_path: PathBuf,
pub log_file: PathBuf,
}
impl DaemonState {
pub fn new() -> Result<Self> {
let lore_dir = dirs::home_dir()
.context("Could not find home directory")?
.join(".lore");
fs::create_dir_all(&lore_dir).context("Failed to create ~/.lore directory")?;
Ok(Self {
pid_file: lore_dir.join("daemon.pid"),
socket_path: lore_dir.join("daemon.sock"),
log_file: lore_dir.join("daemon.log"),
})
}
pub fn is_running(&self) -> bool {
match self.get_pid() {
Some(pid) => Self::process_exists(pid),
None => false,
}
}
pub fn get_pid(&self) -> Option<u32> {
if !self.pid_file.exists() {
return None;
}
let mut file = fs::File::open(&self.pid_file).ok()?;
let mut contents = String::new();
file.read_to_string(&mut contents).ok()?;
contents.trim().parse().ok()
}
pub fn write_pid(&self, pid: u32) -> Result<()> {
let mut file = fs::File::create(&self.pid_file).context("Failed to create PID file")?;
write!(file, "{pid}").context("Failed to write PID")?;
Ok(())
}
pub fn remove_pid(&self) -> Result<()> {
if self.pid_file.exists() {
fs::remove_file(&self.pid_file).context("Failed to remove PID file")?;
}
Ok(())
}
pub fn remove_socket(&self) -> Result<()> {
if self.socket_path.exists() {
fs::remove_file(&self.socket_path).context("Failed to remove socket file")?;
}
Ok(())
}
pub fn cleanup(&self) -> Result<()> {
self.remove_pid()?;
self.remove_socket()?;
Ok(())
}
fn process_exists(pid: u32) -> bool {
#[cfg(unix)]
{
unsafe { libc::kill(pid as libc::pid_t, 0) == 0 }
}
#[cfg(not(unix))]
{
let _ = pid;
true
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DaemonStats {
pub files_watched: usize,
pub sessions_imported: u64,
pub messages_imported: u64,
pub started_at: chrono::DateTime<chrono::Utc>,
pub errors: u64,
}
impl Default for DaemonStats {
fn default() -> Self {
Self {
files_watched: 0,
sessions_imported: 0,
messages_imported: 0,
started_at: chrono::Utc::now(),
errors: 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn create_test_state() -> (DaemonState, tempfile::TempDir) {
let dir = tempdir().expect("Failed to create temp directory");
let state = DaemonState {
pid_file: dir.path().join("daemon.pid"),
socket_path: dir.path().join("daemon.sock"),
log_file: dir.path().join("daemon.log"),
};
(state, dir)
}
#[test]
fn test_is_running_no_pid_file() {
let (state, _dir) = create_test_state();
assert!(
!state.is_running(),
"Should not be running without PID file"
);
}
#[test]
fn test_get_pid_no_file() {
let (state, _dir) = create_test_state();
assert!(
state.get_pid().is_none(),
"Should return None without PID file"
);
}
#[test]
fn test_write_and_get_pid() {
let (state, _dir) = create_test_state();
state.write_pid(12345).expect("Failed to write PID");
let pid = state.get_pid();
assert_eq!(pid, Some(12345), "PID should match written value");
}
#[test]
fn test_remove_pid() {
let (state, _dir) = create_test_state();
state.write_pid(12345).expect("Failed to write PID");
assert!(state.pid_file.exists(), "PID file should exist after write");
state.remove_pid().expect("Failed to remove PID");
assert!(
!state.pid_file.exists(),
"PID file should not exist after remove"
);
}
#[test]
fn test_remove_pid_nonexistent() {
let (state, _dir) = create_test_state();
state
.remove_pid()
.expect("Should not error on nonexistent file");
}
#[test]
fn test_remove_socket() {
let (state, _dir) = create_test_state();
fs::write(&state.socket_path, "").expect("Failed to create file");
assert!(state.socket_path.exists(), "Socket file should exist");
state.remove_socket().expect("Failed to remove socket");
assert!(
!state.socket_path.exists(),
"Socket file should not exist after remove"
);
}
#[test]
fn test_cleanup() {
let (state, _dir) = create_test_state();
state.write_pid(12345).expect("Failed to write PID");
fs::write(&state.socket_path, "").expect("Failed to create socket");
state.cleanup().expect("Failed to cleanup");
assert!(!state.pid_file.exists(), "PID file should be cleaned up");
assert!(
!state.socket_path.exists(),
"Socket file should be cleaned up"
);
}
#[test]
fn test_daemon_stats_default() {
let stats = DaemonStats::default();
assert_eq!(stats.files_watched, 0);
assert_eq!(stats.sessions_imported, 0);
assert_eq!(stats.messages_imported, 0);
assert_eq!(stats.errors, 0);
}
#[test]
fn test_is_running_with_invalid_pid() {
let (state, _dir) = create_test_state();
state.write_pid(999999999).expect("Failed to write PID");
let running = state.is_running();
let _ = running;
}
#[test]
fn test_get_pid_invalid_content() {
let (state, _dir) = create_test_state();
fs::write(&state.pid_file, "not_a_number").expect("Failed to write");
assert!(
state.get_pid().is_none(),
"Should return None for invalid PID"
);
}
}