diff options
Diffstat (limited to '')
| -rw-r--r-- | crates/daemon/src/main.rs | 223 |
1 files changed, 8 insertions, 215 deletions
diff --git a/crates/daemon/src/main.rs b/crates/daemon/src/main.rs index 174cf94b..1966e113 100644 --- a/crates/daemon/src/main.rs +++ b/crates/daemon/src/main.rs @@ -1,17 +1,12 @@ #![expect(unused_crate_dependencies, reason = "Didn't remove them yet")] use clap::Parser; -use eyre::{Result, WrapErr, bail, eyre}; -use fs4::fs_std::FileExt; -use std::fs::{self, File, OpenOptions}; -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::time::{Duration, Instant}; -use tokio::time::sleep; -use turtle_daemon::{ - aclient::{database::ClientSqlite, record::sqlite_store::SqliteStore, settings::Settings}, - client::{DaemonClientErrorKind, HistoryClient, classify_error}, +use eyre::{Result, WrapErr}; +use std::path::PathBuf; +use turtle_daemon::aclient::{ + database::ClientSqlite, record::sqlite_store::SqliteStore, settings::Settings, }; +use turtle_daemon::api::client::{Probe, probe}; #[derive(Parser, Debug)] #[command(infer_subcommands = true)] @@ -22,12 +17,6 @@ pub(crate) enum Cmd { #[arg(long)] show_logs: bool, }, - - /// Show the daemon's current status - Status, - - /// Stop the daemon gracefully - Stop, } #[tokio::main] @@ -36,208 +25,12 @@ async fn main() -> Result<()> { let db_path = PathBuf::from(settings.db_path.as_str()); let record_store_path = PathBuf::from(settings.record_store_path.as_str()); - let db = ClientSqlite::new(db_path, settings.local_timeout).await?; + 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, .. } => start_cmd(settings, store, history_db, show_logs).await, - Cmd::Status => status_cmd(&settings).await, - Cmd::Stop => stop_cmd(&settings).await, - } -} - -const STARTUP_POLL: Duration = Duration::from_millis(40); -const LEGACY_DAEMON_RESTART_MESSAGE: &str = "legacy daemon detected; restart daemon manually"; - -struct PidfileGuard { - file: File, -} - -impl PidfileGuard { - fn acquire(path: &Path) -> Result<Self> { - 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()); - } -} - -enum Probe { - Ready(HistoryClient), - NeedsRestart(String), - Unreachable(eyre::Report), -} - -fn is_legacy_daemon_error(err: &eyre::Report) -> bool { - matches!(classify_error(err), DaemonClientErrorKind::Unimplemented) -} - -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())) -} - -async fn wait_for_lock(path: &Path, timeout: Duration) -> Result<File> { - const LOCK_POLL: Duration = Duration::from_millis(20); - - let file = open_lock_file(path)?; - let start = Instant::now(); - - loop { - match file.try_lock_exclusive() { - Ok(true) => return Ok(file), - Ok(false) => { - if start.elapsed() >= timeout { - bail!("timed out waiting for lock at {}", path.display()); - } - - sleep(LOCK_POLL).await; - } - Err(err) => { - return Err(eyre!("could not lock {}: {err}", path.display())); - } - } - } -} - -async fn wait_for_pidfile_available(path: &Path, timeout: Duration) -> Result<()> { - let file = wait_for_lock(path, timeout).await?; - file.unlock() - .wrap_err_with(|| format!("failed to unlock {}", path.display()))?; - Ok(()) -} - -async fn request_shutdown(settings: &Settings) { - if let Ok(mut client) = connect_client(settings).await { - drop(client.shutdown().await); - } -} - -fn startup_timeout(settings: &Settings) -> Duration { - Duration::from_secs_f64(settings.local_timeout.max(0.5) + 2.0) -} - -async fn status_cmd(settings: &Settings) -> Result<()> { - match probe(settings).await { - Probe::Ready(mut client) => { - let status = client.status().await?; - println!("Daemon running"); - println!(" PID: {}", status.pid); - println!(" Version: {}", status.version); - println!(" Protocol: {}", status.protocol); - println!(" Healthy: {}", status.healthy); - println!(" Socket: {}", settings.daemon.socket_path); - } - Probe::NeedsRestart(reason) => { - println!("Daemon running (needs restart)"); - println!(" Reason: {reason}"); - } - Probe::Unreachable(_) => { - println!("Daemon is not running"); - } - } - - Ok(()) -} - -async fn stop_cmd(settings: &Settings) -> Result<()> { - let Ok(mut client) = connect_client(settings).await else { - println!("Daemon is not running"); - return Ok(()); - }; - - match client.shutdown().await { - Ok(true) => { - println!("Shutdown requested"); - - let pidfile_path = PathBuf::from(&settings.daemon.pidfile_path); - let timeout = Duration::from_secs(5); - match wait_for_pidfile_available(&pidfile_path, timeout).await { - Ok(()) => println!("Daemon stopped"), - Err(_) => println!("Daemon may still be shutting down"), - } - - Ok(()) + Cmd::Start { show_logs, .. } => { + turtle_daemon::boot(settings, sqlite_store, history_db).await } - Ok(false) => bail!("Daemon rejected shutdown request"), - Err(err) => Err(err.wrap_err("Failed to send shutdown request")), - } -} - -async fn start_cmd( - settings: Settings, - store: SqliteStore, - history_db: ClientSqlite, - show_logs: bool, -) -> Result<()> { - let pidfile_path = PathBuf::from(&settings.daemon.pidfile_path); - let _pidfile_guard = PidfileGuard::acquire(&pidfile_path)?; - - turtle_daemon::boot(settings, store, history_db).await?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::{ - DAEMON_PROTOCOL_VERSION, DAEMON_VERSION, daemon_matches_expected, daemon_mismatch_message, - }; - - #[test] - fn test_version_matches() { - assert!(daemon_matches_expected( - DAEMON_VERSION, - DAEMON_PROTOCOL_VERSION - )); - } - - #[test] - fn test_version_mismatch() { - assert!(!daemon_matches_expected("0.0.0", DAEMON_PROTOCOL_VERSION)); - assert!(!daemon_matches_expected(DAEMON_VERSION, 999)); - assert!(!daemon_matches_expected("0.0.0", 999)); - } - - #[test] - fn test_mismatch_message_version() { - let msg = daemon_mismatch_message("0.0.0", DAEMON_PROTOCOL_VERSION); - assert!(msg.contains("out of date"), "got: {msg}"); - assert!(msg.contains("0.0.0")); - assert!(msg.contains(DAEMON_VERSION)); - } - - #[test] - fn test_mismatch_message_protocol() { - let msg = daemon_mismatch_message(DAEMON_VERSION, 999); - assert!(msg.contains("protocol mismatch"), "got: {msg}"); } } |
