1use anyhow::{Context, Result};
8use std::fs;
9use std::io::{Read, Write};
10use std::path::PathBuf;
11
12pub struct DaemonState {
17 pub pid_file: PathBuf,
19 pub socket_path: PathBuf,
21 pub log_file: PathBuf,
23}
24
25impl DaemonState {
26 pub fn new() -> Result<Self> {
35 let lore_dir = dirs::home_dir()
36 .context("Could not find home directory")?
37 .join(".lore");
38
39 fs::create_dir_all(&lore_dir).context("Failed to create ~/.lore directory")?;
40
41 Ok(Self {
42 pid_file: lore_dir.join("daemon.pid"),
43 socket_path: lore_dir.join("daemon.sock"),
44 log_file: lore_dir.join("daemon.log"),
45 })
46 }
47
48 pub fn is_running(&self) -> bool {
54 match self.get_pid() {
55 Some(pid) => Self::process_exists(pid),
56 None => false,
57 }
58 }
59
60 pub fn get_pid(&self) -> Option<u32> {
64 if !self.pid_file.exists() {
65 return None;
66 }
67
68 let mut file = fs::File::open(&self.pid_file).ok()?;
69 let mut contents = String::new();
70 file.read_to_string(&mut contents).ok()?;
71
72 contents.trim().parse().ok()
73 }
74
75 pub fn write_pid(&self, pid: u32) -> Result<()> {
81 let mut file = fs::File::create(&self.pid_file).context("Failed to create PID file")?;
82 write!(file, "{pid}").context("Failed to write PID")?;
83 Ok(())
84 }
85
86 pub fn remove_pid(&self) -> Result<()> {
94 if self.pid_file.exists() {
95 fs::remove_file(&self.pid_file).context("Failed to remove PID file")?;
96 }
97 Ok(())
98 }
99
100 pub fn remove_socket(&self) -> Result<()> {
108 if self.socket_path.exists() {
109 fs::remove_file(&self.socket_path).context("Failed to remove socket file")?;
110 }
111 Ok(())
112 }
113
114 pub fn cleanup(&self) -> Result<()> {
118 self.remove_pid()?;
119 self.remove_socket()?;
120 Ok(())
121 }
122
123 fn process_exists(pid: u32) -> bool {
128 #[cfg(unix)]
130 {
131 unsafe { libc::kill(pid as libc::pid_t, 0) == 0 }
134 }
135
136 #[cfg(not(unix))]
137 {
138 let _ = pid;
141 true
142 }
143 }
144}
145
146#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
148pub struct DaemonStats {
149 pub files_watched: usize,
151 pub sessions_imported: u64,
153 pub messages_imported: u64,
155 pub started_at: chrono::DateTime<chrono::Utc>,
157 pub errors: u64,
159}
160
161impl Default for DaemonStats {
162 fn default() -> Self {
163 Self {
164 files_watched: 0,
165 sessions_imported: 0,
166 messages_imported: 0,
167 started_at: chrono::Utc::now(),
168 errors: 0,
169 }
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use tempfile::tempdir;
177
178 fn create_test_state() -> (DaemonState, tempfile::TempDir) {
180 let dir = tempdir().expect("Failed to create temp directory");
181 let state = DaemonState {
182 pid_file: dir.path().join("daemon.pid"),
183 socket_path: dir.path().join("daemon.sock"),
184 log_file: dir.path().join("daemon.log"),
185 };
186 (state, dir)
187 }
188
189 #[test]
190 fn test_is_running_no_pid_file() {
191 let (state, _dir) = create_test_state();
192 assert!(
193 !state.is_running(),
194 "Should not be running without PID file"
195 );
196 }
197
198 #[test]
199 fn test_get_pid_no_file() {
200 let (state, _dir) = create_test_state();
201 assert!(
202 state.get_pid().is_none(),
203 "Should return None without PID file"
204 );
205 }
206
207 #[test]
208 fn test_write_and_get_pid() {
209 let (state, _dir) = create_test_state();
210
211 state.write_pid(12345).expect("Failed to write PID");
212
213 let pid = state.get_pid();
214 assert_eq!(pid, Some(12345), "PID should match written value");
215 }
216
217 #[test]
218 fn test_remove_pid() {
219 let (state, _dir) = create_test_state();
220
221 state.write_pid(12345).expect("Failed to write PID");
222 assert!(state.pid_file.exists(), "PID file should exist after write");
223
224 state.remove_pid().expect("Failed to remove PID");
225 assert!(
226 !state.pid_file.exists(),
227 "PID file should not exist after remove"
228 );
229 }
230
231 #[test]
232 fn test_remove_pid_nonexistent() {
233 let (state, _dir) = create_test_state();
234
235 state
237 .remove_pid()
238 .expect("Should not error on nonexistent file");
239 }
240
241 #[test]
242 fn test_remove_socket() {
243 let (state, _dir) = create_test_state();
244
245 fs::write(&state.socket_path, "").expect("Failed to create file");
247 assert!(state.socket_path.exists(), "Socket file should exist");
248
249 state.remove_socket().expect("Failed to remove socket");
250 assert!(
251 !state.socket_path.exists(),
252 "Socket file should not exist after remove"
253 );
254 }
255
256 #[test]
257 fn test_cleanup() {
258 let (state, _dir) = create_test_state();
259
260 state.write_pid(12345).expect("Failed to write PID");
261 fs::write(&state.socket_path, "").expect("Failed to create socket");
262
263 state.cleanup().expect("Failed to cleanup");
264
265 assert!(!state.pid_file.exists(), "PID file should be cleaned up");
266 assert!(
267 !state.socket_path.exists(),
268 "Socket file should be cleaned up"
269 );
270 }
271
272 #[test]
273 fn test_daemon_stats_default() {
274 let stats = DaemonStats::default();
275
276 assert_eq!(stats.files_watched, 0);
277 assert_eq!(stats.sessions_imported, 0);
278 assert_eq!(stats.messages_imported, 0);
279 assert_eq!(stats.errors, 0);
280 }
281
282 #[test]
283 fn test_is_running_with_invalid_pid() {
284 let (state, _dir) = create_test_state();
285
286 state.write_pid(999999999).expect("Failed to write PID");
288
289 let running = state.is_running();
292 let _ = running;
295 }
296
297 #[test]
298 fn test_get_pid_invalid_content() {
299 let (state, _dir) = create_test_state();
300
301 fs::write(&state.pid_file, "not_a_number").expect("Failed to write");
303
304 assert!(
305 state.get_pid().is_none(),
306 "Should return None for invalid PID"
307 );
308 }
309}