diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-07-20 12:57:36 +0200 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-07-20 12:57:36 +0200 |
| commit | 73be69cd99f5a4784fe69f5d78f423c72e837284 (patch) | |
| tree | cb47246d2c7edd8cd828abb25a033b732d2289ae /crates/daemon/src/main.rs | |
| parent | chore: Move more stuff out of atuin-client (diff) | |
| download | atuin-73be69cd99f5a4784fe69f5d78f423c72e837284.zip | |
chore: Commit
Diffstat (limited to '')
| -rw-r--r-- | crates/daemon/src/main.rs (renamed from crates/client/src/command/client/daemon.rs) | 270 |
1 files changed, 32 insertions, 238 deletions
diff --git a/crates/client/src/command/client/daemon.rs b/crates/daemon/src/main.rs index 39aa1b1e..26a5cafd 100644 --- a/crates/client/src/command/client/daemon.rs +++ b/crates/daemon/src/main.rs @@ -1,3 +1,7 @@ +#[allow(unused_imports)] +use clap::Parser; +use eyre::{Result, WrapErr, bail, eyre}; +use fs4::fs_std::FileExt; use std::fs::{self, File, OpenOptions}; use std::io::{ErrorKind, Write}; #[cfg(unix)] @@ -5,30 +9,19 @@ use std::os::unix::net::UnixStream as StdUnixStream; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; - -use crate::atuin_client::{ - database::ClientSqlite, history::History, record::sqlite_store::SqliteStore, settings::Settings, -}; -use crate::atuin_daemon::DaemonEvent; -use crate::atuin_daemon::client::{ - ControlClient, DaemonClientErrorKind, HistoryClient, classify_error, -}; -use clap::Subcommand; -#[cfg(unix)] -use daemonize::Daemonize; -use eyre::{Result, WrapErr, bail, eyre}; -use fs4::fs_std::FileExt; use tokio::time::sleep; +use turtle_daemon::{ + DaemonEvent, + aclient::{ + database::ClientSqlite, history::History, record::sqlite_store::SqliteStore, + settings::Settings, + }, + client::{ControlClient, DaemonClientErrorKind, HistoryClient, classify_error}, +}; -#[derive(clap::Args, Debug)] -pub(crate) struct Cmd { - #[command(subcommand)] - subcmd: SubCmd, -} - -#[derive(Subcommand, Debug)] +#[derive(Parser, Debug)] #[command(infer_subcommands = true)] -pub(crate) enum SubCmd { +pub(crate) enum Cmd { /// Start the daemon server Start { #[arg(long, hide = true)] @@ -37,10 +30,6 @@ pub(crate) enum SubCmd { /// Also write daemon logs to the console (useful for debugging) #[arg(long)] show_logs: bool, - - /// Force start: kill existing daemon process and reset the socket - #[arg(long)] - force: bool, }, /// Show the daemon's current status @@ -48,45 +37,35 @@ pub(crate) enum SubCmd { /// Stop the daemon gracefully Stop, - - /// Restart the daemon (stop, then start in background) - Restart, } impl Cmd { - /// Returns `true` when the process should daemonize before creating the - /// async runtime or opening any database connections. - #[cfg(unix)] - pub(crate) fn should_daemonize(&self) -> bool { - match &self.subcmd { - SubCmd::Start { daemonize, .. } => *daemonize, - _ => false, - } - } - - /// Returns `true` when logs should also be written to the console. - pub(crate) fn show_logs(&self) -> bool { - match &self.subcmd { - SubCmd::Start { show_logs, .. } => *show_logs, - _ => false, - } - } - pub(crate) async fn run( self, settings: Settings, store: SqliteStore, history_db: ClientSqlite, ) -> Result<()> { - match self.subcmd { - SubCmd::Start { force, .. } => run(settings, store, history_db, force).await, - SubCmd::Status => status_cmd(&settings).await, - SubCmd::Stop => stop_cmd(&settings).await, - SubCmd::Restart => restart_cmd(&settings).await, + match self { + Cmd::Start { .. } => run(settings, store, history_db).await, + Cmd::Status => status_cmd(&settings).await, + Cmd::Stop => stop_cmd(&settings).await, } } } +#[tokio::main] +async fn main() -> Result<()> { + let settings = Settings::new().wrap_err("could not load client settings")?; + 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 sqlite_store = SqliteStore::new(record_store_path, settings.local_timeout).await?; + + Cmd::parse().run(settings, sqlite_store, db).await +} + const DAEMON_VERSION: &str = env!("CARGO_PKG_VERSION"); const DAEMON_PROTOCOL_VERSION: u32 = 1; const STARTUP_POLL: Duration = Duration::from_millis(40); @@ -221,87 +200,10 @@ async fn request_shutdown(settings: &Settings) { } } -fn spawn_daemon_process() -> Result<()> { - let exe = std::env::current_exe().wrap_err("could not locate atuin executable")?; - - let mut cmd = Command::new(exe); - cmd.arg("daemon") - .arg("start") - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - - #[cfg(unix)] - cmd.arg("--daemonize"); - - cmd.spawn().wrap_err("failed to spawn daemon process")?; - - Ok(()) -} - fn startup_timeout(settings: &Settings) -> Duration { Duration::from_secs_f64(settings.local_timeout.max(0.5) + 2.0) } -#[cfg(unix)] -fn remove_stale_socket_if_present(settings: &Settings) -> Result<()> { - if settings.daemon.systemd_socket { - return Ok(()); - } - - let socket_path = Path::new(&settings.daemon.socket_path); - if !socket_path.exists() { - return Ok(()); - } - - match StdUnixStream::connect(socket_path) { - Ok(stream) => { - drop(stream); - Ok(()) - } - Err(err) if err.kind() == ErrorKind::ConnectionRefused => { - fs::remove_file(socket_path).wrap_err_with(|| { - format!( - "failed to remove stale daemon socket {}", - socket_path.display() - ) - })?; - Ok(()) - } - Err(err) if err.kind() == ErrorKind::NotFound => Ok(()), - Err(_) => Ok(()), - } -} - -async fn wait_until_ready(settings: &Settings, timeout: Duration) -> Result<HistoryClient> { - let start = Instant::now(); - let mut last_error = eyre!("daemon did not become ready"); - - loop { - match probe(settings).await { - Probe::Ready(client) => return Ok(client), - Probe::NeedsRestart(reason) => { - last_error = eyre!(reason); - } - Probe::Unreachable(err) => { - if is_legacy_daemon_error(&err) { - return Err(err.wrap_err(LEGACY_DAEMON_RESTART_MESSAGE)); - } - last_error = err; - } - } - - if start.elapsed() >= timeout { - return Err(last_error.wrap_err(format!( - "timed out waiting for daemon startup after {}ms", - timeout.as_millis() - ))); - } - - sleep(STARTUP_POLL).await; - } -} - pub(crate) async fn start_history(settings: &Settings, history: History) -> Result<String> { match async { connect_client(settings) @@ -429,123 +331,15 @@ async fn stop_cmd(settings: &Settings) -> Result<()> { } } -async fn restart_cmd(settings: &Settings) -> Result<()> { - // Stop if running - match probe(settings).await { - Probe::Ready(_) | Probe::NeedsRestart(_) => { - request_shutdown(settings).await; - println!("Stopping daemon..."); - - let pidfile_path = PathBuf::from(&settings.daemon.pidfile_path); - let timeout = Duration::from_secs(5); - wait_for_pidfile_available(&pidfile_path, timeout) - .await - .wrap_err("Timed out waiting for old daemon to stop")?; - } - Probe::Unreachable(_) => { - println!("No daemon running"); - } - } - - #[cfg(unix)] - remove_stale_socket_if_present(settings)?; - - spawn_daemon_process()?; - println!("Starting daemon..."); - - let timeout = startup_timeout(settings); - let status = wait_until_ready(settings, timeout).await?.status().await?; - - println!("Daemon restarted"); - println!(" PID: {}", status.pid); - println!(" Version: {}", status.version); - - Ok(()) -} - -/// Daemonize the current process. Must be called before creating the tokio -/// runtime or opening database connections, since `fork()` inside an async -/// runtime corrupts its internal state. -#[cfg(unix)] -pub(crate) fn daemonize_current_process() -> Result<()> { - let cwd = - std::env::current_dir().wrap_err("could not determine current directory for daemon")?; - - Daemonize::new() - .working_directory(cwd) - .start() - .wrap_err("failed to daemonize process")?; - - Ok(()) -} - -async fn run( - settings: Settings, - store: SqliteStore, - history_db: ClientSqlite, - force: bool, -) -> Result<()> { - if force { - force_cleanup(&settings); - } - +async fn run(settings: Settings, store: SqliteStore, history_db: ClientSqlite) -> Result<()> { let pidfile_path = PathBuf::from(&settings.daemon.pidfile_path); let _pidfile_guard = PidfileGuard::acquire(&pidfile_path)?; - crate::atuin_daemon::boot(settings, store, history_db).await?; + turtle_daemon::boot(settings, store, history_db).await?; Ok(()) } -/// Force cleanup: kill existing daemon process and remove socket. -fn force_cleanup(settings: &Settings) { - let pidfile_path = Path::new(&settings.daemon.pidfile_path); - - // Read and kill the existing process if pidfile exists - if pidfile_path.exists() { - if let Ok(contents) = fs::read_to_string(pidfile_path) - && let Some(pid_str) = contents.lines().next() - && let Ok(pid) = pid_str.parse::<u32>() - { - kill_process(pid); - // Give it a moment to release resources - std::thread::sleep(Duration::from_millis(100)); - } - - // Remove the pidfile - if let Err(e) = fs::remove_file(pidfile_path) - && e.kind() != ErrorKind::NotFound - { - tracing::warn!("failed to remove pidfile: {e}"); - } - } - - // Remove the socket file - #[cfg(unix)] - { - let socket_path = Path::new(&settings.daemon.socket_path); - if socket_path.exists() - && let Err(e) = fs::remove_file(socket_path) - && e.kind() != ErrorKind::NotFound - { - tracing::warn!("failed to remove socket: {e}"); - } - } -} - -/// Kill a process by PID. -#[cfg(unix)] -fn kill_process(pid: u32) { - // Use kill command to send SIGTERM for graceful shutdown - drop( - Command::new("kill") - .args(["-TERM", &pid.to_string()]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(), - ); -} - #[cfg(test)] mod tests { use super::{ |
