use crate::browser_version_manager::{BrowserVersionInfo, BrowserVersionManager}; use crate::profile::{BrowserProfile, ProfileManager}; use crate::settings_manager::SettingsManager; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::fs; use std::path::PathBuf; use tauri::Emitter; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UpdateNotification { pub id: String, pub browser: String, pub current_version: String, pub new_version: String, pub affected_profiles: Vec, pub is_stable_update: bool, pub timestamp: u64, } #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct AutoUpdateState { pub pending_updates: Vec, pub disabled_browsers: HashSet, // browsers disabled during update #[serde(default)] pub auto_update_downloads: HashSet, // track auto-update downloads for toast suppression pub last_check_timestamp: u64, } pub struct AutoUpdater { browser_version_manager: &'static BrowserVersionManager, settings_manager: &'static SettingsManager, profile_manager: &'static ProfileManager, } impl AutoUpdater { fn new() -> Self { Self { browser_version_manager: BrowserVersionManager::instance(), settings_manager: SettingsManager::instance(), profile_manager: ProfileManager::instance(), } } pub fn instance() -> &'static AutoUpdater { &AUTO_UPDATER } /// Check for updates for all profiles pub async fn check_for_updates( &self, ) -> Result, Box> { let mut notifications = Vec::new(); let mut browser_versions: HashMap> = HashMap::new(); // Group profiles by browser let profiles = self .profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; let mut browser_profiles: HashMap> = HashMap::new(); for profile in profiles { // Only check supported browsers if !self .browser_version_manager .is_browser_supported(&profile.browser) .unwrap_or(false) { continue; } browser_profiles .entry(profile.browser.clone()) .or_default() .push(profile); } for (browser, profiles) in browser_profiles { // Get cached versions first, then try to fetch if needed let versions = if let Some(cached) = self .browser_version_manager .get_cached_browser_versions_detailed(&browser) { cached } else if self.browser_version_manager.should_update_cache(&browser) { // Try to fetch fresh versions match self .browser_version_manager .fetch_browser_versions_detailed(&browser, false) .await { Ok(versions) => versions, Err(_) => continue, // Skip this browser if fetch fails } } else { continue; // No cached versions and cache doesn't need update }; browser_versions.insert(browser.clone(), versions.clone()); // Check each profile for updates for profile in profiles { if let Some(update) = self.check_profile_update(&profile, &versions)? { // Apply chromium threshold logic if browser == "chromium" { // For chromium, only show notifications if there are 400+ new versions let current_version = &profile.version.parse::().unwrap(); let new_version = &update.new_version.parse::().unwrap(); let result = new_version - current_version; log::info!( "Current version: {current_version}, New version: {new_version}, Result: {result}" ); if result > 400 { notifications.push(update); } else { log::info!( "Skipping chromium update notification: only {result} new versions (need 400+)" ); } } else { notifications.push(update); } } } } Ok(notifications) } pub async fn check_for_updates_with_progress(&self, app_handle: &tauri::AppHandle) { log::info!("Starting auto-update check with progress..."); // Check for browser updates and trigger auto-downloads match self.check_for_updates().await { Ok(update_notifications) => { if !update_notifications.is_empty() { log::info!( "Found {} browser updates to auto-download", update_notifications.len() ); // Trigger automatic downloads for each update for notification in update_notifications { log::info!( "Auto-downloading {} version {}", notification.browser, notification.new_version ); // Clone app_handle for the async task let browser = notification.browser.clone(); let new_version = notification.new_version.clone(); let notification_id = notification.id.clone(); let affected_profiles = notification.affected_profiles.clone(); let app_handle_clone = app_handle.clone(); // Spawn async task to handle the download and auto-update tokio::spawn(async move { // TODO: update the logic to use the downloaded browsers registry instance instead of the static method // First, check if browser already exists match crate::downloaded_browsers_registry::is_browser_downloaded( browser.clone(), new_version.clone(), ) { true => { log::info!("Browser {browser} {new_version} already downloaded, proceeding to auto-update profiles"); // Browser already exists, go straight to profile update match AutoUpdater::instance() .complete_browser_update_with_auto_update( &app_handle_clone, &browser.clone(), &new_version.clone(), ) .await { Ok(updated_profiles) => { log::info!( "Auto-update completed for {} profiles: {:?}", updated_profiles.len(), updated_profiles ); } Err(e) => { log::error!("Failed to complete auto-update for {browser}: {e}"); } } } false => { log::info!("Downloading browser {browser} version {new_version}..."); // Emit the auto-update event to trigger frontend handling let auto_update_event = serde_json::json!({ "browser": browser, "new_version": new_version, "notification_id": notification_id, "affected_profiles": affected_profiles }); if let Err(e) = app_handle_clone.emit("browser-auto-update-available", &auto_update_event) { log::error!("Failed to emit auto-update event for {browser}: {e}"); } else { log::info!("Emitted auto-update event for {browser}"); } } } }); } } else { log::info!("No browser updates needed"); } } Err(e) => { log::error!("Failed to check for browser updates: {e}"); } } } /// Check if a specific profile has an available update fn check_profile_update( &self, profile: &BrowserProfile, available_versions: &[BrowserVersionInfo], ) -> Result, Box> { let current_version = &profile.version; let is_current_nightly = crate::api_client::is_browser_version_nightly(&profile.browser, current_version, None); // Find the best available update let best_update = available_versions .iter() .filter(|v| { // Only consider versions newer than current self.is_version_newer(&v.version, current_version) && crate::api_client::is_browser_version_nightly(&profile.browser, &v.version, None) == is_current_nightly }) .max_by(|a, b| self.compare_versions(&a.version, &b.version)); if let Some(update_version) = best_update { let notification = UpdateNotification { id: format!( "{}_{}_to_{}", profile.browser, current_version, update_version.version ), browser: profile.browser.clone(), current_version: current_version.clone(), new_version: update_version.version.clone(), affected_profiles: vec![profile.name.clone()], is_stable_update: !update_version.is_prerelease, timestamp: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(), }; Ok(Some(notification)) } else { Ok(None) } } /// Group update notifications by browser and version pub fn group_update_notifications( &self, notifications: Vec, ) -> Vec { let mut grouped: HashMap = HashMap::new(); for notification in notifications { let key = format!("{}_{}", notification.browser, notification.new_version); if let Some(existing) = grouped.get_mut(&key) { // Merge affected profiles existing .affected_profiles .extend(notification.affected_profiles); existing.affected_profiles.sort(); existing.affected_profiles.dedup(); } else { grouped.insert(key, notification); } } let mut result: Vec = grouped.into_values().collect(); // Sort by priority: stable updates first, then by timestamp result.sort_by(|a, b| match (a.is_stable_update, b.is_stable_update) { (true, false) => std::cmp::Ordering::Less, (false, true) => std::cmp::Ordering::Greater, _ => b.timestamp.cmp(&a.timestamp), }); result } /// Automatically update all affected profile versions after browser download pub async fn auto_update_profile_versions( &self, app_handle: &tauri::AppHandle, browser: &str, new_version: &str, ) -> Result, Box> { let profiles = self .profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; let mut updated_profiles = Vec::new(); // Find all profiles for this browser that should be updated for profile in profiles { if profile.browser == browser { // Check if profile is currently running if profile.process_id.is_some() { continue; // Skip running profiles } // Check if this is an update (newer version) if self.is_version_newer(new_version, &profile.version) { // Update the profile version match self.profile_manager.update_profile_version( app_handle, &profile.id.to_string(), new_version, ) { Ok(_) => { updated_profiles.push(profile.name); } Err(e) => { log::error!("Failed to update profile {}: {}", profile.name, e); } } } } } Ok(updated_profiles) } /// Complete browser update process with auto-update of profile versions pub async fn complete_browser_update_with_auto_update( &self, app_handle: &tauri::AppHandle, browser: &str, new_version: &str, ) -> Result, Box> { // Auto-update profile versions first let updated_profiles = self .auto_update_profile_versions(app_handle, browser, new_version) .await?; // Remove browser from disabled list and clean up auto-update tracking let mut state = self.load_auto_update_state()?; state.disabled_browsers.remove(browser); let download_key = format!("{browser}-{new_version}"); state.auto_update_downloads.remove(&download_key); self.save_auto_update_state(&state)?; Ok(updated_profiles) } /// Check if browser is disabled due to ongoing update pub fn is_browser_disabled( &self, browser: &str, ) -> Result> { let state = self.load_auto_update_state()?; Ok(state.disabled_browsers.contains(browser)) } /// Dismiss update notification pub fn dismiss_update_notification( &self, notification_id: &str, ) -> Result<(), Box> { let mut state = self.load_auto_update_state()?; state.pending_updates.retain(|n| n.id != notification_id); self.save_auto_update_state(&state)?; Ok(()) } fn is_version_newer(&self, version1: &str, version2: &str) -> bool { crate::api_client::is_version_newer(version1, version2) } fn compare_versions(&self, version1: &str, version2: &str) -> std::cmp::Ordering { crate::api_client::compare_versions(version1, version2) } fn get_auto_update_state_file(&self) -> PathBuf { self .settings_manager .get_settings_dir() .join("auto_update_state.json") } pub fn load_auto_update_state( &self, ) -> Result> { let state_file = self.get_auto_update_state_file(); if !state_file.exists() { return Ok(AutoUpdateState::default()); } let content = fs::read_to_string(state_file)?; let state: AutoUpdateState = serde_json::from_str(&content)?; Ok(state) } pub fn save_auto_update_state( &self, state: &AutoUpdateState, ) -> Result<(), Box> { let settings_dir = self.settings_manager.get_settings_dir(); std::fs::create_dir_all(&settings_dir)?; let state_file = self.get_auto_update_state_file(); let json = serde_json::to_string_pretty(state)?; fs::write(state_file, json)?; Ok(()) } /// Get pending update versions for a specific browser /// Returns a set of (browser, version) pairs that have pending updates pub fn get_pending_update_versions( &self, ) -> Result, Box> { let state = self.load_auto_update_state()?; let mut pending_versions = std::collections::HashSet::new(); for update in &state.pending_updates { pending_versions.insert((update.browser.clone(), update.new_version.clone())); } Ok(pending_versions) } /// Get pending update for a specific browser version if it exists pub fn get_pending_update( &self, browser: &str, current_version: &str, ) -> Result, Box> { let state = self.load_auto_update_state()?; for update in &state.pending_updates { if update.browser == browser && update.current_version == current_version { return Ok(Some(update.clone())); } } Ok(None) } } // Tauri commands #[tauri::command] pub async fn check_for_browser_updates() -> Result, String> { let updater = AutoUpdater::instance(); let notifications = updater .check_for_updates() .await .map_err(|e| format!("Failed to check for updates: {e}"))?; let grouped = updater.group_update_notifications(notifications); Ok(grouped) } #[tauri::command] pub async fn dismiss_update_notification(notification_id: String) -> Result<(), String> { let updater = AutoUpdater::instance(); updater .dismiss_update_notification(¬ification_id) .map_err(|e| format!("Failed to dismiss notification: {e}")) } #[tauri::command] pub async fn complete_browser_update_with_auto_update( app_handle: tauri::AppHandle, browser: String, new_version: String, ) -> Result, String> { let updater = AutoUpdater::instance(); updater .complete_browser_update_with_auto_update(&app_handle, &browser, &new_version) .await .map_err(|e| format!("Failed to complete browser update: {e}")) } #[tauri::command] pub async fn check_for_updates_with_progress(app_handle: tauri::AppHandle) { let updater = AutoUpdater::instance(); updater.check_for_updates_with_progress(&app_handle).await; } #[cfg(test)] mod tests { use super::*; fn create_test_profile(name: &str, browser: &str, version: &str) -> BrowserProfile { BrowserProfile { id: uuid::Uuid::new_v4(), name: name.to_string(), browser: browser.to_string(), version: version.to_string(), process_id: None, proxy_id: None, last_launch: None, release_type: "stable".to_string(), camoufox_config: None, group_id: None, tags: Vec::new(), note: None, sync_enabled: false, last_sync: None, } } fn create_test_version_info(version: &str, is_prerelease: bool) -> BrowserVersionInfo { BrowserVersionInfo { version: version.to_string(), is_prerelease, date: "2024-01-01".to_string(), } } #[test] fn test_compare_versions() { let updater = AutoUpdater::instance(); assert_eq!( updater.compare_versions("1.0.0", "1.0.0"), std::cmp::Ordering::Equal ); assert_eq!( updater.compare_versions("1.0.1", "1.0.0"), std::cmp::Ordering::Greater ); assert_eq!( updater.compare_versions("1.0.0", "1.0.1"), std::cmp::Ordering::Less ); assert_eq!( updater.compare_versions("2.0.0", "1.9.9"), std::cmp::Ordering::Greater ); assert_eq!( updater.compare_versions("1.10.0", "1.9.0"), std::cmp::Ordering::Greater ); } #[test] fn test_is_version_newer() { let updater = AutoUpdater::instance(); assert!(updater.is_version_newer("1.0.1", "1.0.0")); assert!(updater.is_version_newer("2.0.0", "1.9.9")); assert!(!updater.is_version_newer("1.0.0", "1.0.1")); assert!(!updater.is_version_newer("1.0.0", "1.0.0")); } #[test] fn test_camoufox_beta_version_comparison() { let updater = AutoUpdater::instance(); // Test the exact user-reported scenario: 135.0.1beta24 vs 135.0beta22 assert!( updater.is_version_newer("135.0.1beta24", "135.0beta22"), "135.0.1beta24 should be newer than 135.0beta22" ); assert_eq!( updater.compare_versions("135.0.1beta24", "135.0beta22"), std::cmp::Ordering::Greater, "135.0.1beta24 should compare as greater than 135.0beta22" ); // Test other camoufox beta version combinations assert!( updater.is_version_newer("135.0.5beta24", "135.0.5beta22"), "135.0.5beta24 should be newer than 135.0.5beta22" ); assert!( updater.is_version_newer("135.0.1beta1", "135.0beta1"), "135.0.1beta1 should be newer than 135.0beta1 due to patch version" ); // Test that older versions are not considered newer assert!( !updater.is_version_newer("135.0beta22", "135.0.1beta24"), "135.0beta22 should NOT be newer than 135.0.1beta24" ); } #[test] fn test_beta_version_ordering_comprehensive() { let updater = AutoUpdater::instance(); // Test various beta version patterns that could appear in camoufox let test_cases = vec![ ("135.0.1beta24", "135.0beta22", true), // User reported case ("135.0.5beta24", "135.0.5beta22", true), // Same patch, different beta ("135.1beta1", "135.0beta99", true), // Higher minor beats beta number ("136.0beta1", "135.9.9beta99", true), // Higher major beats everything ("135.0.1beta1", "135.0beta1", true), // Patch version matters ("135.0beta22", "135.0.1beta24", false), // Reverse of user case ]; for (newer, older, should_be_newer) in test_cases { let result = updater.is_version_newer(newer, older); assert_eq!( result, should_be_newer, "Expected {} {} {} but got {}", newer, if should_be_newer { ">" } else { "<=" }, older, if result { "true" } else { "false" } ); } } #[test] fn test_check_profile_update_stable_to_stable() { let updater = AutoUpdater::instance(); let profile = create_test_profile("test", "firefox", "1.0.0"); let versions = vec![ create_test_version_info("1.0.1", false), // stable, newer create_test_version_info("1.1.0-alpha", true), // alpha, should be ignored create_test_version_info("0.9.0", false), // stable, older ]; let result = updater.check_profile_update(&profile, &versions).unwrap(); assert!(result.is_some()); let update = result.unwrap(); assert_eq!(update.new_version, "1.0.1"); assert!(update.is_stable_update); } #[test] fn test_check_profile_update_alpha_to_alpha() { let updater = AutoUpdater::instance(); let profile = create_test_profile("test", "firefox", "1.0.0-alpha"); let versions = vec![ create_test_version_info("1.0.1", false), // stable, should be included create_test_version_info("1.1.0-alpha", true), // alpha, newer create_test_version_info("0.9.0-alpha", true), // alpha, older ]; let result = updater.check_profile_update(&profile, &versions).unwrap(); assert!(result.is_some()); let update = result.unwrap(); // Should pick the newest version (alpha user can upgrade to stable or newer alpha) assert_eq!(update.new_version, "1.1.0-alpha"); assert!(!update.is_stable_update); } #[test] fn test_check_profile_update_no_update_available() { let updater = AutoUpdater::instance(); let profile = create_test_profile("test", "firefox", "1.0.0"); let versions = vec![ create_test_version_info("0.9.0", false), // older create_test_version_info("1.0.0", false), // same version ]; let result = updater.check_profile_update(&profile, &versions).unwrap(); assert!(result.is_none()); } #[test] fn test_group_update_notifications() { let updater = AutoUpdater::instance(); let notifications = vec![ UpdateNotification { id: "firefox_1.0.0_to_1.1.0_profile1".to_string(), browser: "firefox".to_string(), current_version: "1.0.0".to_string(), new_version: "1.1.0".to_string(), affected_profiles: vec!["profile1".to_string()], is_stable_update: true, timestamp: 1000, }, UpdateNotification { id: "firefox_1.0.0_to_1.1.0_profile2".to_string(), browser: "firefox".to_string(), current_version: "1.0.0".to_string(), new_version: "1.1.0".to_string(), affected_profiles: vec!["profile2".to_string()], is_stable_update: true, timestamp: 1001, }, UpdateNotification { id: "chrome_1.0.0_to_1.1.0-alpha".to_string(), browser: "chrome".to_string(), current_version: "1.0.0".to_string(), new_version: "1.1.0-alpha".to_string(), affected_profiles: vec!["profile3".to_string()], is_stable_update: false, timestamp: 1002, }, ]; let grouped = updater.group_update_notifications(notifications); assert_eq!(grouped.len(), 2); // Find the Firefox notification let firefox_notification = grouped.iter().find(|n| n.browser == "firefox").unwrap(); assert_eq!(firefox_notification.affected_profiles.len(), 2); assert!(firefox_notification .affected_profiles .contains(&"profile1".to_string())); assert!(firefox_notification .affected_profiles .contains(&"profile2".to_string())); // Stable updates should come first assert!(grouped[0].is_stable_update); } #[test] fn test_auto_update_state_persistence() { use std::sync::Once; use tempfile::TempDir; static INIT: Once = Once::new(); INIT.call_once(|| { // Initialize any required static data }); // Create a temporary directory for testing let temp_dir = TempDir::new().unwrap(); // Create a mock settings manager that uses the temp directory struct TestSettingsManager { settings_dir: std::path::PathBuf, } impl TestSettingsManager { fn new(settings_dir: std::path::PathBuf) -> Self { Self { settings_dir } } fn get_settings_dir(&self) -> std::path::PathBuf { self.settings_dir.clone() } } let test_settings_manager = TestSettingsManager::new(temp_dir.path().to_path_buf()); let mut state = AutoUpdateState::default(); state.disabled_browsers.insert("firefox".to_string()); state .auto_update_downloads .insert("firefox-1.1.0".to_string()); state.pending_updates.push(UpdateNotification { id: "test".to_string(), browser: "firefox".to_string(), current_version: "1.0.0".to_string(), new_version: "1.1.0".to_string(), affected_profiles: vec!["profile1".to_string()], is_stable_update: true, timestamp: 1000, }); // Test save and load let state_file = test_settings_manager .get_settings_dir() .join("auto_update_state.json"); std::fs::create_dir_all(test_settings_manager.get_settings_dir()) .expect("Failed to create settings directory"); let json = serde_json::to_string_pretty(&state).expect("Failed to serialize state"); std::fs::write(&state_file, json).expect("Failed to write state file"); // Load state let content = std::fs::read_to_string(&state_file).expect("Failed to read state file"); let loaded_state: AutoUpdateState = serde_json::from_str(&content).expect("Failed to deserialize state"); assert_eq!(loaded_state.disabled_browsers.len(), 1); assert!(loaded_state.disabled_browsers.contains("firefox")); assert_eq!(loaded_state.auto_update_downloads.len(), 1); assert!(loaded_state.auto_update_downloads.contains("firefox-1.1.0")); assert_eq!(loaded_state.pending_updates.len(), 1); assert_eq!(loaded_state.pending_updates[0].id, "test"); } #[tokio::test] async fn test_browser_disable_enable_cycle() { use tempfile::TempDir; // Create a temporary directory for testing let temp_dir = TempDir::new().unwrap(); // Create a mock settings manager that uses the temp directory struct TestSettingsManager { settings_dir: std::path::PathBuf, } impl TestSettingsManager { fn new(settings_dir: std::path::PathBuf) -> Self { Self { settings_dir } } fn get_settings_dir(&self) -> std::path::PathBuf { self.settings_dir.clone() } } let test_settings_manager = TestSettingsManager::new(temp_dir.path().to_path_buf()); // Test browser disable/enable cycle with manual state management let state_file = test_settings_manager .get_settings_dir() .join("auto_update_state.json"); std::fs::create_dir_all(test_settings_manager.get_settings_dir()) .expect("Failed to create settings directory"); // Initially not disabled (empty state file means default state) let state = AutoUpdateState::default(); assert!( !state.disabled_browsers.contains("firefox"), "Firefox should not be disabled initially" ); // Start update (should disable) let mut state = AutoUpdateState::default(); state.disabled_browsers.insert("firefox".to_string()); state .auto_update_downloads .insert("firefox-1.1.0".to_string()); let json = serde_json::to_string_pretty(&state).expect("Failed to serialize state"); std::fs::write(&state_file, json).expect("Failed to write state file"); // Check that it's disabled let content = std::fs::read_to_string(&state_file).expect("Failed to read state file"); let loaded_state: AutoUpdateState = serde_json::from_str(&content).expect("Failed to deserialize state"); assert!( loaded_state.disabled_browsers.contains("firefox"), "Firefox should be disabled" ); assert!( loaded_state.auto_update_downloads.contains("firefox-1.1.0"), "Firefox download should be tracked" ); // Complete update (should enable) let mut state = loaded_state; state.disabled_browsers.remove("firefox"); state.auto_update_downloads.remove("firefox-1.1.0"); let json = serde_json::to_string_pretty(&state).expect("Failed to serialize final state"); std::fs::write(&state_file, json).expect("Failed to write final state file"); // Check that it's enabled again let content = std::fs::read_to_string(&state_file).expect("Failed to read final state file"); let final_state: AutoUpdateState = serde_json::from_str(&content).expect("Failed to deserialize final state"); assert!( !final_state.disabled_browsers.contains("firefox"), "Firefox should be enabled again" ); assert!( !final_state.auto_update_downloads.contains("firefox-1.1.0"), "Firefox download should not be tracked anymore" ); } #[test] fn test_dismiss_update_notification() { use tempfile::TempDir; // Create a temporary directory for testing let temp_dir = TempDir::new().unwrap(); // Create a mock settings manager that uses the temp directory struct TestSettingsManager { settings_dir: std::path::PathBuf, } impl TestSettingsManager { fn new(settings_dir: std::path::PathBuf) -> Self { Self { settings_dir } } fn get_settings_dir(&self) -> std::path::PathBuf { self.settings_dir.clone() } } let test_settings_manager = TestSettingsManager::new(temp_dir.path().to_path_buf()); let mut state = AutoUpdateState::default(); state.pending_updates.push(UpdateNotification { id: "test_notification".to_string(), browser: "firefox".to_string(), current_version: "1.0.0".to_string(), new_version: "1.1.0".to_string(), affected_profiles: vec!["profile1".to_string()], is_stable_update: true, timestamp: 1000, }); // Save initial state let state_file = test_settings_manager .get_settings_dir() .join("auto_update_state.json"); std::fs::create_dir_all(test_settings_manager.get_settings_dir()) .expect("Failed to create settings directory"); let json = serde_json::to_string_pretty(&state).expect("Failed to serialize initial state"); std::fs::write(&state_file, json).expect("Failed to write initial state file"); // Dismiss notification (remove from pending updates) state .pending_updates .retain(|n| n.id != "test_notification"); let json = serde_json::to_string_pretty(&state).expect("Failed to serialize updated state"); std::fs::write(&state_file, json).expect("Failed to write updated state file"); // Check that it's removed let content = std::fs::read_to_string(&state_file).expect("Failed to read updated state file"); let loaded_state: AutoUpdateState = serde_json::from_str(&content).expect("Failed to deserialize updated state"); assert_eq!( loaded_state.pending_updates.len(), 0, "Pending updates should be empty after dismissal" ); } } // Global singleton instance lazy_static::lazy_static! { static ref AUTO_UPDATER: AutoUpdater = AutoUpdater::new(); }