database_replicator/
daemon.rs1use anyhow::{Context, Result};
5use std::fs;
6use std::path::PathBuf;
7
8pub fn get_daemon_dir() -> Result<PathBuf> {
11 #[cfg(windows)]
12 let daemon_dir = {
13 let app_data = dirs::data_local_dir().context("Failed to determine AppData directory")?;
14 app_data.join("seren-replicator")
15 };
16
17 #[cfg(not(windows))]
18 let daemon_dir = {
19 let home = dirs::home_dir().context("Failed to determine home directory")?;
20 home.join(".seren-replicator")
21 };
22
23 if !daemon_dir.exists() {
25 fs::create_dir_all(&daemon_dir)
26 .with_context(|| format!("Failed to create daemon directory: {:?}", daemon_dir))?;
27 }
28
29 Ok(daemon_dir)
30}
31
32pub fn get_pid_file_path() -> Result<PathBuf> {
34 Ok(get_daemon_dir()?.join("sync.pid"))
35}
36
37pub fn get_log_file_path() -> Result<PathBuf> {
39 Ok(get_daemon_dir()?.join("sync.log"))
40}
41
42#[cfg(unix)]
44fn is_process_running(pid: i32) -> bool {
45 unsafe { libc::kill(pid, 0) == 0 }
47}
48
49#[cfg(windows)]
50fn is_process_running(pid: i32) -> bool {
51 use std::ptr::null_mut;
52
53 const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
55 const SYNCHRONIZE: u32 = 0x00100000;
56
57 unsafe {
58 let handle = OpenProcess(
59 PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE,
60 0,
61 pid as u32,
62 );
63 if handle.is_null() {
64 return false;
65 }
66
67 let mut exit_code: u32 = 0;
69 let result = GetExitCodeProcess(handle, &mut exit_code);
70 CloseHandle(handle);
71
72 result != 0 && exit_code == 259
74 }
75}
76
77#[cfg(windows)]
78extern "system" {
79 fn OpenProcess(
80 dwDesiredAccess: u32,
81 bInheritHandle: i32,
82 dwProcessId: u32,
83 ) -> *mut std::ffi::c_void;
84 fn GetExitCodeProcess(hProcess: *mut std::ffi::c_void, lpExitCode: *mut u32) -> i32;
85 fn CloseHandle(hObject: *mut std::ffi::c_void) -> i32;
86 fn TerminateProcess(hProcess: *mut std::ffi::c_void, uExitCode: u32) -> i32;
87}
88
89pub fn read_pid() -> Result<Option<i32>> {
91 let pid_file = get_pid_file_path()?;
92
93 if !pid_file.exists() {
94 return Ok(None);
95 }
96
97 let content = fs::read_to_string(&pid_file)
98 .with_context(|| format!("Failed to read PID file: {:?}", pid_file))?;
99
100 let pid: i32 = content
101 .trim()
102 .parse()
103 .with_context(|| format!("Invalid PID in file: {}", content.trim()))?;
104
105 Ok(Some(pid))
106}
107
108pub fn write_pid() -> Result<()> {
110 let pid_file = get_pid_file_path()?;
111 let pid = std::process::id();
112
113 fs::write(&pid_file, pid.to_string())
114 .with_context(|| format!("Failed to write PID file: {:?}", pid_file))?;
115
116 Ok(())
117}
118
119pub fn remove_pid_file() -> Result<()> {
121 let pid_file = get_pid_file_path()?;
122
123 if pid_file.exists() {
124 fs::remove_file(&pid_file)
125 .with_context(|| format!("Failed to remove PID file: {:?}", pid_file))?;
126 }
127
128 Ok(())
129}
130
131#[derive(Debug)]
133pub struct DaemonStatus {
134 pub running: bool,
135 pub pid: Option<i32>,
136 pub pid_file_exists: bool,
137}
138
139pub fn check_status() -> Result<DaemonStatus> {
141 let pid_file = get_pid_file_path()?;
142 let pid_file_exists = pid_file.exists();
143
144 let (running, pid) = match read_pid()? {
145 Some(pid) => {
146 let running = is_process_running(pid);
147 (running, Some(pid))
148 }
149 None => (false, None),
150 };
151
152 Ok(DaemonStatus {
153 running,
154 pid,
155 pid_file_exists,
156 })
157}
158
159#[cfg(unix)]
161pub fn stop_daemon() -> Result<bool> {
162 let status = check_status()?;
163
164 if !status.running {
165 if status.pid_file_exists {
166 remove_pid_file()?;
167 println!("Removed stale PID file (process was not running)");
168 }
169 return Ok(false);
170 }
171
172 let pid = status.pid.unwrap();
173 println!("Sending SIGTERM to daemon (PID: {})", pid);
174
175 let result = unsafe { libc::kill(pid, libc::SIGTERM) };
176
177 if result != 0 {
178 anyhow::bail!(
179 "Failed to send SIGTERM to process {}: {}",
180 pid,
181 std::io::Error::last_os_error()
182 );
183 }
184
185 let start = std::time::Instant::now();
187 let timeout = std::time::Duration::from_secs(10);
188
189 while is_process_running(pid) {
190 if start.elapsed() > timeout {
191 println!("Process didn't exit within 10 seconds, sending SIGKILL");
192 unsafe { libc::kill(pid, libc::SIGKILL) };
193 std::thread::sleep(std::time::Duration::from_millis(500));
194 break;
195 }
196 std::thread::sleep(std::time::Duration::from_millis(100));
197 }
198
199 remove_pid_file()?;
200 Ok(true)
201}
202
203#[cfg(windows)]
204pub fn stop_daemon() -> Result<bool> {
205 let status = check_status()?;
206
207 if !status.running {
208 if status.pid_file_exists {
209 remove_pid_file()?;
210 println!("Removed stale PID file (process was not running)");
211 }
212 return Ok(false);
213 }
214
215 let pid = status.pid.unwrap();
216 println!("Terminating daemon (PID: {})", pid);
217
218 const PROCESS_TERMINATE: u32 = 0x0001;
219
220 unsafe {
221 let handle = OpenProcess(PROCESS_TERMINATE, 0, pid as u32);
222 if handle.is_null() {
223 anyhow::bail!(
224 "Failed to open process {}: {}",
225 pid,
226 std::io::Error::last_os_error()
227 );
228 }
229
230 let result = TerminateProcess(handle, 0);
231 CloseHandle(handle);
232
233 if result == 0 {
234 anyhow::bail!(
235 "Failed to terminate process {}: {}",
236 pid,
237 std::io::Error::last_os_error()
238 );
239 }
240 }
241
242 std::thread::sleep(std::time::Duration::from_millis(500));
244
245 remove_pid_file()?;
246 Ok(true)
247}
248
249#[cfg(unix)]
251pub fn daemonize() -> Result<()> {
252 use daemonize::Daemonize;
253 use std::fs::OpenOptions;
254
255 let pid_file = get_pid_file_path()?;
256 let log_file = get_log_file_path()?;
257
258 let status = check_status()?;
260 if status.running {
261 anyhow::bail!(
262 "Daemon is already running (PID: {}). Use --stop to stop it first.",
263 status.pid.unwrap()
264 );
265 }
266
267 if status.pid_file_exists {
269 remove_pid_file()?;
270 }
271
272 let stdout = OpenOptions::new()
274 .create(true)
275 .append(true)
276 .open(&log_file)
277 .with_context(|| format!("Failed to open log file: {:?}", log_file))?;
278
279 let stderr = OpenOptions::new()
280 .create(true)
281 .append(true)
282 .open(&log_file)
283 .with_context(|| format!("Failed to open log file: {:?}", log_file))?;
284
285 println!("Starting daemon...");
286 println!("PID file: {:?}", pid_file);
287 println!("Log file: {:?}", log_file);
288
289 let daemonize = Daemonize::new()
290 .pid_file(&pid_file)
291 .chown_pid_file(true)
292 .working_directory(".")
293 .stdout(stdout)
294 .stderr(stderr);
295
296 daemonize.start().context("Failed to daemonize process")?;
297
298 tracing::info!("Daemon started (PID: {})", std::process::id());
299 Ok(())
300}
301
302#[cfg(windows)]
304pub fn daemonize() -> Result<()> {
305 use std::os::windows::process::CommandExt;
306 use std::process::Command;
307
308 let pid_file = get_pid_file_path()?;
309 let log_file = get_log_file_path()?;
310
311 let status = check_status()?;
313 if status.running {
314 anyhow::bail!(
315 "Daemon is already running (PID: {}). Use --stop to stop it first.",
316 status.pid.unwrap()
317 );
318 }
319
320 if status.pid_file_exists {
322 remove_pid_file()?;
323 }
324
325 let exe = std::env::current_exe().context("Failed to get current executable path")?;
327
328 let args: Vec<String> = std::env::args()
330 .skip(1) .filter(|arg| arg != "--daemon")
332 .collect();
333
334 let mut daemon_args = args.clone();
336 daemon_args.push("--daemon-child".to_string());
337
338 println!("Starting daemon...");
339 println!("PID file: {:?}", pid_file);
340 println!("Log file: {:?}", log_file);
341
342 const CREATE_NO_WINDOW: u32 = 0x08000000;
345
346 let child = Command::new(exe)
348 .args(&daemon_args)
349 .creation_flags(CREATE_NO_WINDOW)
350 .spawn()
351 .context("Failed to spawn daemon process")?;
352
353 let pid = child.id();
354 println!("Daemon started with PID: {}", pid);
355
356 Ok(())
358}
359
360pub fn is_daemon_child() -> bool {
363 std::env::args().any(|arg| arg == "--daemon-child")
364}
365
366pub fn init_daemon_child() -> Result<PathBuf> {
369 let log_file = get_log_file_path()?;
370
371 write_pid()?;
373
374 Ok(log_file)
375}
376
377pub fn print_status() -> Result<()> {
379 let status = check_status()?;
380 let log_file = get_log_file_path()?;
381
382 if status.running {
383 println!("Daemon status: RUNNING");
384 println!("PID: {}", status.pid.unwrap());
385 println!("Log file: {:?}", log_file);
386
387 if log_file.exists() {
389 println!("\nRecent log entries:");
390 println!("-------------------");
391 let content = fs::read_to_string(&log_file)?;
392 let lines: Vec<&str> = content.lines().collect();
393 let start = if lines.len() > 10 {
394 lines.len() - 10
395 } else {
396 0
397 };
398 for line in &lines[start..] {
399 println!("{}", line);
400 }
401 }
402 } else {
403 println!("Daemon status: NOT RUNNING");
404 if status.pid_file_exists {
405 println!(
406 "Note: Stale PID file exists (PID {} is not running)",
407 status.pid.unwrap_or(0)
408 );
409 println!("Run with --stop to clean up the stale PID file");
410 }
411 }
412
413 Ok(())
414}
415
416pub fn cleanup() -> Result<()> {
418 remove_pid_file()
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424
425 #[test]
426 fn test_daemon_dir_creation() {
427 let dir = get_daemon_dir();
428 assert!(dir.is_ok());
429 let path = dir.unwrap();
430 assert!(path.to_string_lossy().contains("seren-replicator"));
431 }
432
433 #[test]
434 fn test_pid_file_path() {
435 let path = get_pid_file_path();
436 assert!(path.is_ok());
437 let path = path.unwrap();
438 assert!(path.to_string_lossy().ends_with("sync.pid"));
439 }
440
441 #[test]
442 fn test_log_file_path() {
443 let path = get_log_file_path();
444 assert!(path.is_ok());
445 let path = path.unwrap();
446 assert!(path.to_string_lossy().ends_with("sync.log"));
447 }
448
449 #[test]
450 fn test_check_status_no_daemon() {
451 let status = check_status();
452 assert!(status.is_ok());
453 }
454
455 #[test]
456 fn test_is_daemon_child_false() {
457 let result = is_daemon_child();
460 let _ = result;
462 }
463}