From 2799620afa3a7fcfc791651bd865e6526966d19b Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Mon, 6 Apr 2026 19:13:45 -0700 Subject: [PATCH 1/2] fix: ensure daemon is running Instead of only ensuring the daemon is running as part of writing history, route all calls through a function that ensures it's running if an error occurs. This fixes the case where a system boots, no commands run, and the user tries to search history. --- crates/atuin/src/command/client/daemon.rs | 64 +++++++++++++-- crates/atuin/src/command/client/history.rs | 6 +- .../command/client/search/engines/daemon.rs | 80 ++++++++++++++----- .../atuin/src/command/client/store/rebuild.rs | 4 +- 4 files changed, 123 insertions(+), 31 deletions(-) diff --git a/crates/atuin/src/command/client/daemon.rs b/crates/atuin/src/command/client/daemon.rs index 645475055a5..5dcd11a5767 100644 --- a/crates/atuin/src/command/client/daemon.rs +++ b/crates/atuin/src/command/client/daemon.rs @@ -9,7 +9,8 @@ use std::time::{Duration, Instant}; use atuin_client::{ database::Sqlite, history::History, record::sqlite_store::SqliteStore, settings::Settings, }; -use atuin_daemon::client::{DaemonClientErrorKind, HistoryClient, classify_error}; +use atuin_daemon::DaemonEvent; +use atuin_daemon::client::{ControlClient, DaemonClientErrorKind, HistoryClient, classify_error}; use clap::Subcommand; #[cfg(unix)] use daemonize::Daemonize; @@ -343,7 +344,14 @@ fn ensure_autostart_supported(settings: &Settings) -> Result<()> { Ok(()) } -async fn restart_daemon(settings: &Settings) -> Result { +/// Ensure the daemon is running, starting it if necessary. +/// +/// If the daemon is already running and up-to-date, this is a no-op. +/// If it is not running or needs a restart, this will spawn a new daemon +/// process and wait for it to become ready. +/// +/// Returns an error if the daemon could not be started. +pub async fn ensure_daemon_running(settings: &Settings) -> Result<()> { ensure_autostart_supported(settings)?; let timeout = startup_timeout(settings); @@ -352,9 +360,9 @@ async fn restart_daemon(settings: &Settings) -> Result { let startup_lock = wait_for_lock(&startup_lock_path, timeout).await?; match probe(settings).await { - Probe::Ready(client) => { + Probe::Ready(_) => { drop(startup_lock); - return Ok(client); + return Ok(()); } Probe::NeedsRestart(_) => { request_shutdown(settings).await; @@ -373,10 +381,15 @@ async fn restart_daemon(settings: &Settings) -> Result { remove_stale_socket_if_present(settings)?; spawn_daemon_process()?; - let client = wait_until_ready(settings, timeout).await?; + let _ = wait_until_ready(settings, timeout).await?; drop(startup_lock); - Ok(client) + Ok(()) +} + +async fn restart_daemon(settings: &Settings) -> Result { + ensure_daemon_running(settings).await?; + connect_client(settings).await } fn ensure_reply_compatible(settings: &Settings, version: &str, protocol: u32) -> Result<()> { @@ -465,6 +478,45 @@ pub async fn end_history(settings: &Settings, id: String, duration: u64, exit: i Ok(()) } +/// Emit a daemon event, auto-starting the daemon if it is not running. +/// +/// If the daemon is not reachable and `daemon.autostart` is enabled, this +/// will start the daemon and retry the event. If the daemon cannot be +/// started or the retry fails, a warning is printed to stderr. +pub async fn emit_event(settings: &Settings, event: DaemonEvent) { + // Try to connect and send + match ControlClient::from_settings(settings).await { + Ok(mut client) => { + if let Err(e) = client.send_event(event).await { + tracing::debug!(?e, "failed to send event to daemon"); + } + return; + } + Err(e) if !settings.daemon.autostart || !should_retry_after_error(&e) => { + tracing::debug!(?e, "daemon not available, skipping event emission"); + return; + } + Err(_) => {} + } + + // Auto-start the daemon and retry + if let Err(e) = ensure_daemon_running(settings).await { + eprintln!("Could not start daemon: {e}"); + return; + } + + match ControlClient::from_settings(settings).await { + Ok(mut client) => { + if let Err(e) = client.send_event(event).await { + eprintln!("Daemon started but failed to send event: {e}"); + } + } + Err(e) => { + eprintln!("Daemon started but failed to connect: {e}"); + } + } +} + async fn status_cmd(settings: &Settings) -> Result<()> { match probe(settings).await { Probe::Ready(mut client) => { diff --git a/crates/atuin/src/command/client/history.rs b/crates/atuin/src/command/client/history.rs index fe9a7e32521..66ef4af13ce 100644 --- a/crates/atuin/src/command/client/history.rs +++ b/crates/atuin/src/command/client/history.rs @@ -11,7 +11,7 @@ use eyre::{Context, Result}; use runtime_format::{FormatKey, FormatKeyError, ParseSegment, ParsedFmt}; #[cfg(feature = "daemon")] -use atuin_daemon::emit_event; +use super::daemon as daemon_cmd; use atuin_client::{ database::{Database, Sqlite, current_context}, @@ -629,7 +629,7 @@ impl Cmd { } #[cfg(feature = "daemon")] - let _ = emit_event(atuin_daemon::DaemonEvent::HistoryPruned).await; + daemon_cmd::emit_event(settings, atuin_daemon::DaemonEvent::HistoryPruned).await; } Ok(()) } @@ -690,7 +690,7 @@ impl Cmd { } #[cfg(feature = "daemon")] - let _ = emit_event(atuin_daemon::DaemonEvent::HistoryDeleted { ids }).await; + daemon_cmd::emit_event(settings, atuin_daemon::DaemonEvent::HistoryDeleted { ids }).await; } Ok(()) } diff --git a/crates/atuin/src/command/client/search/engines/daemon.rs b/crates/atuin/src/command/client/search/engines/daemon.rs index c5de39abbb0..50471898791 100644 --- a/crates/atuin/src/command/client/search/engines/daemon.rs +++ b/crates/atuin/src/command/client/search/engines/daemon.rs @@ -4,7 +4,7 @@ use atuin_client::{ history::History, settings::{SearchMode, Settings}, }; -use atuin_daemon::client::SearchClient; +use atuin_daemon::client::{DaemonClientErrorKind, SearchClient, classify_error}; use atuin_nucleo_matcher::{ Config, Matcher, Utf32Str, pattern::{CaseMatching, Normalization, Pattern}, @@ -14,10 +14,12 @@ use tracing::{Level, debug, instrument, span}; use uuid::Uuid; use super::{SearchEngine, SearchState}; +use crate::command::client::daemon; pub struct Search { client: Option, query_id: u64, + settings: Settings, #[cfg(unix)] socket_path: String, #[cfg(not(unix))] @@ -29,6 +31,7 @@ impl Search { Search { client: None, query_id: 0, + settings: settings.clone(), #[cfg(unix)] socket_path: settings.daemon.socket_path.clone(), #[cfg(not(unix))] @@ -39,17 +42,31 @@ impl Search { #[instrument(skip_all, level = Level::TRACE, name = "get_daemon_client")] async fn get_client(&mut self) -> Result<&mut SearchClient> { if self.client.is_none() { - #[cfg(unix)] - let client = SearchClient::new(self.socket_path.clone()).await?; - - #[cfg(not(unix))] - let client = SearchClient::new(self.tcp_port).await?; - - self.client = Some(client); + self.connect().await?; } Ok(self.client.as_mut().unwrap()) } + async fn connect(&mut self) -> Result<()> { + #[cfg(unix)] + let client = SearchClient::new(self.socket_path.clone()).await?; + + #[cfg(not(unix))] + let client = SearchClient::new(self.tcp_port).await?; + + self.client = Some(client); + Ok(()) + } + + fn should_retry(err: &eyre::Report) -> bool { + matches!( + classify_error(err), + DaemonClientErrorKind::Connect + | DaemonClientErrorKind::Unavailable + | DaemonClientErrorKind::Unimplemented + ) + } + fn next_query_id(&mut self) -> u64 { self.query_id += 1; self.query_id @@ -115,17 +132,41 @@ impl SearchEngine for Search { let span = span!(Level::TRACE, "daemon_search.req_resp", query = %query, query_id = query_id); - let client = self.get_client().await?; - - let _span = span.enter(); - let mut stream = client - .search( - query.clone(), - query_id, - state.filter_mode, - Some(state.context.clone()), - ) - .await?; + // Try to connect and search; if it fails with a retriable error, + // auto-start the daemon and retry once. + let first_attempt = async { + let client = self.get_client().await?; + client + .search( + query.clone(), + query_id, + state.filter_mode, + Some(state.context.clone()), + ) + .await + } + .await; + + let mut stream = match first_attempt { + Ok(stream) => stream, + Err(err) if self.settings.daemon.autostart && Self::should_retry(&err) => { + debug!("daemon not available, attempting auto-start"); + self.client = None; + + daemon::ensure_daemon_running(&self.settings).await?; + + let client = self.get_client().await?; + client + .search( + query.clone(), + query_id, + state.filter_mode, + Some(state.context.clone()), + ) + .await? + } + Err(err) => return Err(err), + }; let mut ids = Vec::with_capacity(200); span!(Level::TRACE, "daemon_search.resp") @@ -155,7 +196,6 @@ impl SearchEngine for Search { } }) .await; - drop(_span); drop(span); if ids.is_empty() { diff --git a/crates/atuin/src/command/client/store/rebuild.rs b/crates/atuin/src/command/client/store/rebuild.rs index a98f8142462..8b334ced867 100644 --- a/crates/atuin/src/command/client/store/rebuild.rs +++ b/crates/atuin/src/command/client/store/rebuild.rs @@ -4,7 +4,7 @@ use clap::Args; use eyre::{Result, bail}; #[cfg(feature = "daemon")] -use atuin_daemon::emit_event; +use crate::command::client::daemon as daemon_cmd; use atuin_client::{ database::Database, encryption, history::store::HistoryStore, @@ -61,7 +61,7 @@ impl Rebuild { history_store.build(database).await?; #[cfg(feature = "daemon")] - let _ = emit_event(atuin_daemon::DaemonEvent::HistoryRebuilt).await; + daemon_cmd::emit_event(settings, atuin_daemon::DaemonEvent::HistoryRebuilt).await; Ok(()) } From 33c7a43bc38eee990a7581cfdb96027eee57d22e Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Mon, 6 Apr 2026 19:19:34 -0700 Subject: [PATCH 2/2] format --- crates/atuin/src/command/client/history.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/atuin/src/command/client/history.rs b/crates/atuin/src/command/client/history.rs index 66ef4af13ce..805b54abeaa 100644 --- a/crates/atuin/src/command/client/history.rs +++ b/crates/atuin/src/command/client/history.rs @@ -690,7 +690,8 @@ impl Cmd { } #[cfg(feature = "daemon")] - daemon_cmd::emit_event(settings, atuin_daemon::DaemonEvent::HistoryDeleted { ids }).await; + daemon_cmd::emit_event(settings, atuin_daemon::DaemonEvent::HistoryDeleted { ids }) + .await; } Ok(()) }