1pub mod server;
28pub mod state;
29pub mod sync;
30pub mod watcher;
31
32use anyhow::Result;
33use std::sync::Arc;
34use tokio::signal;
35use tokio::sync::{oneshot, RwLock};
36use tracing_appender::non_blocking::WorkerGuard;
37
38use crate::config::Config;
39
40pub use server::{send_command_sync, DaemonCommand, DaemonResponse};
41pub use state::{DaemonState, DaemonStats};
42pub use watcher::SessionWatcher;
43
44pub use sync::SyncState;
46
47pub async fn run_daemon() -> Result<()> {
64 let state = DaemonState::new()?;
65
66 if state.is_running() {
68 anyhow::bail!(
69 "Daemon is already running (PID {})",
70 state.get_pid().unwrap_or(0)
71 );
72 }
73
74 let config_path = Config::config_path()?;
76 if !config_path.exists() {
77 eprintln!(
78 "Error: Lore has not been initialized.\n\n\
79 Run 'lore init' first to:\n \
80 - Select which AI tools to watch\n \
81 - Configure your machine identity\n \
82 - Import existing sessions\n\n\
83 Then start the daemon with 'lore daemon start' or let init do it for you."
84 );
85 std::process::exit(0);
87 }
88
89 let _guard = setup_logging(&state)?;
91
92 tracing::info!("Starting Lore daemon...");
93
94 let pid = std::process::id();
96 state.write_pid(pid)?;
97 tracing::info!("Daemon started with PID {}", pid);
98
99 let stats = Arc::new(RwLock::new(DaemonStats::default()));
101
102 let sync_state = Arc::new(RwLock::new(sync::SyncState::load().unwrap_or_default()));
104
105 let (stop_tx, stop_rx) = oneshot::channel::<()>();
107 let (broadcast_tx, _) = tokio::sync::broadcast::channel::<()>(1);
108
109 let server_stats = stats.clone();
111 let socket_path = state.socket_path.clone();
112 let server_broadcast_rx = broadcast_tx.subscribe();
113 let server_handle = tokio::spawn(async move {
114 if let Err(e) = server::run_server(
115 &socket_path,
116 server_stats,
117 Some(stop_tx),
118 server_broadcast_rx,
119 )
120 .await
121 {
122 tracing::error!("IPC server error: {}", e);
123 }
124 });
125
126 let mut watcher = SessionWatcher::new()?;
128 let watcher_stats = stats.clone();
129 let watcher_broadcast_rx = broadcast_tx.subscribe();
130 let watcher_handle = tokio::spawn(async move {
131 if let Err(e) = watcher.watch(watcher_stats, watcher_broadcast_rx).await {
132 tracing::error!("Watcher error: {}", e);
133 }
134 });
135
136 let sync_broadcast_rx = broadcast_tx.subscribe();
138 let sync_handle = tokio::spawn(async move {
139 sync::run_periodic_sync(sync_state, sync_broadcast_rx).await;
140 });
141
142 tokio::select! {
144 _ = signal::ctrl_c() => {
145 tracing::info!("Received Ctrl+C, shutting down...");
146 }
147 _ = stop_rx => {
148 tracing::info!("Received stop command, shutting down...");
149 }
150 }
151
152 let _ = broadcast_tx.send(());
154
155 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
157
158 server_handle.abort();
160 watcher_handle.abort();
161 sync_handle.abort();
162
163 state.cleanup()?;
165
166 tracing::info!("Daemon stopped");
167
168 Ok(())
169}
170
171fn setup_logging(state: &DaemonState) -> Result<WorkerGuard> {
178 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
179
180 let file_appender = tracing_appender::rolling::never(
181 state.log_file.parent().unwrap_or(std::path::Path::new(".")),
182 state.log_file.file_name().unwrap_or_default(),
183 );
184 let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
185
186 let file_layer = tracing_subscriber::fmt::layer()
187 .with_writer(non_blocking)
188 .with_ansi(false);
189
190 let _ = tracing_subscriber::registry()
193 .with(
194 tracing_subscriber::EnvFilter::try_from_default_env()
195 .unwrap_or_else(|_| "lore=info".into()),
196 )
197 .with(file_layer)
198 .try_init();
199
200 Ok(guard)
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn test_daemon_state_paths() {
209 let state = DaemonState::new();
211 assert!(state.is_ok(), "DaemonState creation should succeed");
212
213 let state = state.unwrap();
214 assert!(
215 state.pid_file.to_string_lossy().contains("daemon.pid"),
216 "PID file path should contain daemon.pid"
217 );
218 assert!(
219 state.socket_path.to_string_lossy().contains("daemon.sock"),
220 "Socket path should contain daemon.sock"
221 );
222 assert!(
223 state.log_file.to_string_lossy().contains("daemon.log"),
224 "Log file path should contain daemon.log"
225 );
226 }
227}