pub mod server;
pub mod state;
pub mod sync;
pub mod watcher;
use anyhow::Result;
use std::sync::Arc;
use tokio::signal;
use tokio::sync::{oneshot, RwLock};
use tracing_appender::non_blocking::WorkerGuard;
use crate::config::Config;
pub use server::{send_command_sync, DaemonCommand, DaemonResponse};
pub use state::{DaemonState, DaemonStats};
pub use watcher::SessionWatcher;
pub use sync::SyncState;
pub async fn run_daemon() -> Result<()> {
let state = DaemonState::new()?;
if state.is_running() {
anyhow::bail!(
"Daemon is already running (PID {})",
state.get_pid().unwrap_or(0)
);
}
let config_path = Config::config_path()?;
if !config_path.exists() {
eprintln!(
"Error: Lore has not been initialized.\n\n\
Run 'lore init' first to:\n \
- Select which AI tools to watch\n \
- Configure your machine identity\n \
- Import existing sessions\n\n\
Then start the daemon with 'lore daemon start' or let init do it for you."
);
std::process::exit(0);
}
let _guard = setup_logging(&state)?;
tracing::info!("Starting Lore daemon...");
let pid = std::process::id();
state.write_pid(pid)?;
tracing::info!("Daemon started with PID {}", pid);
let stats = Arc::new(RwLock::new(DaemonStats::default()));
let sync_state = Arc::new(RwLock::new(sync::SyncState::load().unwrap_or_default()));
let (stop_tx, stop_rx) = oneshot::channel::<()>();
let (broadcast_tx, _) = tokio::sync::broadcast::channel::<()>(1);
let server_stats = stats.clone();
let socket_path = state.socket_path.clone();
let server_broadcast_rx = broadcast_tx.subscribe();
let server_handle = tokio::spawn(async move {
if let Err(e) = server::run_server(
&socket_path,
server_stats,
Some(stop_tx),
server_broadcast_rx,
)
.await
{
tracing::error!("IPC server error: {}", e);
}
});
let mut watcher = SessionWatcher::new()?;
let watcher_stats = stats.clone();
let watcher_broadcast_rx = broadcast_tx.subscribe();
let watcher_handle = tokio::spawn(async move {
if let Err(e) = watcher.watch(watcher_stats, watcher_broadcast_rx).await {
tracing::error!("Watcher error: {}", e);
}
});
let sync_broadcast_rx = broadcast_tx.subscribe();
let sync_handle = tokio::spawn(async move {
sync::run_periodic_sync(sync_state, sync_broadcast_rx).await;
});
tokio::select! {
_ = signal::ctrl_c() => {
tracing::info!("Received Ctrl+C, shutting down...");
}
_ = stop_rx => {
tracing::info!("Received stop command, shutting down...");
}
}
let _ = broadcast_tx.send(());
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
server_handle.abort();
watcher_handle.abort();
sync_handle.abort();
state.cleanup()?;
tracing::info!("Daemon stopped");
Ok(())
}
fn setup_logging(state: &DaemonState) -> Result<WorkerGuard> {
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
let file_appender = tracing_appender::rolling::never(
state.log_file.parent().unwrap_or(std::path::Path::new(".")),
state.log_file.file_name().unwrap_or_default(),
);
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let file_layer = tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_ansi(false);
let _ = tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "lore=info".into()),
)
.with(file_layer)
.try_init();
Ok(guard)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_daemon_state_paths() {
let state = DaemonState::new();
assert!(state.is_ok(), "DaemonState creation should succeed");
let state = state.unwrap();
assert!(
state.pid_file.to_string_lossy().contains("daemon.pid"),
"PID file path should contain daemon.pid"
);
assert!(
state.socket_path.to_string_lossy().contains("daemon.sock"),
"Socket path should contain daemon.sock"
);
assert!(
state.log_file.to_string_lossy().contains("daemon.log"),
"Log file path should contain daemon.log"
);
}
}