1pub mod server;
26pub mod state;
27pub mod watcher;
28
29use anyhow::Result;
30use std::sync::Arc;
31use tokio::signal;
32use tokio::sync::{oneshot, RwLock};
33use tracing_appender::non_blocking::WorkerGuard;
34
35pub use server::{send_command_sync, DaemonCommand, DaemonResponse};
36pub use state::{DaemonState, DaemonStats};
37pub use watcher::SessionWatcher;
38
39pub async fn run_daemon() -> Result<()> {
56 let state = DaemonState::new()?;
57
58 if state.is_running() {
60 anyhow::bail!(
61 "Daemon is already running (PID {})",
62 state.get_pid().unwrap_or(0)
63 );
64 }
65
66 let _guard = setup_logging(&state)?;
68
69 tracing::info!("Starting Lore daemon...");
70
71 let pid = std::process::id();
73 state.write_pid(pid)?;
74 tracing::info!("Daemon started with PID {}", pid);
75
76 let stats = Arc::new(RwLock::new(DaemonStats::default()));
78
79 let (stop_tx, stop_rx) = oneshot::channel::<()>();
81 let (broadcast_tx, _) = tokio::sync::broadcast::channel::<()>(1);
82
83 let server_stats = stats.clone();
85 let socket_path = state.socket_path.clone();
86 let server_broadcast_rx = broadcast_tx.subscribe();
87 let server_handle = tokio::spawn(async move {
88 if let Err(e) = server::run_server(
89 &socket_path,
90 server_stats,
91 Some(stop_tx),
92 server_broadcast_rx,
93 )
94 .await
95 {
96 tracing::error!("IPC server error: {}", e);
97 }
98 });
99
100 let mut watcher = SessionWatcher::new()?;
102 let watcher_stats = stats.clone();
103 let watcher_broadcast_rx = broadcast_tx.subscribe();
104 let watcher_handle = tokio::spawn(async move {
105 if let Err(e) = watcher.watch(watcher_stats, watcher_broadcast_rx).await {
106 tracing::error!("Watcher error: {}", e);
107 }
108 });
109
110 tokio::select! {
112 _ = signal::ctrl_c() => {
113 tracing::info!("Received Ctrl+C, shutting down...");
114 }
115 _ = stop_rx => {
116 tracing::info!("Received stop command, shutting down...");
117 }
118 }
119
120 let _ = broadcast_tx.send(());
122
123 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
125
126 server_handle.abort();
128 watcher_handle.abort();
129
130 state.cleanup()?;
132
133 tracing::info!("Daemon stopped");
134
135 Ok(())
136}
137
138fn setup_logging(state: &DaemonState) -> Result<WorkerGuard> {
145 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
146
147 let file_appender = tracing_appender::rolling::never(
148 state.log_file.parent().unwrap_or(std::path::Path::new(".")),
149 state.log_file.file_name().unwrap_or_default(),
150 );
151 let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
152
153 let file_layer = tracing_subscriber::fmt::layer()
154 .with_writer(non_blocking)
155 .with_ansi(false);
156
157 let _ = tracing_subscriber::registry()
160 .with(
161 tracing_subscriber::EnvFilter::try_from_default_env()
162 .unwrap_or_else(|_| "lore=info".into()),
163 )
164 .with(file_layer)
165 .try_init();
166
167 Ok(guard)
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_daemon_state_paths() {
176 let state = DaemonState::new();
178 assert!(state.is_ok(), "DaemonState creation should succeed");
179
180 let state = state.unwrap();
181 assert!(
182 state.pid_file.to_string_lossy().contains("daemon.pid"),
183 "PID file path should contain daemon.pid"
184 );
185 assert!(
186 state.socket_path.to_string_lossy().contains("daemon.sock"),
187 "Socket path should contain daemon.sock"
188 );
189 assert!(
190 state.log_file.to_string_lossy().contains("daemon.log"),
191 "Log file path should contain daemon.log"
192 );
193 }
194}