diff --git a/crates/ahfail-ui/src/update.rs b/crates/ahfail-ui/src/update.rs index 93310b8..41ae421 100644 --- a/crates/ahfail-ui/src/update.rs +++ b/crates/ahfail-ui/src/update.rs @@ -1,5 +1,6 @@ use std::path::PathBuf; use std::process::Command; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::SystemTime; use std::fs; @@ -9,6 +10,9 @@ const UPDATE_NOTIFY_MSG: &str = "Update available — visit https://gitea.weircon.dk/agw/gtk-ahfail/releases"; const CHECK_INTERVAL_SECS: u64 = 86_400; +// Guards against multiple windows spawning duplicate checks on the same failure. +static CHECK_IN_FLIGHT: AtomicBool = AtomicBool::new(false); + pub fn is_newer(latest: &str, current: &str) -> bool { let parse = |s: &str| -> Option<(u32, u32, u32)> { let s = s.trim_start_matches('v'); @@ -25,7 +29,10 @@ pub fn is_newer(latest: &str, current: &str) -> bool { fn cache_file() -> PathBuf { let dir = std::env::var("HOME") .map(|h| PathBuf::from(h).join(".cache/ahfail")) - .unwrap_or_else(|_| PathBuf::from("/tmp/ahfail-cache")); + .unwrap_or_else(|_| { + let uid = unsafe { libc::getuid() }; + PathBuf::from(format!("/tmp/ahfail-cache-{uid}")) + }); let _ = fs::create_dir_all(&dir); dir.join("last_update_check") } @@ -51,7 +58,7 @@ fn send_notification() { #[cfg(target_os = "linux")] let _ = Command::new("notify-send") .args(["ahfail", UPDATE_NOTIFY_MSG]) - .spawn(); + .status(); #[cfg(target_os = "macos")] let _ = Command::new("osascript") @@ -59,7 +66,7 @@ fn send_notification() { "display notification \"{}\" with title \"ahfail\"", UPDATE_NOTIFY_MSG )]) - .spawn(); + .status(); } /// Run in a background thread. Checks Gitea for a newer release and sends a @@ -67,10 +74,28 @@ fn send_notification() { /// Fails silently on any error. pub fn check_for_update(current_version: &str) { if rate_limited() { return; } + // Prevent concurrent threads (e.g. multi-monitor) from all firing at once. + if CHECK_IN_FLIGHT.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() { + return; + } + + let agent = ureq::AgentBuilder::new() + .timeout_connect(std::time::Duration::from_secs(5)) + .timeout_read(std::time::Duration::from_secs(10)) + .build(); + + let Ok(resp) = agent.get(GITEA_API).call() else { + CHECK_IN_FLIGHT.store(false, Ordering::SeqCst); + return; + }; touch_cache(); - let Ok(resp) = ureq::get(GITEA_API).call() else { return }; - let Ok(body) = resp.into_string() else { return }; + let Ok(body) = resp.into_string() else { + CHECK_IN_FLIGHT.store(false, Ordering::SeqCst); + return; + }; + + CHECK_IN_FLIGHT.store(false, Ordering::SeqCst); if let Some(tag) = extract_tag_name(&body) { if is_newer(&tag, current_version) { @@ -80,8 +105,11 @@ pub fn check_for_update(current_version: &str) { } fn extract_tag_name(json: &str) -> Option { - let key = "\"tag_name\":\""; - let start = json.find(key)? + key.len(); - let end = json[start..].find('"')? + start; - Some(json[start..end].to_string()) + // Handle both compact ("tag_name":"v1.0") and spaced ("tag_name": "v1.0") JSON. + let key = "\"tag_name\""; + let after_key = json.find(key)? + key.len(); + let rest = json[after_key..].trim_start_matches(|c: char| c == ':' || c.is_ascii_whitespace()); + let rest = rest.strip_prefix('"')?; + let end = rest.find('"')?; + Some(rest[..end].to_string()) } diff --git a/crates/ahfail-ui/tests/update_tests.rs b/crates/ahfail-ui/tests/update_tests.rs index 7d5d624..612c5a2 100644 --- a/crates/ahfail-ui/tests/update_tests.rs +++ b/crates/ahfail-ui/tests/update_tests.rs @@ -12,3 +12,9 @@ fn newer_version_detected() { fn strips_v_prefix() { assert!(is_newer("0.2.0", "0.1.0")); } + +#[test] +fn multi_digit_minor_version() { + assert!(is_newer("v0.10.0", "v0.9.0")); + assert!(!is_newer("v0.9.0", "v0.10.0")); +}