diff options
Diffstat (limited to '')
| -rw-r--r-- | crates/daemon/src/main.rs | 180 |
1 files changed, 180 insertions, 0 deletions
diff --git a/crates/daemon/src/main.rs b/crates/daemon/src/main.rs new file mode 100644 index 00000000..50a41775 --- /dev/null +++ b/crates/daemon/src/main.rs @@ -0,0 +1,180 @@ +#![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 tracing_subscriber::EnvFilter; + +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<()> { + 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, .. } => { + if show_logs + && let Err(e) = tracing_subscriber::fmt() + .with_file(true) + .with_line_number(true) + .with_level(true) + .without_time() + .with_env_filter( + EnvFilter::builder() + .from_env_lossy() + .add_directive("turtle_daemon=debug".parse().unwrap()), + ) + .try_init() + { + eprintln!("failed to initialize logging: {e}"); + } + + 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<Self> { + let mut file = open_lock_file(path)?; + + if let Err(fs::TryLockError::WouldBlock) = file.try_lock() { + 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<File> { + 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())) +} |
