fix: update check — handle spaced JSON, move cache touch after HTTP success, add timeouts and multi-monitor guard
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
@@ -9,6 +10,9 @@ const UPDATE_NOTIFY_MSG: &str =
|
|||||||
"Update available — visit https://gitea.weircon.dk/agw/gtk-ahfail/releases";
|
"Update available — visit https://gitea.weircon.dk/agw/gtk-ahfail/releases";
|
||||||
const CHECK_INTERVAL_SECS: u64 = 86_400;
|
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 {
|
pub fn is_newer(latest: &str, current: &str) -> bool {
|
||||||
let parse = |s: &str| -> Option<(u32, u32, u32)> {
|
let parse = |s: &str| -> Option<(u32, u32, u32)> {
|
||||||
let s = s.trim_start_matches('v');
|
let s = s.trim_start_matches('v');
|
||||||
@@ -25,7 +29,10 @@ pub fn is_newer(latest: &str, current: &str) -> bool {
|
|||||||
fn cache_file() -> PathBuf {
|
fn cache_file() -> PathBuf {
|
||||||
let dir = std::env::var("HOME")
|
let dir = std::env::var("HOME")
|
||||||
.map(|h| PathBuf::from(h).join(".cache/ahfail"))
|
.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);
|
let _ = fs::create_dir_all(&dir);
|
||||||
dir.join("last_update_check")
|
dir.join("last_update_check")
|
||||||
}
|
}
|
||||||
@@ -51,7 +58,7 @@ fn send_notification() {
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
let _ = Command::new("notify-send")
|
let _ = Command::new("notify-send")
|
||||||
.args(["ahfail", UPDATE_NOTIFY_MSG])
|
.args(["ahfail", UPDATE_NOTIFY_MSG])
|
||||||
.spawn();
|
.status();
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let _ = Command::new("osascript")
|
let _ = Command::new("osascript")
|
||||||
@@ -59,7 +66,7 @@ fn send_notification() {
|
|||||||
"display notification \"{}\" with title \"ahfail\"",
|
"display notification \"{}\" with title \"ahfail\"",
|
||||||
UPDATE_NOTIFY_MSG
|
UPDATE_NOTIFY_MSG
|
||||||
)])
|
)])
|
||||||
.spawn();
|
.status();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run in a background thread. Checks Gitea for a newer release and sends a
|
/// 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.
|
/// Fails silently on any error.
|
||||||
pub fn check_for_update(current_version: &str) {
|
pub fn check_for_update(current_version: &str) {
|
||||||
if rate_limited() { return; }
|
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();
|
touch_cache();
|
||||||
|
|
||||||
let Ok(resp) = ureq::get(GITEA_API).call() else { return };
|
let Ok(body) = resp.into_string() else {
|
||||||
let Ok(body) = resp.into_string() else { return };
|
CHECK_IN_FLIGHT.store(false, Ordering::SeqCst);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
CHECK_IN_FLIGHT.store(false, Ordering::SeqCst);
|
||||||
|
|
||||||
if let Some(tag) = extract_tag_name(&body) {
|
if let Some(tag) = extract_tag_name(&body) {
|
||||||
if is_newer(&tag, current_version) {
|
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<String> {
|
fn extract_tag_name(json: &str) -> Option<String> {
|
||||||
let key = "\"tag_name\":\"";
|
// Handle both compact ("tag_name":"v1.0") and spaced ("tag_name": "v1.0") JSON.
|
||||||
let start = json.find(key)? + key.len();
|
let key = "\"tag_name\"";
|
||||||
let end = json[start..].find('"')? + start;
|
let after_key = json.find(key)? + key.len();
|
||||||
Some(json[start..end].to_string())
|
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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,3 +12,9 @@ fn newer_version_detected() {
|
|||||||
fn strips_v_prefix() {
|
fn strips_v_prefix() {
|
||||||
assert!(is_newer("0.2.0", "0.1.0"));
|
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"));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user