aboutsummaryrefslogtreecommitdiffstats
path: root/crates/daemon/src/main.rs
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-20 15:13:12 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-20 15:13:12 +0200
commit1594307d9cd819b1ed739fa1ec048c6b27e6f4b0 (patch)
tree39b427e3cd6390bc218136454b809450016e0037 /crates/daemon/src/main.rs
parentchore: Commit (diff)
downloadatuin-1594307d9cd819b1ed739fa1ec048c6b27e6f4b0.zip
chore: Commit
Diffstat (limited to 'crates/daemon/src/main.rs')
-rw-r--r--crates/daemon/src/main.rs223
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}");
}
}