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
35use crate::config::Config;
36
37pub use server::{send_command_sync, DaemonCommand, DaemonResponse};
38pub use state::{DaemonState, DaemonStats};
39pub use watcher::SessionWatcher;
40
41pub async fn run_daemon() -> Result<()> {
58 let state = DaemonState::new()?;
59
60 if state.is_running() {
62 anyhow::bail!(
63 "Daemon is already running (PID {})",
64 state.get_pid().unwrap_or(0)
65 );
66 }
67
68 let config_path = Config::config_path()?;
70 if !config_path.exists() {
71 eprintln!(
72 "Error: Lore has not been initialized.\n\n\
73 Run 'lore init' first to:\n \
74 - Select which AI tools to watch\n \
75 - Configure your machine identity\n \
76 - Import existing sessions\n\n\
77 Then start the daemon with 'lore daemon start' or let init do it for you."
78 );
79 std::process::exit(0);
81 }
82
83 let _guard = setup_logging(&state)?;
85
86 tracing::info!("Starting Lore daemon...");
87
88 let pid = std::process::id();
90 state.write_pid(pid)?;
91 tracing::info!("Daemon started with PID {}", pid);
92
93 let stats = Arc::new(RwLock::new(DaemonStats::default()));
95
96 let (stop_tx, stop_rx) = oneshot::channel::<()>();
98 let (broadcast_tx, _) = tokio::sync::broadcast::channel::<()>(1);
99
100 let server_stats = stats.clone();
102 let socket_path = state.socket_path.clone();
103 let server_broadcast_rx = broadcast_tx.subscribe();
104 let server_handle = tokio::spawn(async move {
105 if let Err(e) = server::run_server(
106 &socket_path,
107 server_stats,
108 Some(stop_tx),
109 server_broadcast_rx,
110 )
111 .await
112 {
113 tracing::error!("IPC server error: {}", e);
114 }
115 });
116
117 let mut watcher = SessionWatcher::new()?;
119 let watcher_stats = stats.clone();
120 let watcher_broadcast_rx = broadcast_tx.subscribe();
121 let watcher_handle = tokio::spawn(async move {
122 if let Err(e) = watcher.watch(watcher_stats, watcher_broadcast_rx).await {
123 tracing::error!("Watcher error: {}", e);
124 }
125 });
126
127 tokio::select! {
129 _ = signal::ctrl_c() => {
130 tracing::info!("Received Ctrl+C, shutting down...");
131 }
132 _ = stop_rx => {
133 tracing::info!("Received stop command, shutting down...");
134 }
135 }
136
137 let _ = broadcast_tx.send(());
139
140 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
142
143 server_handle.abort();
145 watcher_handle.abort();
146
147 state.cleanup()?;
149
150 tracing::info!("Daemon stopped");
151
152 Ok(())
153}
154
155fn setup_logging(state: &DaemonState) -> Result<WorkerGuard> {
162 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
163
164 let file_appender = tracing_appender::rolling::never(
165 state.log_file.parent().unwrap_or(std::path::Path::new(".")),
166 state.log_file.file_name().unwrap_or_default(),
167 );
168 let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
169
170 let file_layer = tracing_subscriber::fmt::layer()
171 .with_writer(non_blocking)
172 .with_ansi(false);
173
174 let _ = tracing_subscriber::registry()
177 .with(
178 tracing_subscriber::EnvFilter::try_from_default_env()
179 .unwrap_or_else(|_| "lore=info".into()),
180 )
181 .with(file_layer)
182 .try_init();
183
184 Ok(guard)
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn test_daemon_state_paths() {
193 let state = DaemonState::new();
195 assert!(state.is_ok(), "DaemonState creation should succeed");
196
197 let state = state.unwrap();
198 assert!(
199 state.pid_file.to_string_lossy().contains("daemon.pid"),
200 "PID file path should contain daemon.pid"
201 );
202 assert!(
203 state.socket_path.to_string_lossy().contains("daemon.sock"),
204 "Socket path should contain daemon.sock"
205 );
206 assert!(
207 state.log_file.to_string_lossy().contains("daemon.log"),
208 "Log file path should contain daemon.log"
209 );
210 }
211}