#![expect( clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss )] use std::{ fs::{self, File, OpenOptions}, io::Write, path::{Path, PathBuf}, }; use clap::Parser; use eyre::WrapErr; use eyre::{Result, bail}; use fs4::fs_std::FileExt; use tracing_subscriber::util::SubscriberInitExt; use crate::{ aclient::{database::ClientSqlite, record::sqlite_store::SqliteStore, settings::Settings}, api::{control::ControlService, history::HistoryService}, daemon::Daemon, }; pub(crate) mod aclient; pub(crate) mod api; pub(crate) mod daemon; pub(crate) mod events; pub(crate) mod server; const DAEMON_VERSION: &str = env!("CARGO_PKG_VERSION"); #[derive(Parser, Debug)] #[command(infer_subcommands = true)] enum Cmd { /// Start the daemon server Start { /// Also write daemon logs to the console (useful for debugging) #[arg(long)] show_logs: bool, }, } #[tokio::main] async fn main() -> Result<()> { if let Err(e) = tracing_subscriber::registry().try_init() { eprintln!("failed to initialize logging: {e}"); } let settings = Settings::new().wrap_err("could not load client settings")?; if !settings.paths_ok() { bail!("Failed to verify all paths :("); } let db_path = PathBuf::from(settings.db_path.as_str()); let record_store_path = PathBuf::from(settings.record_store_path.as_str()); let history_db = ClientSqlite::new(db_path, settings.local_timeout).await?; let sqlite_store = SqliteStore::new(record_store_path, settings.local_timeout).await?; match Cmd::parse() { Cmd::Start { show_logs, .. } => boot(settings, sqlite_store, history_db).await, } } /// Boot the daemon. /// /// This creates a daemon, /// starts the gRPC server with services, and runs the event loop. async fn boot(settings: Settings, store: SqliteStore, history_db: ClientSqlite) -> Result<()> { let pidfile_path = PathBuf::from(&settings.daemon.pidfile_path); let _pidfile_guard = PidfileGuard::acquire(&pidfile_path)?; let mut daemon = Daemon::builder(settings.clone()) .store(store) .history_db(history_db) .build()?; let handle = { let handle = daemon.handle(); // Spawn signal handler to emit ShutdownRequested on Ctrl+C/SIGTERM let signal_handle = handle.clone(); tokio::spawn(async move { shutdown_signal().await; tracing::info!("received shutdown signal"); signal_handle.shutdown(); }); handle }; let history_service = HistoryService::new(handle.clone()).await?; let control_service = ControlService::new(handle.clone()); server::run_grpc_server( &settings, history_service.into_server(), control_service.into_server(), handle, )?; daemon.wait_for_shutdown().await?; tracing::info!("daemon shut down complete"); Ok(()) } /// Wait for a shutdown signal (Ctrl+C or SIGTERM). #[cfg(unix)] async fn shutdown_signal() { let mut term = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) .expect("failed to register sigterm handler"); let mut int = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt()) .expect("failed to register sigint handler"); tokio::select! { _ = term.recv() => {}, _ = int.recv() => {}, } } struct PidfileGuard { file: File, } impl PidfileGuard { fn acquire(path: &Path) -> Result { let mut file = open_lock_file(path)?; if !file.try_lock_exclusive()? { bail!( "daemon already running (pidfile lock busy at {})", path.display() ); } file.set_len(0) .wrap_err_with(|| format!("could not truncate daemon pidfile {}", path.display()))?; writeln!(file, "{}", std::process::id()) .and_then(|()| writeln!(file, "{DAEMON_VERSION}")) .wrap_err_with(|| format!("could not write daemon pidfile {}", path.display()))?; Ok(Self { file }) } } impl Drop for PidfileGuard { fn drop(&mut self) { drop(self.file.unlock()); } } fn open_lock_file(path: &Path) -> Result { if let Some(parent) = path.parent() { fs::create_dir_all(parent) .wrap_err_with(|| format!("could not create lock directory {}", parent.display()))?; } OpenOptions::new() .read(true) .write(true) .create(true) .truncate(false) .open(path) .wrap_err_with(|| format!("could not open lock file {}", path.display())) }