use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
const CHECK_INTERVAL_SECS: u64 = 86400; const TIMEOUT_SECS: u64 = 3;
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
pub fn check_for_update() {
if crate::QUIET.load(std::sync::atomic::Ordering::Relaxed) {
return;
}
if std::env::var("TKT_UPDATE_CHECK").as_deref() == Ok("0") {
return;
}
if std::env::var("CI").is_ok() {
return;
}
let cache_path = match cache_file() {
Some(p) => p,
None => return,
};
if let Some(cached) = read_cache(&cache_path) {
if cached.timestamp + CHECK_INTERVAL_SECS > unix_now() {
if let Some(ref latest) = cached.latest_version {
if is_newer(latest, CURRENT_VERSION) {
print_notice(latest);
}
}
return;
}
}
match fetch_latest_version() {
Some(latest) => {
write_cache(&cache_path, &latest);
if is_newer(&latest, CURRENT_VERSION) {
print_notice(&latest);
}
}
None => {
write_cache(&cache_path, CURRENT_VERSION);
}
}
}
fn print_notice(latest: &str) {
eprintln!(
"\n (tkt {} available — run `cargo install tkt` to update)",
latest
);
}
fn fetch_latest_version() -> Option<String> {
let output = std::process::Command::new("curl")
.args([
"--silent",
"--max-time",
&TIMEOUT_SECS.to_string(),
"--proto",
"=https",
"--tlsv1.2",
"-H",
"User-Agent: tkt-update-check",
"https://crates.io/api/v1/crates/tkt",
])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let body = String::from_utf8(output.stdout).ok()?;
let marker = "\"max_stable_version\":\"";
let start = body.find(marker)? + marker.len();
let end = start + body[start..].find('"')?;
Some(body[start..end].to_string())
}
fn is_newer(latest: &str, current: &str) -> bool {
let parse = |s: &str| -> Vec<u32> { s.split('.').filter_map(|p| p.parse().ok()).collect() };
let l = parse(latest);
let c = parse(current);
l > c
}
struct CacheEntry {
timestamp: u64,
latest_version: Option<String>,
}
fn cache_file() -> Option<PathBuf> {
let dir = dirs::config_dir()?.join("tkt");
std::fs::create_dir_all(&dir).ok()?;
Some(dir.join("update-check.txt"))
}
fn read_cache(path: &PathBuf) -> Option<CacheEntry> {
let content = std::fs::read_to_string(path).ok()?;
let mut lines = content.lines();
let timestamp: u64 = lines.next()?.parse().ok()?;
let version = lines.next().map(|s| s.to_string());
Some(CacheEntry {
timestamp,
latest_version: version.filter(|v| !v.is_empty()),
})
}
fn write_cache(path: &PathBuf, version: &str) {
let content = format!("{}\n{}\n", unix_now(), version);
let _ = std::fs::write(path, content);
}
fn unix_now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_comparison() {
assert!(is_newer("0.2.0", "0.1.0"));
assert!(is_newer("1.0.0", "0.9.9"));
assert!(!is_newer("0.1.0", "0.1.0"));
assert!(!is_newer("0.1.0", "0.2.0"));
}
}