From 1e4fc55bdff030423a0bd1d676e540c7c1640857 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Sat, 23 May 2026 11:56:33 -0700 Subject: [PATCH] feat(update): background update check with post-command notice Spawn the GitHub release check in a background thread before the command runs, then join it (recv_timeout 6 s) after the command finishes so the notice always appears at the bottom of output. Uses mpsc::Receiver instead of JoinHandle::join to prevent blocking if the HTTP request stalls past its own 5 s timeout. Unifies the notice message to "hotdata update" for all install methods. Co-Authored-By: Claude Sonnet 4.6 --- src/main.rs | 17 ++++++++++++----- src/update.rs | 52 ++++++++++++++++++++++++++++++++------------------- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/main.rs b/src/main.rs index e0474ea..2b28a40 100644 --- a/src/main.rs +++ b/src/main.rs @@ -172,11 +172,15 @@ fn main() { skill::maybe_auto_update_after_cli_upgrade(); } - // Quiet update-available notice. Skip during `hotdata update` itself so - // we don't talk over the updater's own output. - if !matches!(&cli.command, Some(Commands::Update)) { - update::maybe_print_update_notice(); - } + // Kick off the update check in the background so it runs concurrently + // with the command. We join and print after the command finishes so the + // notice always appears at the bottom of the output. Skipped during + // `hotdata update` itself so it doesn't talk over the updater's output. + let update_handle = if !matches!(&cli.command, Some(Commands::Update)) { + update::spawn_update_check() + } else { + None + }; match cli.command { None => { @@ -1008,6 +1012,9 @@ fn main() { Commands::Update => update::run_update(), }, } + + // Print update notice after command output (joined from background thread). + update::maybe_print_update_notice(update_handle); } /// Parse a database target like `airbnb.listings` or `airbnb.public.listings` diff --git a/src/update.rs b/src/update.rs index 3aa8ba2..37613a2 100644 --- a/src/update.rs +++ b/src/update.rs @@ -120,31 +120,45 @@ fn stderr_is_tty() -> bool { std::io::stderr().is_terminal() } -/// Print a one-line notice if a newer release exists. No-op when stderr -/// isn't a TTY, when --no-input is set, or when the cache says we're up -/// to date. Best-effort: network/cache errors are swallowed silently so -/// commands never fail because of the update check. -pub fn maybe_print_update_notice() { - if !stderr_is_tty() { - return; - } - if !util::is_interactive() { - return; - } - if std::env::var_os("HOTDATA_NO_UPDATE_CHECK").is_some() { - return; +fn should_check() -> bool { + stderr_is_tty() + && util::is_interactive() + && std::env::var_os("HOTDATA_NO_UPDATE_CHECK").is_none() +} + +/// How long `maybe_print_update_notice` will wait for the background thread +/// before giving up. In practice the thread finishes well within this window +/// because `fetch_latest_version` has its own 5-second HTTP timeout and cache +/// hits resolve in microseconds. +const NOTICE_WAIT_MS: u64 = 6_000; + +/// Spawn a background thread that checks for a newer release. Returns a +/// channel receiver that `maybe_print_update_notice` can poll after the +/// command runs. No-op (returns None) when stderr isn't a TTY, `--no-input` +/// is set, or `HOTDATA_NO_UPDATE_CHECK` is set. +pub fn spawn_update_check() -> Option>> { + if !should_check() { + return None; } - let Some(latest) = cached_latest_if_newer() else { + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let _ = tx.send(cached_latest_if_newer()); + }); + Some(rx) +} + +/// Poll the receiver returned by `spawn_update_check` and print a one-line +/// notice if a newer release was found. Call this *after* the command has +/// produced its own output so the notice appears at the bottom. +pub fn maybe_print_update_notice(rx: Option>>) { + let Some(rx) = rx else { return }; + let Ok(Some(latest)) = rx.recv_timeout(Duration::from_millis(NOTICE_WAIT_MS)) else { return; }; - let how = match detect_install_method() { - InstallMethod::Homebrew => format!("Run: brew upgrade {HOMEBREW_FORMULA}"), - InstallMethod::Other => "Run: hotdata update".to_string(), - }; eprintln!( "{}", format!( - "A new version of hotdata is available (v{CURRENT_VERSION} → v{latest}). {how}" + "\nA new version of hotdata is available (v{CURRENT_VERSION} → v{latest}). Run: hotdata update" ) .yellow() );