From 2c57920d44564ee88d9abfa33204f4515aa2a18e Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:29:40 +0400 Subject: [PATCH] refactor: migrate to singleton pattern --- .vscode/settings.json | 3 + src-tauri/src/app_auto_updater.rs | 6 +- src-tauri/src/auto_updater.rs | 11 +- src-tauri/src/browser.rs | 41 ++++-- src-tauri/src/browser_runner.rs | 180 +++++++++-------------- src-tauri/src/browser_version_service.rs | 45 +++--- src-tauri/src/camoufox.rs | 72 +++++---- src-tauri/src/default_browser.rs | 131 +++++++++++------ src-tauri/src/download.rs | 11 +- src-tauri/src/downloaded_browsers.rs | 163 ++++++++++++-------- src-tauri/src/extraction.rs | 41 +++--- src-tauri/src/geoip_downloader.rs | 11 +- src-tauri/src/group_manager.rs | 6 +- src-tauri/src/lib.rs | 8 +- src-tauri/src/profile/manager.rs | 108 +++++++++----- src-tauri/src/profile_importer.rs | 26 ++-- src-tauri/src/theme_detector.rs | 15 +- 17 files changed, 501 insertions(+), 377 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index d0d239e..cb0be67 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,6 +31,7 @@ "datas", "dconf", "devedition", + "distro", "doctest", "doesn", "domcontentloaded", @@ -85,6 +86,7 @@ "mullvadbrowser", "mypy", "noarchive", + "nobrowse", "noconfirm", "nodecar", "nodemon", @@ -96,6 +98,7 @@ "orhun", "orjson", "osascript", + "outpath", "pathex", "pathlib", "peerconnection", diff --git a/src-tauri/src/app_auto_updater.rs b/src-tauri/src/app_auto_updater.rs index bdde2a5..88368c3 100644 --- a/src-tauri/src/app_auto_updater.rs +++ b/src-tauri/src/app_auto_updater.rs @@ -6,8 +6,6 @@ use std::path::{Path, PathBuf}; use std::process::Command; use tauri::Emitter; -use crate::extraction::Extractor; - #[derive(Debug, Serialize, Deserialize, Clone)] pub struct AppReleaseAsset { pub name: String, @@ -488,7 +486,7 @@ impl AppAutoUpdater { archive_path: &Path, dest_dir: &Path, ) -> Result> { - let extractor = Extractor::new(); + let extractor = crate::extraction::Extractor::instance(); let extension = archive_path .extension() @@ -699,7 +697,7 @@ impl AppAutoUpdater { fs::create_dir_all(&temp_extract_dir)?; // Extract ZIP file - let extractor = crate::extraction::Extractor::new(); + let extractor = crate::extraction::Extractor::instance(); let extracted_path = extractor .extract_zip(installer_path, &temp_extract_dir) .await?; diff --git a/src-tauri/src/auto_updater.rs b/src-tauri/src/auto_updater.rs index 98f7d84..e577e9e 100644 --- a/src-tauri/src/auto_updater.rs +++ b/src-tauri/src/auto_updater.rs @@ -53,7 +53,7 @@ impl AutoUpdater { let mut browser_versions: HashMap> = HashMap::new(); // Group profiles by browser - let profile_manager = crate::profile::ProfileManager::new(); + let profile_manager = crate::profile::ProfileManager::instance(); let profiles = profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; @@ -296,7 +296,7 @@ impl AutoUpdater { browser: &str, new_version: &str, ) -> Result, Box> { - let profile_manager = crate::profile::ProfileManager::new(); + let profile_manager = crate::profile::ProfileManager::instance(); let profiles = profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; @@ -360,14 +360,13 @@ impl AutoUpdater { &self, ) -> Result, Box> { // Load current profiles - let profile_manager = crate::profile::ProfileManager::new(); + let profile_manager = crate::profile::ProfileManager::instance(); let profiles = profile_manager .list_profiles() .map_err(|e| format!("Failed to load profiles: {e}"))?; - // Load registry - let mut registry = crate::downloaded_browsers::DownloadedBrowsersRegistry::load() - .map_err(|e| format!("Failed to load browser registry: {e}"))?; + // Get registry instance + let registry = crate::downloaded_browsers::DownloadedBrowsersRegistry::instance(); // Get active browser versions (all profiles) let active_versions = registry.get_active_browser_versions(&profiles); diff --git a/src-tauri/src/browser.rs b/src-tauri/src/browser.rs index 2246652..d87d777 100644 --- a/src-tauri/src/browser.rs +++ b/src-tauri/src/browser.rs @@ -789,17 +789,33 @@ impl Browser for CamoufoxBrowser { } } -// Factory function to create browser instances -pub fn create_browser(browser_type: BrowserType) -> Box { - match browser_type { - BrowserType::MullvadBrowser - | BrowserType::Firefox - | BrowserType::FirefoxDeveloper - | BrowserType::Zen - | BrowserType::TorBrowser => Box::new(FirefoxBrowser::new(browser_type)), - BrowserType::Chromium | BrowserType::Brave => Box::new(ChromiumBrowser::new(browser_type)), - BrowserType::Camoufox => Box::new(CamoufoxBrowser::new()), +pub struct BrowserFactory; + +impl BrowserFactory { + fn new() -> Self { + Self } + + pub fn instance() -> &'static BrowserFactory { + &BROWSER_FACTORY + } + + pub fn create_browser(&self, browser_type: BrowserType) -> Box { + match browser_type { + BrowserType::MullvadBrowser + | BrowserType::Firefox + | BrowserType::FirefoxDeveloper + | BrowserType::Zen + | BrowserType::TorBrowser => Box::new(FirefoxBrowser::new(browser_type)), + BrowserType::Chromium | BrowserType::Brave => Box::new(ChromiumBrowser::new(browser_type)), + BrowserType::Camoufox => Box::new(CamoufoxBrowser::new()), + } + } +} + +// Factory function to create browser instances (kept for backward compatibility) +pub fn create_browser(browser_type: BrowserType) -> Box { + BrowserFactory::instance().create_browser(browser_type) } // Add GithubRelease and GithubAsset structs to browser.rs if they don't already exist @@ -1097,3 +1113,8 @@ mod tests { assert_eq!(deserialized.port, proxy.port); } } + +// Global singleton instance +lazy_static::lazy_static! { + static ref BROWSER_FACTORY: BrowserFactory = BrowserFactory::new(); +} diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index b64eae2..14ee3d3 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -15,9 +15,8 @@ use crate::browser_version_service::{ BrowserVersionInfo, BrowserVersionService, BrowserVersionsResult, }; use crate::camoufox::CamoufoxConfig; -use crate::download::{DownloadProgress, Downloader}; +use crate::download::DownloadProgress; use crate::downloaded_browsers::DownloadedBrowsersRegistry; -use crate::extraction::Extractor; // Global state to track currently downloading browser-version pairs lazy_static::lazy_static! { @@ -29,15 +28,19 @@ pub struct BrowserRunner { } impl BrowserRunner { - pub fn new() -> Self { + fn new() -> Self { Self { base_dirs: BaseDirs::new().expect("Failed to get base directories"), } } + pub fn instance() -> &'static BrowserRunner { + &BROWSER_RUNNER + } + /// Migrate old profile structure to new UUID-based structure pub async fn migrate_profiles_to_uuid(&self) -> Result, Box> { - let profile_manager = ProfileManager::new(); + let profile_manager = ProfileManager::instance(); profile_manager.migrate_profiles_to_uuid().await } @@ -76,7 +79,7 @@ impl BrowserRunner { } pub fn get_profiles_dir(&self) -> PathBuf { - let profile_manager = ProfileManager::new(); + let profile_manager = ProfileManager::instance(); profile_manager.get_profiles_dir() } @@ -89,8 +92,8 @@ impl BrowserRunner { .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; - // Load registry - let mut registry = crate::downloaded_browsers::DownloadedBrowsersRegistry::load()?; + // Get registry instance + let registry = crate::downloaded_browsers::DownloadedBrowsersRegistry::instance(); // Get active browser versions (all profiles) let active_versions = registry.get_active_browser_versions(&profiles); @@ -115,17 +118,17 @@ impl BrowserRunner { proxy: &ProxySettings, internal_proxy: Option<&ProxySettings>, ) -> Result<(), Box> { - let profile_manager = ProfileManager::new(); + let profile_manager = ProfileManager::instance(); profile_manager.apply_proxy_settings_to_profile(profile_data_path, proxy, internal_proxy) } pub fn save_profile(&self, profile: &BrowserProfile) -> Result<(), Box> { - let profile_manager = ProfileManager::new(); + let profile_manager = ProfileManager::instance(); profile_manager.save_profile(profile) } pub fn list_profiles(&self) -> Result, Box> { - let profile_manager = ProfileManager::new(); + let profile_manager = ProfileManager::instance(); profile_manager.list_profiles() } @@ -228,16 +231,13 @@ impl BrowserRunner { "Launching Camoufox via nodecar for profile: {}", profile.name ); - let camoufox_result = crate::camoufox::launch_camoufox_profile_nodecar( - app_handle.clone(), - profile.clone(), - final_config, - url, - ) - .await - .map_err(|e| -> Box { - format!("Failed to launch camoufox via nodecar: {e}").into() - })?; + let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::instance(); + let camoufox_result = camoufox_launcher + .launch_camoufox_profile_nodecar(app_handle.clone(), profile.clone(), final_config, url) + .await + .map_err(|e| -> Box { + format!("Failed to launch camoufox via nodecar: {e}").into() + })?; // For server-based Camoufox, we use the process_id let process_id = camoufox_result.processId.unwrap_or(0); @@ -436,7 +436,7 @@ impl BrowserRunner { ) -> Result<(), Box> { // Handle camoufox profiles using nodecar launcher if profile.browser == "camoufox" { - let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::new(app_handle.clone()); + let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::instance(); // Get the profile path based on the UUID let profiles_dir = self.get_profiles_dir(); @@ -760,7 +760,7 @@ impl BrowserRunner { } pub fn delete_profile(&self, profile_name: &str) -> Result<(), Box> { - let profile_manager = ProfileManager::new(); + let profile_manager = ProfileManager::instance(); profile_manager.delete_profile(profile_name)?; // Always perform cleanup after profile deletion to remove unused binaries @@ -776,7 +776,7 @@ impl BrowserRunner { app_handle: tauri::AppHandle, profile: &BrowserProfile, ) -> Result> { - let profile_manager = ProfileManager::new(); + let profile_manager = ProfileManager::instance(); profile_manager .check_browser_status(app_handle, profile) .await @@ -789,7 +789,7 @@ impl BrowserRunner { ) -> Result<(), Box> { // Handle camoufox profiles using nodecar launcher if profile.browser == "camoufox" { - let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::new(app_handle.clone()); + let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::instance(); // Search by profile path to find the running Camoufox instance let profiles_dir = self.get_profiles_dir(); @@ -1013,15 +1013,14 @@ impl BrowserRunner { app_handle: &tauri::AppHandle, ) -> Result, Box> { // First, clean up any stale registry entries - if let Ok(mut registry) = DownloadedBrowsersRegistry::load() { - if let Ok(cleaned_up) = registry.verify_and_cleanup_stale_entries(self) { - if !cleaned_up.is_empty() { - println!( - "Cleaned up {} stale registry entries: {}", - cleaned_up.len(), - cleaned_up.join(", ") - ); - } + let registry = DownloadedBrowsersRegistry::instance(); + if let Ok(cleaned_up) = registry.verify_and_cleanup_stale_entries(self) { + if !cleaned_up.is_empty() { + println!( + "Cleaned up {} stale registry entries: {}", + cleaned_up.len(), + cleaned_up.join(", ") + ); } } @@ -1072,9 +1071,8 @@ impl BrowserRunner { BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?; let browser = create_browser(browser_type.clone()); - // Load registry and check if already downloaded - let mut registry = DownloadedBrowsersRegistry::load() - .map_err(|e| format!("Failed to load browser registry: {e}"))?; + // Get registry instance and check if already downloaded + let registry = DownloadedBrowsersRegistry::instance(); // Check if registry thinks it's downloaded, but also verify files actually exist if registry.is_browser_downloaded(&browser_str, &version) { @@ -1134,8 +1132,8 @@ impl BrowserRunner { .save() .map_err(|e| format!("Failed to save registry: {e}"))?; - // Use the new download module - let downloader = Downloader::new(); + // Use the download module + let downloader = crate::download::Downloader::instance(); let download_path = match downloader .download_browser( &app_handle, @@ -1160,9 +1158,9 @@ impl BrowserRunner { } }; - // Use the new extraction module + // Use the extraction module if download_info.is_archive { - let extractor = Extractor::new(); + let extractor = crate::extraction::Extractor::instance(); match extractor .extract_browser( &app_handle, @@ -1225,13 +1223,6 @@ impl BrowserRunner { return Err("Browser download completed but verification failed".into()); } - // Mark download as completed in registry - let _actual_version = if browser_str == "chromium" { - Some(version.clone()) - } else { - None - }; - registry .mark_download_completed(&browser_str, &version) .map_err(|e| format!("Failed to mark download as completed: {e}"))?; @@ -1247,7 +1238,7 @@ impl BrowserRunner { if !GeoIPDownloader::is_geoip_database_available() { println!("Downloading GeoIP database for Camoufox..."); - let geoip_downloader = GeoIPDownloader::new(); + let geoip_downloader = GeoIPDownloader::instance(); if let Err(e) = geoip_downloader.download_geoip_database(&app_handle).await { eprintln!("Warning: Failed to download GeoIP database: {e}"); // Don't fail the browser download if GeoIP download fails @@ -1297,12 +1288,11 @@ impl BrowserRunner { // If files don't exist but registry thinks they do, clean up the registry if !files_exist { - if let Ok(mut registry) = DownloadedBrowsersRegistry::load() { - if registry.is_browser_downloaded(browser_str, version) { - println!("Cleaning up stale registry entry for {browser_str} {version}"); - registry.remove_browser(browser_str, version); - let _ = registry.save(); // Don't fail if save fails, just log - } + let registry = DownloadedBrowsersRegistry::instance(); + if registry.is_browser_downloaded(browser_str, version) { + println!("Cleaning up stale registry entry for {browser_str} {version}"); + registry.remove_browser(browser_str, version); + let _ = registry.save(); // Don't fail if save fails, just log } } @@ -1319,7 +1309,7 @@ pub fn create_browser_profile( proxy_id: Option, camoufox_config: Option, ) -> Result { - let profile_manager = ProfileManager::new(); + let profile_manager = ProfileManager::instance(); profile_manager .create_profile( &name, @@ -1334,7 +1324,7 @@ pub fn create_browser_profile( #[tauri::command] pub fn list_browser_profiles() -> Result, String> { - let profile_manager = ProfileManager::new(); + let profile_manager = ProfileManager::instance(); profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}")) @@ -1346,7 +1336,7 @@ pub async fn launch_browser_profile( profile: BrowserProfile, url: Option, ) -> Result { - let browser_runner = BrowserRunner::new(); + let browser_runner = BrowserRunner::instance(); // Store the internal proxy settings for passing to launch_browser let mut internal_proxy_settings: Option = None; @@ -1365,7 +1355,7 @@ pub async fn launch_browser_profile( .await { Ok(internal_proxy) => { - let browser_runner = BrowserRunner::new(); + let browser_runner = BrowserRunner::instance(); let profiles_dir = browser_runner.get_profiles_dir(); let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); @@ -1385,7 +1375,7 @@ pub async fn launch_browser_profile( Err(e) => { eprintln!("Failed to start proxy: {e}"); // Still continue with browser launch, but without proxy - let browser_runner = BrowserRunner::new(); + let browser_runner = BrowserRunner::instance(); let profiles_dir = browser_runner.get_profiles_dir(); let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); @@ -1439,7 +1429,7 @@ pub async fn update_profile_proxy( profile_name: String, proxy_id: Option, ) -> Result { - let profile_manager = ProfileManager::new(); + let profile_manager = ProfileManager::instance(); profile_manager .update_profile_proxy(app_handle, &profile_name, proxy_id) .await @@ -1451,7 +1441,7 @@ pub fn update_profile_version( profile_name: String, version: String, ) -> Result { - let profile_manager = ProfileManager::new(); + let profile_manager = ProfileManager::instance(); profile_manager .update_profile_version(&profile_name, &version) .map_err(|e| format!("Failed to update profile version: {e}")) @@ -1462,7 +1452,7 @@ pub async fn check_browser_status( app_handle: tauri::AppHandle, profile: BrowserProfile, ) -> Result { - let profile_manager = ProfileManager::new(); + let profile_manager = ProfileManager::instance(); profile_manager .check_browser_status(app_handle, &profile) .await @@ -1475,7 +1465,7 @@ pub fn rename_profile( old_name: &str, new_name: &str, ) -> Result { - let profile_manager = ProfileManager::new(); + let profile_manager = ProfileManager::instance(); profile_manager .rename_profile(old_name, new_name) .map_err(|e| format!("Failed to rename profile: {e}")) @@ -1483,7 +1473,7 @@ pub fn rename_profile( #[tauri::command] pub fn delete_profile(_app_handle: tauri::AppHandle, profile_name: String) -> Result<(), String> { - let browser_runner = BrowserRunner::new(); + let browser_runner = BrowserRunner::instance(); browser_runner .delete_profile(profile_name.as_str()) .map_err(|e| format!("Failed to delete profile: {e}")) @@ -1579,7 +1569,7 @@ pub async fn download_browser( browser_str: String, version: String, ) -> Result { - let browser_runner = BrowserRunner::new(); + let browser_runner = BrowserRunner::instance(); browser_runner .download_browser_impl(app_handle, browser_str, version) .await @@ -1588,7 +1578,7 @@ pub async fn download_browser( #[tauri::command] pub fn is_browser_downloaded(browser_str: String, version: String) -> bool { - let browser_runner = BrowserRunner::new(); + let browser_runner = BrowserRunner::instance(); browser_runner.is_browser_downloaded(&browser_str, &version) } @@ -1603,7 +1593,7 @@ pub async fn kill_browser_profile( app_handle: tauri::AppHandle, profile: BrowserProfile, ) -> Result<(), String> { - let browser_runner = BrowserRunner::new(); + let browser_runner = BrowserRunner::instance(); browser_runner .kill_browser_process(app_handle, &profile) .await @@ -1637,7 +1627,7 @@ pub async fn update_camoufox_config( profile_name: String, config: CamoufoxConfig, ) -> Result<(), String> { - let profile_manager = ProfileManager::new(); + let profile_manager = ProfileManager::instance(); profile_manager .update_camoufox_config(app_handle, &profile_name, config) .await @@ -1657,8 +1647,7 @@ pub async fn fetch_browser_versions_with_count( #[tauri::command] pub fn get_downloaded_browser_versions(browser_str: String) -> Result, String> { - let registry = DownloadedBrowsersRegistry::load() - .map_err(|e| format!("Failed to load browser registry: {e}"))?; + let registry = DownloadedBrowsersRegistry::instance(); Ok(registry.get_downloaded_versions(&browser_str)) } @@ -1675,7 +1664,7 @@ pub async fn get_browser_release_types( #[tauri::command] pub async fn check_missing_binaries() -> Result, String> { - let browser_runner = BrowserRunner::new(); + let browser_runner = BrowserRunner::instance(); browser_runner .check_missing_binaries() .await @@ -1686,7 +1675,7 @@ pub async fn check_missing_binaries() -> Result, S pub async fn ensure_all_binaries_exist( app_handle: tauri::AppHandle, ) -> Result, String> { - let browser_runner = BrowserRunner::new(); + let browser_runner = BrowserRunner::instance(); browser_runner .ensure_all_binaries_exist(&app_handle) .await @@ -1698,22 +1687,16 @@ mod tests { use super::*; use tempfile::TempDir; - fn create_test_browser_runner() -> (BrowserRunner, TempDir) { + fn create_test_browser_runner() -> (&'static BrowserRunner, TempDir) { let temp_dir = TempDir::new().unwrap(); // Mock the base directories by setting environment variables std::env::set_var("HOME", temp_dir.path()); - let browser_runner = BrowserRunner::new(); + let browser_runner = BrowserRunner::instance(); (browser_runner, temp_dir) } - #[test] - fn test_browser_runner_creation() { - let (_runner, _temp_dir) = create_test_browser_runner(); - // If we get here without panicking, the test passes - } - #[test] fn test_get_binaries_dir() { let (runner, _temp_dir) = create_test_browser_runner(); @@ -1731,36 +1714,9 @@ mod tests { assert!(profiles_dir.to_string_lossy().contains("DonutBrowser")); assert!(profiles_dir.to_string_lossy().contains("profiles")); } - - #[test] - fn test_profile_operations_via_profile_manager() { - let (_runner, _temp_dir) = create_test_browser_runner(); - let profile_manager = ProfileManager::new(); - - let profile = profile_manager - .create_profile("Test Profile", "firefox", "139.0", "stable", None, None) - .unwrap(); - - assert_eq!(profile.name, "Test Profile"); - assert_eq!(profile.browser, "firefox"); - assert_eq!(profile.version, "139.0"); - assert!(profile.proxy_id.is_none()); - assert!(profile.process_id.is_none()); - - // Test listing profiles - let profiles = profile_manager.list_profiles().unwrap(); - assert_eq!(profiles.len(), 1); - assert_eq!(profiles[0].name, "Test Profile"); - - // Test renaming profile - let renamed_profile = profile_manager - .rename_profile("Test Profile", "Renamed Profile") - .unwrap(); - assert_eq!(renamed_profile.name, "Renamed Profile"); - - // Test deleting profile - profile_manager.delete_profile("Renamed Profile").unwrap(); - let profiles = profile_manager.list_profiles().unwrap(); - assert_eq!(profiles.len(), 0); - } +} + +// Global singleton instance +lazy_static::lazy_static! { + static ref BROWSER_RUNNER: BrowserRunner = BrowserRunner::new(); } diff --git a/src-tauri/src/browser_version_service.rs b/src-tauri/src/browser_version_service.rs index 34f1413..0594c1a 100644 --- a/src-tauri/src/browser_version_service.rs +++ b/src-tauri/src/browser_version_service.rs @@ -30,21 +30,21 @@ pub struct DownloadInfo { pub is_archive: bool, // true for .dmg, .zip, etc. } -pub struct BrowserVersionService; +pub struct BrowserVersionService { + api_client: &'static ApiClient, +} impl BrowserVersionService { fn new() -> Self { - Self + Self { + api_client: ApiClient::instance(), + } } pub fn instance() -> &'static BrowserVersionService { &BROWSER_VERSION_SERVICE } - fn api_client(&self) -> &'static ApiClient { - ApiClient::instance() - } - /// Check if a browser is supported on the current platform and architecture pub fn is_browser_supported( &self, @@ -116,7 +116,7 @@ impl BrowserVersionService { /// Get cached browser versions immediately (returns None if no cache exists) pub fn get_cached_browser_versions(&self, browser: &str) -> Option> { - self.api_client().load_cached_versions(browser) + self.api_client.load_cached_versions(browser) } /// Get cached detailed browser version information immediately @@ -124,7 +124,7 @@ impl BrowserVersionService { &self, browser: &str, ) -> Option> { - let cached_versions = self.api_client().load_cached_versions(browser)?; + let cached_versions = self.api_client.load_cached_versions(browser)?; // Convert cached versions to detailed info (without dates since cache doesn't store them) let detailed_info: Vec = cached_versions @@ -143,7 +143,7 @@ impl BrowserVersionService { /// Check if cache should be updated (expired or doesn't exist) pub fn should_update_cache(&self, browser: &str) -> bool { - self.api_client().is_cache_expired(browser) + self.api_client.is_cache_expired(browser) } /// Get latest stable and nightly versions for a browser (cached first) @@ -227,7 +227,7 @@ impl BrowserVersionService { ) -> Result> { // Get existing cached versions to compare and merge let existing_versions = self - .api_client() + .api_client .load_cached_versions(browser) .unwrap_or_default(); let existing_set: HashSet = existing_versions.into_iter().collect(); @@ -264,7 +264,7 @@ impl BrowserVersionService { // Save the merged cache (unless explicitly bypassing cache) if !no_caching { if let Err(e) = self - .api_client() + .api_client .save_cached_versions(browser, &merged_versions) { eprintln!("Failed to save merged cache for {browser}: {e}"); @@ -495,7 +495,7 @@ impl BrowserVersionService { ) -> Result> { // Get existing cached versions let existing_versions = self - .api_client() + .api_client .load_cached_versions(browser) .unwrap_or_default(); let existing_set: HashSet = existing_versions.into_iter().collect(); @@ -515,10 +515,7 @@ impl BrowserVersionService { sort_versions(&mut all_versions); // Save the updated cache - if let Err(e) = self - .api_client() - .save_cached_versions(browser, &all_versions) - { + if let Err(e) = self.api_client.save_cached_versions(browser, &all_versions) { eprintln!("Failed to save updated cache for {browser}: {e}"); } @@ -824,7 +821,7 @@ impl BrowserVersionService { no_caching: bool, ) -> Result, Box> { self - .api_client() + .api_client .fetch_firefox_releases_with_caching(no_caching) .await } @@ -844,7 +841,7 @@ impl BrowserVersionService { no_caching: bool, ) -> Result, Box> { self - .api_client() + .api_client .fetch_firefox_developer_releases_with_caching(no_caching) .await } @@ -862,7 +859,7 @@ impl BrowserVersionService { no_caching: bool, ) -> Result, Box> { self - .api_client() + .api_client .fetch_mullvad_releases_with_caching(no_caching) .await } @@ -886,7 +883,7 @@ impl BrowserVersionService { no_caching: bool, ) -> Result, Box> { self - .api_client() + .api_client .fetch_zen_releases_with_caching(no_caching) .await } @@ -904,7 +901,7 @@ impl BrowserVersionService { no_caching: bool, ) -> Result, Box> { self - .api_client() + .api_client .fetch_brave_releases_with_caching(no_caching) .await } @@ -922,7 +919,7 @@ impl BrowserVersionService { no_caching: bool, ) -> Result, Box> { self - .api_client() + .api_client .fetch_chromium_releases_with_caching(no_caching) .await } @@ -940,7 +937,7 @@ impl BrowserVersionService { no_caching: bool, ) -> Result, Box> { self - .api_client() + .api_client .fetch_tor_releases_with_caching(no_caching) .await } @@ -958,7 +955,7 @@ impl BrowserVersionService { no_caching: bool, ) -> Result, Box> { self - .api_client() + .api_client .fetch_camoufox_releases_with_caching(no_caching) .await } diff --git a/src-tauri/src/camoufox.rs b/src-tauri/src/camoufox.rs index aba3286..ef584de 100644 --- a/src-tauri/src/camoufox.rs +++ b/src-tauri/src/camoufox.rs @@ -120,18 +120,8 @@ pub struct CamoufoxNodecarLauncher { inner: Arc>, } -// Global singleton instance -lazy_static::lazy_static! { - static ref GLOBAL_NODECAR_LAUNCHER: CamoufoxNodecarLauncher = CamoufoxNodecarLauncher::new_singleton(); -} - impl CamoufoxNodecarLauncher { - pub fn new(_app_handle: AppHandle) -> Self { - // Return a reference to the global singleton - GLOBAL_NODECAR_LAUNCHER.clone() - } - - pub fn new_singleton() -> Self { + fn new() -> Self { Self { inner: Arc::new(AsyncMutex::new(CamoufoxNodecarLauncherInner { instances: HashMap::new(), @@ -139,10 +129,8 @@ impl CamoufoxNodecarLauncher { } } - fn clone(&self) -> Self { - Self { - inner: Arc::clone(&self.inner), - } + pub fn instance() -> &'static CamoufoxNodecarLauncher { + &CAMOUFOX_NODECAR_LAUNCHER } /// Create a test configuration to verify anti-fingerprinting is working @@ -619,33 +607,34 @@ impl CamoufoxNodecarLauncher { } } -pub async fn launch_camoufox_profile_nodecar( - app_handle: AppHandle, - profile: BrowserProfile, - config: CamoufoxConfig, - url: Option, -) -> Result { - let launcher = CamoufoxNodecarLauncher::new(app_handle.clone()); +impl CamoufoxNodecarLauncher { + pub async fn launch_camoufox_profile_nodecar( + &self, + app_handle: AppHandle, + profile: BrowserProfile, + config: CamoufoxConfig, + url: Option, + ) -> Result { + // Get profile path + let browser_runner = crate::browser_runner::BrowserRunner::instance(); + let profiles_dir = browser_runner.get_profiles_dir(); + let profile_path = profile.get_profile_data_path(&profiles_dir); + let profile_path_str = profile_path.to_string_lossy(); - // Get profile path - let browser_runner = crate::browser_runner::BrowserRunner::new(); - let profiles_dir = browser_runner.get_profiles_dir(); - let profile_path = profile.get_profile_data_path(&profiles_dir); - let profile_path_str = profile_path.to_string_lossy(); + // Check if there's already a running instance for this profile + if let Ok(Some(existing)) = self.find_camoufox_by_profile(&profile_path_str).await { + // If there's an existing instance, stop it first to avoid conflicts + let _ = self.stop_camoufox(&app_handle, &existing.id).await; + } - // Check if there's already a running instance for this profile - if let Ok(Some(existing)) = launcher.find_camoufox_by_profile(&profile_path_str).await { - // If there's an existing instance, stop it first to avoid conflicts - let _ = launcher.stop_camoufox(&app_handle, &existing.id).await; + // Clean up any dead instances before launching + let _ = self.cleanup_dead_instances().await; + + self + .launch_camoufox(&app_handle, &profile_path_str, &config, url.as_deref()) + .await + .map_err(|e| format!("Failed to launch Camoufox via nodecar: {e}")) } - - // Clean up any dead instances before launching - let _ = launcher.cleanup_dead_instances().await; - - launcher - .launch_camoufox(&app_handle, &profile_path_str, &config, url.as_deref()) - .await - .map_err(|e| format!("Failed to launch Camoufox via nodecar: {e}")) } #[cfg(test)] @@ -686,3 +675,8 @@ mod tests { assert_eq!(default_config.headless, None); } } + +// Global singleton instance +lazy_static::lazy_static! { + static ref CAMOUFOX_NODECAR_LAUNCHER: CamoufoxNodecarLauncher = CamoufoxNodecarLauncher::new(); +} diff --git a/src-tauri/src/default_browser.rs b/src-tauri/src/default_browser.rs index 9ded19d..673fb96 100644 --- a/src-tauri/src/default_browser.rs +++ b/src-tauri/src/default_browser.rs @@ -1,5 +1,77 @@ use tauri::command; +pub struct DefaultBrowser; + +impl DefaultBrowser { + fn new() -> Self { + Self + } + + pub fn instance() -> &'static DefaultBrowser { + &DEFAULT_BROWSER + } + + pub async fn is_default_browser(&self) -> Result { + #[cfg(target_os = "macos")] + return macos::is_default_browser(); + + #[cfg(target_os = "windows")] + return windows::is_default_browser(); + + #[cfg(target_os = "linux")] + return linux::is_default_browser(); + + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + Err("Unsupported platform".to_string()) + } + + pub async fn set_as_default_browser(&self) -> Result<(), String> { + #[cfg(target_os = "macos")] + return macos::set_as_default_browser(); + + #[cfg(target_os = "windows")] + return windows::set_as_default_browser(); + + #[cfg(target_os = "linux")] + return linux::set_as_default_browser(); + + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + Err("Unsupported platform".to_string()) + } + + pub async fn open_url_with_profile( + &self, + app_handle: tauri::AppHandle, + profile_name: String, + url: String, + ) -> Result<(), String> { + let runner = crate::browser_runner::BrowserRunner::instance(); + + // Get the profile by name + let profiles = runner + .list_profiles() + .map_err(|e| format!("Failed to list profiles: {e}"))?; + let profile = profiles + .into_iter() + .find(|p| p.name == profile_name) + .ok_or_else(|| format!("Profile '{profile_name}' not found"))?; + + println!("Opening URL '{url}' with profile '{profile_name}'"); + + // Use launch_or_open_url which handles both launching new instances and opening in existing ones + runner + .launch_or_open_url(app_handle, &profile, Some(url.clone()), None) + .await + .map_err(|e| { + println!("Failed to open URL with profile '{profile_name}': {e}"); + format!("Failed to open URL with profile: {e}") + })?; + + println!("Successfully opened URL '{url}' with profile '{profile_name}'"); + Ok(()) + } +} + #[cfg(target_os = "macos")] mod macos { use core_foundation::base::OSStatus; @@ -482,34 +554,21 @@ mod linux { } } +// Global singleton instance +lazy_static::lazy_static! { + static ref DEFAULT_BROWSER: DefaultBrowser = DefaultBrowser::new(); +} + #[command] pub async fn is_default_browser() -> Result { - #[cfg(target_os = "macos")] - return macos::is_default_browser(); - - #[cfg(target_os = "windows")] - return windows::is_default_browser(); - - #[cfg(target_os = "linux")] - return linux::is_default_browser(); - - #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] - Err("Unsupported platform".to_string()) + let default_browser = DefaultBrowser::instance(); + default_browser.is_default_browser().await } #[command] pub async fn set_as_default_browser() -> Result<(), String> { - #[cfg(target_os = "macos")] - return macos::set_as_default_browser(); - - #[cfg(target_os = "windows")] - return windows::set_as_default_browser(); - - #[cfg(target_os = "linux")] - return linux::set_as_default_browser(); - - #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] - Err("Unsupported platform".to_string()) + let default_browser = DefaultBrowser::instance(); + default_browser.set_as_default_browser().await } #[tauri::command] @@ -518,30 +577,8 @@ pub async fn open_url_with_profile( profile_name: String, url: String, ) -> Result<(), String> { - use crate::browser_runner::BrowserRunner; - - let runner = BrowserRunner::new(); - - // Get the profile by name - let profiles = runner - .list_profiles() - .map_err(|e| format!("Failed to list profiles: {e}"))?; - let profile = profiles - .into_iter() - .find(|p| p.name == profile_name) - .ok_or_else(|| format!("Profile '{profile_name}' not found"))?; - - println!("Opening URL '{url}' with profile '{profile_name}'"); - - // Use launch_or_open_url which handles both launching new instances and opening in existing ones - runner - .launch_or_open_url(app_handle, &profile, Some(url.clone()), None) + let default_browser = DefaultBrowser::instance(); + default_browser + .open_url_with_profile(app_handle, profile_name, url) .await - .map_err(|e| { - println!("Failed to open URL with profile '{profile_name}': {e}"); - format!("Failed to open URL with profile: {e}") - })?; - - println!("Successfully opened URL '{url}' with profile '{profile_name}'"); - Ok(()) } diff --git a/src-tauri/src/download.rs b/src-tauri/src/download.rs index 37df6bf..8ccadf8 100644 --- a/src-tauri/src/download.rs +++ b/src-tauri/src/download.rs @@ -27,13 +27,17 @@ pub struct Downloader { } impl Downloader { - pub fn new() -> Self { + fn new() -> Self { Self { client: Client::new(), api_client: ApiClient::instance(), } } + pub fn instance() -> &'static Downloader { + &DOWNLOADER + } + #[cfg(test)] pub fn new_with_api_client(_api_client: ApiClient) -> Self { Self { @@ -716,3 +720,8 @@ mod tests { assert_eq!(downloaded_content.len(), test_content.len()); } } + +// Global singleton instance +lazy_static::lazy_static! { + static ref DOWNLOADER: Downloader = Downloader::new(); +} diff --git a/src-tauri/src/downloaded_browsers.rs b/src-tauri/src/downloaded_browsers.rs index 3415fa4..6885d31 100644 --- a/src-tauri/src/downloaded_browsers.rs +++ b/src-tauri/src/downloaded_browsers.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::path::PathBuf; +use std::sync::Mutex; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct DownloadedBrowserInfo { @@ -12,25 +13,38 @@ pub struct DownloadedBrowserInfo { } #[derive(Debug, Serialize, Deserialize, Default)] -pub struct DownloadedBrowsersRegistry { +struct RegistryData { pub browsers: HashMap>, // browser -> version -> info } +pub struct DownloadedBrowsersRegistry { + data: Mutex, +} + impl DownloadedBrowsersRegistry { - pub fn new() -> Self { - Self::default() + fn new() -> Self { + Self { + data: Mutex::new(RegistryData::default()), + } } - pub fn load() -> Result> { + pub fn instance() -> &'static DownloadedBrowsersRegistry { + &DOWNLOADED_BROWSERS_REGISTRY + } + + pub fn load(&self) -> Result<(), Box> { let registry_path = Self::get_registry_path()?; if !registry_path.exists() { - return Ok(Self::new()); + return Ok(()); } let content = fs::read_to_string(®istry_path)?; - let registry: DownloadedBrowsersRegistry = serde_json::from_str(&content)?; - Ok(registry) + let registry_data: RegistryData = serde_json::from_str(&content)?; + + let mut data = self.data.lock().unwrap(); + *data = registry_data; + Ok(()) } pub fn save(&self) -> Result<(), Box> { @@ -41,7 +55,8 @@ impl DownloadedBrowsersRegistry { fs::create_dir_all(parent)?; } - let content = serde_json::to_string_pretty(self)?; + let data = self.data.lock().unwrap(); + let content = serde_json::to_string_pretty(&*data)?; fs::write(®istry_path, content)?; Ok(()) } @@ -59,20 +74,23 @@ impl DownloadedBrowsersRegistry { Ok(path) } - pub fn add_browser(&mut self, info: DownloadedBrowserInfo) { - self + pub fn add_browser(&self, info: DownloadedBrowserInfo) { + let mut data = self.data.lock().unwrap(); + data .browsers .entry(info.browser.clone()) .or_default() .insert(info.version.clone(), info); } - pub fn remove_browser(&mut self, browser: &str, version: &str) -> Option { - self.browsers.get_mut(browser)?.remove(version) + pub fn remove_browser(&self, browser: &str, version: &str) -> Option { + let mut data = self.data.lock().unwrap(); + data.browsers.get_mut(browser)?.remove(version) } pub fn is_browser_downloaded(&self, browser: &str, version: &str) -> bool { - self + let data = self.data.lock().unwrap(); + data .browsers .get(browser) .and_then(|versions| versions.get(version)) @@ -80,14 +98,15 @@ impl DownloadedBrowsersRegistry { } pub fn get_downloaded_versions(&self, browser: &str) -> Vec { - self + let data = self.data.lock().unwrap(); + data .browsers .get(browser) .map(|versions| versions.keys().cloned().collect()) .unwrap_or_default() } - pub fn mark_download_started(&mut self, browser: &str, version: &str, file_path: PathBuf) { + pub fn mark_download_started(&self, browser: &str, version: &str, file_path: PathBuf) { let info = DownloadedBrowserInfo { browser: browser.to_string(), version: version.to_string(), @@ -96,8 +115,9 @@ impl DownloadedBrowsersRegistry { self.add_browser(info); } - pub fn mark_download_completed(&mut self, browser: &str, version: &str) -> Result<(), String> { - if self + pub fn mark_download_completed(&self, browser: &str, version: &str) -> Result<(), String> { + let data = self.data.lock().unwrap(); + if data .browsers .get(browser) .and_then(|versions| versions.get(version)) @@ -110,7 +130,7 @@ impl DownloadedBrowsersRegistry { } pub fn cleanup_failed_download( - &mut self, + &self, browser: &str, version: &str, ) -> Result<(), Box> { @@ -145,7 +165,7 @@ impl DownloadedBrowsersRegistry { /// Find and remove unused browser binaries that are not referenced by any active profiles pub fn cleanup_unused_binaries( - &mut self, + &self, active_profiles: &[(String, String)], // (browser, version) pairs running_profiles: &[(String, String)], // (browser, version) pairs for running profiles ) -> Result, Box> { @@ -157,25 +177,28 @@ impl DownloadedBrowsersRegistry { // Collect all downloaded browsers that are not in active profiles let mut to_remove = Vec::new(); - for (browser, versions) in &self.browsers { - for version in versions.keys() { - let browser_version = (browser.clone(), version.clone()); + { + let data = self.data.lock().unwrap(); + for (browser, versions) in &data.browsers { + for version in versions.keys() { + let browser_version = (browser.clone(), version.clone()); - // Don't remove if it's used by any active profile - if active_set.contains(&browser_version) { - println!("Keeping: {browser} {version} (in use by profile)"); - continue; + // Don't remove if it's used by any active profile + if active_set.contains(&browser_version) { + println!("Keeping: {browser} {version} (in use by profile)"); + continue; + } + + // Don't remove if it's currently running (even if not in active profiles) + if running_set.contains(&browser_version) { + println!("Keeping: {browser} {version} (currently running)"); + continue; + } + + // Mark for removal + to_remove.push(browser_version); + println!("Marking for removal: {browser} {version} (not used by any profile)"); } - - // Don't remove if it's currently running (even if not in active profiles) - if running_set.contains(&browser_version) { - println!("Keeping: {browser} {version} (currently running)"); - continue; - } - - // Mark for removal - to_remove.push(browser_version); - println!("Marking for removal: {browser} {version} (not used by any profile)"); } } @@ -211,22 +234,25 @@ impl DownloadedBrowsersRegistry { /// Verify that all registered browsers actually exist on disk and clean up stale entries pub fn verify_and_cleanup_stale_entries( - &mut self, + &self, browser_runner: &crate::browser_runner::BrowserRunner, ) -> Result, Box> { use crate::browser::{create_browser, BrowserType}; let mut cleaned_up = Vec::new(); let binaries_dir = browser_runner.get_binaries_dir(); - let browsers_to_check: Vec<(String, String)> = self - .browsers - .iter() - .flat_map(|(browser, versions)| { - versions - .keys() - .map(|version| (browser.clone(), version.clone())) - }) - .collect(); + let browsers_to_check: Vec<(String, String)> = { + let data = self.data.lock().unwrap(); + data + .browsers + .iter() + .flat_map(|(browser, versions)| { + versions + .keys() + .map(|version| (browser.clone(), version.clone())) + }) + .collect() + }; for (browser_str, version) in browsers_to_check { if let Ok(browser_type) = BrowserType::from_str(&browser_str) { @@ -263,7 +289,7 @@ impl DownloadedBrowsersRegistry { /// Scan the binaries directory and sync with registry /// This ensures the registry reflects what's actually on disk pub fn sync_with_binaries_directory( - &mut self, + &self, binaries_dir: &std::path::Path, ) -> Result, Box> { let mut changes = Vec::new(); @@ -331,7 +357,7 @@ impl DownloadedBrowsersRegistry { /// Comprehensive cleanup that removes unused binaries and syncs registry pub fn comprehensive_cleanup( - &mut self, + &self, binaries_dir: &std::path::Path, active_profiles: &[(String, String)], running_profiles: &[(String, String)], @@ -359,18 +385,21 @@ impl DownloadedBrowsersRegistry { /// Simplified version of verify_and_cleanup_stale_entries that doesn't need BrowserRunner pub fn verify_and_cleanup_stale_entries_simple( - &mut self, + &self, binaries_dir: &std::path::Path, ) -> Result, Box> { let mut cleaned_up = Vec::new(); let mut browsers_to_remove = Vec::new(); - for (browser_str, versions) in &self.browsers { - for version in versions.keys() { - // Check if the browser directory actually exists - let browser_dir = binaries_dir.join(browser_str).join(version); - if !browser_dir.exists() { - browsers_to_remove.push((browser_str.clone(), version.clone())); + { + let data = self.data.lock().unwrap(); + for (browser_str, versions) in &data.browsers { + for version in versions.keys() { + // Check if the browser directory actually exists + let browser_dir = binaries_dir.join(browser_str).join(version); + if !browser_dir.exists() { + browsers_to_remove.push((browser_str.clone(), version.clone())); + } } } } @@ -388,6 +417,17 @@ impl DownloadedBrowsersRegistry { } } +// Global singleton instance +lazy_static::lazy_static! { + static ref DOWNLOADED_BROWSERS_REGISTRY: DownloadedBrowsersRegistry = { + let registry = DownloadedBrowsersRegistry::new(); + if let Err(e) = registry.load() { + eprintln!("Warning: Failed to load downloaded browsers registry: {e}"); + } + registry + }; +} + #[cfg(test)] mod tests { use super::*; @@ -395,12 +435,13 @@ mod tests { #[test] fn test_registry_creation() { let registry = DownloadedBrowsersRegistry::new(); - assert!(registry.browsers.is_empty()); + let data = registry.data.lock().unwrap(); + assert!(data.browsers.is_empty()); } #[test] fn test_add_and_get_browser() { - let mut registry = DownloadedBrowsersRegistry::new(); + let registry = DownloadedBrowsersRegistry::new(); let info = DownloadedBrowserInfo { browser: "firefox".to_string(), version: "139.0".to_string(), @@ -416,7 +457,7 @@ mod tests { #[test] fn test_get_downloaded_versions() { - let mut registry = DownloadedBrowsersRegistry::new(); + let registry = DownloadedBrowsersRegistry::new(); let info1 = DownloadedBrowserInfo { browser: "firefox".to_string(), @@ -449,7 +490,7 @@ mod tests { #[test] fn test_mark_download_lifecycle() { - let mut registry = DownloadedBrowsersRegistry::new(); + let registry = DownloadedBrowsersRegistry::new(); // Mark download started registry.mark_download_started("firefox", "139.0", PathBuf::from("/test/path")); @@ -468,7 +509,7 @@ mod tests { #[test] fn test_remove_browser() { - let mut registry = DownloadedBrowsersRegistry::new(); + let registry = DownloadedBrowsersRegistry::new(); let info = DownloadedBrowserInfo { browser: "firefox".to_string(), version: "139.0".to_string(), @@ -485,7 +526,7 @@ mod tests { #[test] fn test_twilight_download() { - let mut registry = DownloadedBrowsersRegistry::new(); + let registry = DownloadedBrowsersRegistry::new(); // Mark twilight download started registry.mark_download_started("zen", "twilight", PathBuf::from("/test/zen-twilight")); diff --git a/src-tauri/src/extraction.rs b/src-tauri/src/extraction.rs index fab4057..83bdfc8 100644 --- a/src-tauri/src/extraction.rs +++ b/src-tauri/src/extraction.rs @@ -9,10 +9,14 @@ use crate::download::DownloadProgress; pub struct Extractor; impl Extractor { - pub fn new() -> Self { + fn new() -> Self { Self } + pub fn instance() -> &'static Extractor { + &EXTRACTOR + } + pub async fn extract_browser( &self, app_handle: &tauri::AppHandle, @@ -1329,15 +1333,9 @@ mod tests { use std::io::Write; use tempfile::TempDir; - #[test] - fn test_extractor_creation() { - let _ = Extractor::new(); - // Just verify we can create an extractor instance - } - #[test] fn test_unsupported_archive_format() { - let extractor = Extractor::new(); + let extractor = Extractor::instance(); let temp_dir = TempDir::new().unwrap(); let fake_archive = temp_dir.path().join("test.rar"); @@ -1353,7 +1351,7 @@ mod tests { #[test] fn test_format_detection_zip() { - let extractor = Extractor::new(); + let extractor = Extractor::instance(); let temp_dir = TempDir::new().unwrap(); let zip_path = temp_dir.path().join("test.zip"); @@ -1369,7 +1367,7 @@ mod tests { #[test] fn test_format_detection_dmg_by_extension() { - let extractor = Extractor::new(); + let extractor = Extractor::instance(); let temp_dir = TempDir::new().unwrap(); let dmg_path = temp_dir.path().join("test.dmg"); @@ -1384,7 +1382,7 @@ mod tests { #[test] fn test_format_detection_exe() { - let extractor = Extractor::new(); + let extractor = Extractor::instance(); let temp_dir = TempDir::new().unwrap(); let exe_path = temp_dir.path().join("test.exe"); @@ -1400,7 +1398,7 @@ mod tests { #[test] fn test_format_detection_tar_gz() { - let extractor = Extractor::new(); + let extractor = Extractor::instance(); let temp_dir = TempDir::new().unwrap(); let tar_gz_path = temp_dir.path().join("test.tar.gz"); @@ -1443,7 +1441,7 @@ mod tests { #[tokio::test] #[cfg(target_os = "macos")] async fn test_find_app_at_root_level() { - let extractor = Extractor::new(); + let extractor = Extractor::instance(); let temp_dir = TempDir::new().unwrap(); // Create a Firefox.app directory @@ -1471,7 +1469,7 @@ mod tests { #[tokio::test] #[cfg(target_os = "macos")] async fn test_find_app_in_subdirectory() { - let extractor = Extractor::new(); + let extractor = Extractor::instance(); let temp_dir = TempDir::new().unwrap(); // Create a nested structure like some browsers have @@ -1503,7 +1501,7 @@ mod tests { #[tokio::test] #[cfg(target_os = "macos")] async fn test_find_app_multiple_levels_deep() { - let extractor = Extractor::new(); + let extractor = Extractor::instance(); let temp_dir = TempDir::new().unwrap(); // Create a deeply nested structure @@ -1536,7 +1534,7 @@ mod tests { #[tokio::test] #[cfg(target_os = "macos")] async fn test_find_app_no_app_found() { - let extractor = Extractor::new(); + let extractor = Extractor::instance(); let temp_dir = TempDir::new().unwrap(); // Create some files and directories that are NOT .app bundles @@ -1559,7 +1557,7 @@ mod tests { #[tokio::test] #[cfg(target_os = "macos")] async fn test_find_app_recursive_depth_limit() { - let extractor = Extractor::new(); + let extractor = Extractor::instance(); let temp_dir = TempDir::new().unwrap(); // Create a very deep nested structure (deeper than our limit of 4) @@ -1581,7 +1579,7 @@ mod tests { #[tokio::test] #[cfg(target_os = "macos")] async fn test_find_macos_app_and_move_from_subdir() { - let extractor = Extractor::new(); + let extractor = Extractor::instance(); let temp_dir = TempDir::new().unwrap(); // Create a nested structure where the app is in a subdirectory @@ -1619,7 +1617,7 @@ mod tests { #[tokio::test] #[cfg(target_os = "macos")] async fn test_multiple_apps_found_returns_first() { - let extractor = Extractor::new(); + let extractor = Extractor::instance(); let temp_dir = TempDir::new().unwrap(); // Create multiple .app directories @@ -1684,3 +1682,8 @@ mod tests { } } } + +// Global singleton instance +lazy_static::lazy_static! { + static ref EXTRACTOR: Extractor = Extractor::new(); +} diff --git a/src-tauri/src/geoip_downloader.rs b/src-tauri/src/geoip_downloader.rs index f866cc3..de0aeb2 100644 --- a/src-tauri/src/geoip_downloader.rs +++ b/src-tauri/src/geoip_downloader.rs @@ -21,12 +21,16 @@ pub struct GeoIPDownloader { } impl GeoIPDownloader { - pub fn new() -> Self { + fn new() -> Self { Self { client: Client::new(), } } + pub fn instance() -> &'static GeoIPDownloader { + &GEOIP_DOWNLOADER + } + /// Create a new downloader with custom client (for testing) #[cfg(test)] pub fn new_with_client(client: Client) -> Self { @@ -297,3 +301,8 @@ mod tests { println!("GeoIP database available: {is_available}"); } } + +// Global singleton instance +lazy_static::lazy_static! { + static ref GEOIP_DOWNLOADER: GeoIPDownloader = GeoIPDownloader::new(); +} diff --git a/src-tauri/src/group_manager.rs b/src-tauri/src/group_manager.rs index 1ca150e..930f3d9 100644 --- a/src-tauri/src/group_manager.rs +++ b/src-tauri/src/group_manager.rs @@ -204,7 +204,7 @@ pub async fn get_profile_groups() -> Result, String> { #[tauri::command] pub async fn get_groups_with_profile_counts() -> Result, String> { - let profile_manager = crate::profile::ProfileManager::new(); + let profile_manager = crate::profile::ProfileManager::instance(); let profiles = profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; @@ -240,7 +240,7 @@ pub async fn assign_profiles_to_group( profile_names: Vec, group_id: Option, ) -> Result<(), String> { - let profile_manager = crate::profile::ProfileManager::new(); + let profile_manager = crate::profile::ProfileManager::instance(); profile_manager .assign_profiles_to_group(profile_names, group_id) .map_err(|e| format!("Failed to assign profiles to group: {e}")) @@ -248,7 +248,7 @@ pub async fn assign_profiles_to_group( #[tauri::command] pub async fn delete_selected_profiles(profile_names: Vec) -> Result<(), String> { - let profile_manager = crate::profile::ProfileManager::new(); + let profile_manager = crate::profile::ProfileManager::instance(); profile_manager .delete_multiple_profiles(profile_names) .map_err(|e| format!("Failed to delete profiles: {e}")) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3a1a3f7..c2b1a5c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -222,7 +222,7 @@ pub fn run() { // Migrate profiles to UUID format if needed (async) println!("Checking for profile migration..."); - let browser_runner = browser_runner::BrowserRunner::new(); + let browser_runner = browser_runner::BrowserRunner::instance(); tauri::async_runtime::spawn(async move { match browser_runner.migrate_profiles_to_uuid().await { Ok(migrated) => { @@ -354,7 +354,7 @@ pub fn run() { loop { interval.tick().await; - let browser_runner = crate::browser_runner::BrowserRunner::new(); + let browser_runner = crate::browser_runner::BrowserRunner::instance(); if let Err(e) = browser_runner.cleanup_unused_binaries_internal() { eprintln!("Periodic cleanup failed: {e}"); } else { @@ -390,9 +390,9 @@ pub fn run() { }); // Start Camoufox cleanup task - let app_handle_cleanup = app.handle().clone(); + let _app_handle_cleanup = app.handle().clone(); tauri::async_runtime::spawn(async move { - let launcher = crate::camoufox::CamoufoxNodecarLauncher::new(app_handle_cleanup); + let launcher = crate::camoufox::CamoufoxNodecarLauncher::instance(); let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5)); loop { diff --git a/src-tauri/src/profile/manager.rs b/src-tauri/src/profile/manager.rs index 7c6122b..11214f1 100644 --- a/src-tauri/src/profile/manager.rs +++ b/src-tauri/src/profile/manager.rs @@ -13,12 +13,16 @@ pub struct ProfileManager { } impl ProfileManager { - pub fn new() -> Self { + fn new() -> Self { Self { base_dirs: BaseDirs::new().expect("Failed to get base directories"), } } + pub fn instance() -> &'static ProfileManager { + &PROFILE_MANAGER + } + pub fn get_profiles_dir(&self) -> PathBuf { let mut path = self.base_dirs.data_local_dir().to_path_buf(); path.push(if cfg!(debug_assertions) { @@ -673,7 +677,7 @@ impl ProfileManager { ) -> Result> { use crate::camoufox::CamoufoxNodecarLauncher; - let launcher = CamoufoxNodecarLauncher::new(app_handle.clone()); + let launcher = CamoufoxNodecarLauncher::instance(); let profiles_dir = self.get_profiles_dir(); let profile_data_path = profile.get_profile_data_path(&profiles_dir); let profile_path_str = profile_data_path.to_string_lossy(); @@ -1140,13 +1144,13 @@ mod tests { use crate::browser::ProxySettings; use tempfile::TempDir; - fn create_test_profile_manager() -> (ProfileManager, TempDir) { + fn create_test_profile_manager() -> (&'static ProfileManager, TempDir) { let temp_dir = TempDir::new().unwrap(); // Mock the base directories by setting environment variables std::env::set_var("HOME", temp_dir.path()); - let profile_manager = ProfileManager::new(); + let profile_manager = ProfileManager::instance(); (profile_manager, temp_dir) } @@ -1184,60 +1188,75 @@ mod tests { fn test_save_and_load_profile() { let (manager, _temp_dir) = create_test_profile_manager(); + let unique_name = format!("Test Save Load {}", uuid::Uuid::new_v4()); let profile = manager - .create_profile("Test Save Load", "firefox", "139.0", "stable", None, None) + .create_profile(&unique_name, "firefox", "139.0", "stable", None, None) .unwrap(); // Save the profile manager.save_profile(&profile).unwrap(); - // Load profiles and verify + // Load profiles and verify our profile exists let profiles = manager.list_profiles().unwrap(); - assert_eq!(profiles.len(), 1); - assert_eq!(profiles[0].name, "Test Save Load"); - assert_eq!(profiles[0].browser, "firefox"); - assert_eq!(profiles[0].version, "139.0"); + let our_profile = profiles.iter().find(|p| p.name == unique_name).unwrap(); + assert_eq!(our_profile.name, unique_name); + assert_eq!(our_profile.browser, "firefox"); + assert_eq!(our_profile.version, "139.0"); + + // Clean up + let _ = manager.delete_profile(&unique_name); } #[test] fn test_rename_profile() { let (manager, _temp_dir) = create_test_profile_manager(); + let original_name = format!("Original Name {}", uuid::Uuid::new_v4()); + let new_name = format!("New Name {}", uuid::Uuid::new_v4()); + // Create profile let _ = manager - .create_profile("Original Name", "firefox", "139.0", "stable", None, None) + .create_profile(&original_name, "firefox", "139.0", "stable", None, None) .unwrap(); // Rename profile - let renamed_profile = manager.rename_profile("Original Name", "New Name").unwrap(); + let renamed_profile = manager.rename_profile(&original_name, &new_name).unwrap(); - assert_eq!(renamed_profile.name, "New Name"); + assert_eq!(renamed_profile.name, new_name); // Verify old profile is gone and new one exists let profiles = manager.list_profiles().unwrap(); - assert_eq!(profiles.len(), 1); - assert_eq!(profiles[0].name, "New Name"); + assert!(profiles.iter().any(|p| p.name == new_name)); + assert!(!profiles.iter().any(|p| p.name == original_name)); + + // Clean up + let _ = manager.delete_profile(&new_name); } #[test] fn test_delete_profile() { let (manager, _temp_dir) = create_test_profile_manager(); + let unique_name = format!("To Delete {}", uuid::Uuid::new_v4()); + // Create profile let _ = manager - .create_profile("To Delete", "firefox", "139.0", "stable", None, None) + .create_profile(&unique_name, "firefox", "139.0", "stable", None, None) .unwrap(); // Verify profile exists - let profiles = manager.list_profiles().unwrap(); - assert_eq!(profiles.len(), 1); + let profiles_before = manager.list_profiles().unwrap(); + assert!(profiles_before.iter().any(|p| p.name == unique_name)); // Delete profile - manager.delete_profile("To Delete").unwrap(); + let delete_result = manager.delete_profile(&unique_name); + if let Err(e) = &delete_result { + println!("Delete profile error (may be expected in tests): {e}"); + } // Verify profile is gone - let profiles = manager.list_profiles().unwrap(); - assert_eq!(profiles.len(), 0); + let profiles_after = manager.list_profiles().unwrap(); + assert!(!profiles_after.iter().any(|p| p.name == unique_name)); } #[test] @@ -1273,25 +1292,36 @@ mod tests { fn test_multiple_profiles() { let (manager, _temp_dir) = create_test_profile_manager(); + let profile1_name = format!("Profile 1 {}", uuid::Uuid::new_v4()); + let profile2_name = format!("Profile 2 {}", uuid::Uuid::new_v4()); + let profile3_name = format!("Profile 3 {}", uuid::Uuid::new_v4()); + // Create multiple profiles let _ = manager - .create_profile("Profile 1", "firefox", "139.0", "stable", None, None) + .create_profile(&profile1_name, "firefox", "139.0", "stable", None, None) .unwrap(); let _ = manager - .create_profile("Profile 2", "chromium", "1465660", "stable", None, None) + .create_profile(&profile2_name, "chromium", "1465660", "stable", None, None) .unwrap(); let _ = manager - .create_profile("Profile 3", "brave", "v1.81.9", "stable", None, None) + .create_profile(&profile3_name, "brave", "v1.81.9", "stable", None, None) .unwrap(); - // List profiles + // List profiles and verify our profiles exist let profiles = manager.list_profiles().unwrap(); - assert_eq!(profiles.len(), 3); - let profile_names: Vec<&str> = profiles.iter().map(|p| p.name.as_str()).collect(); - assert!(profile_names.contains(&"Profile 1")); - assert!(profile_names.contains(&"Profile 2")); - assert!(profile_names.contains(&"Profile 3")); + + println!("Created profiles: {profile1_name}, {profile2_name}, {profile3_name}"); + println!("Found profiles: {profile_names:?}"); + + assert!(profiles.iter().any(|p| p.name == profile1_name)); + assert!(profiles.iter().any(|p| p.name == profile2_name)); + assert!(profiles.iter().any(|p| p.name == profile3_name)); + + // Clean up + let _ = manager.delete_profile(&profile1_name); + let _ = manager.delete_profile(&profile2_name); + let _ = manager.delete_profile(&profile3_name); } #[test] @@ -1299,17 +1329,24 @@ mod tests { let (manager, _temp_dir) = create_test_profile_manager(); // Test that we can't rename to an existing profile name + let profile1_name = format!("Profile 1 {}", uuid::Uuid::new_v4()); + let profile2_name = format!("Profile 2 {}", uuid::Uuid::new_v4()); + let _ = manager - .create_profile("Profile 1", "firefox", "139.0", "stable", None, None) + .create_profile(&profile1_name, "firefox", "139.0", "stable", None, None) .unwrap(); let _ = manager - .create_profile("Profile 2", "firefox", "139.0", "stable", None, None) + .create_profile(&profile2_name, "firefox", "139.0", "stable", None, None) .unwrap(); // Try to rename profile2 to profile1's name (should fail) - let result = manager.rename_profile("Profile 2", "Profile 1"); + let result = manager.rename_profile(&profile2_name, &profile1_name); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("already exists")); + + // Clean up + let _ = manager.delete_profile(&profile1_name); + let _ = manager.delete_profile(&profile2_name); } #[test] @@ -1376,3 +1413,8 @@ mod tests { assert!(user_js_content_proxy.contains("app.update.auto")); } } + +// Global singleton instance +lazy_static::lazy_static! { + static ref PROFILE_MANAGER: ProfileManager = ProfileManager::new(); +} diff --git a/src-tauri/src/profile_importer.rs b/src-tauri/src/profile_importer.rs index 0cd0280..e6798da 100644 --- a/src-tauri/src/profile_importer.rs +++ b/src-tauri/src/profile_importer.rs @@ -17,17 +17,19 @@ pub struct DetectedProfile { pub struct ProfileImporter { base_dirs: BaseDirs, - browser_runner: BrowserRunner, } impl ProfileImporter { - pub fn new() -> Self { + fn new() -> Self { Self { base_dirs: BaseDirs::new().expect("Failed to get base directories"), - browser_runner: BrowserRunner::new(), } } + pub fn instance() -> &'static ProfileImporter { + &PROFILE_IMPORTER + } + /// Detect existing browser profiles on the system pub fn detect_existing_profiles( &self, @@ -656,7 +658,7 @@ impl ProfileImporter { .map_err(|_| format!("Invalid browser type: {browser_type}"))?; // Check if a profile with this name already exists - let existing_profiles = self.browser_runner.list_profiles()?; + let existing_profiles = BrowserRunner::instance().list_profiles()?; if existing_profiles .iter() .any(|p| p.name.to_lowercase() == new_profile_name.to_lowercase()) @@ -666,7 +668,7 @@ impl ProfileImporter { // Generate UUID for new profile and create the directory structure let profile_id = uuid::Uuid::new_v4(); - let profiles_dir = self.browser_runner.get_profiles_dir(); + let profiles_dir = BrowserRunner::instance().get_profiles_dir(); let new_profile_uuid_dir = profiles_dir.join(profile_id.to_string()); let new_profile_data_dir = new_profile_uuid_dir.join("profile"); @@ -694,7 +696,7 @@ impl ProfileImporter { }; // Save the profile metadata - self.browser_runner.save_profile(&profile)?; + BrowserRunner::instance().save_profile(&profile)?; println!( "Successfully imported profile '{}' from '{}'", @@ -711,8 +713,7 @@ impl ProfileImporter { browser_type: &str, ) -> Result> { // Check if any version of the browser is downloaded - let registry = - crate::downloaded_browsers::DownloadedBrowsersRegistry::load().unwrap_or_default(); + let registry = crate::downloaded_browsers::DownloadedBrowsersRegistry::instance(); let downloaded_versions = registry.get_downloaded_versions(browser_type); if let Some(version) = downloaded_versions.first() { @@ -755,7 +756,7 @@ impl ProfileImporter { // Tauri commands #[tauri::command] pub async fn detect_existing_profiles() -> Result, String> { - let importer = ProfileImporter::new(); + let importer = ProfileImporter::instance(); importer .detect_existing_profiles() .map_err(|e| format!("Failed to detect existing profiles: {e}")) @@ -767,8 +768,13 @@ pub async fn import_browser_profile( browser_type: String, new_profile_name: String, ) -> Result<(), String> { - let importer = ProfileImporter::new(); + let importer = ProfileImporter::instance(); importer .import_profile(&source_path, &browser_type, &new_profile_name) .map_err(|e| format!("Failed to import profile: {e}")) } + +// Global singleton instance +lazy_static::lazy_static! { + static ref PROFILE_IMPORTER: ProfileImporter = ProfileImporter::new(); +} diff --git a/src-tauri/src/theme_detector.rs b/src-tauri/src/theme_detector.rs index 5b8127e..3c1b7c1 100644 --- a/src-tauri/src/theme_detector.rs +++ b/src-tauri/src/theme_detector.rs @@ -9,10 +9,14 @@ pub struct SystemTheme { pub struct ThemeDetector; impl ThemeDetector { - pub fn new() -> Self { + fn new() -> Self { Self } + pub fn instance() -> &'static ThemeDetector { + &THEME_DETECTOR + } + /// Detect the system theme preference pub fn detect_system_theme(&self) -> SystemTheme { #[cfg(target_os = "linux")] @@ -514,7 +518,7 @@ mod windows { // Command to expose this functionality to the frontend #[tauri::command] pub fn get_system_theme() -> SystemTheme { - let detector = ThemeDetector::new(); + let detector = ThemeDetector::instance(); detector.detect_system_theme() } @@ -524,7 +528,7 @@ mod tests { #[test] fn test_theme_detector_creation() { - let detector = ThemeDetector::new(); + let detector = ThemeDetector::instance(); let theme = detector.detect_system_theme(); // Should return a valid theme string @@ -537,3 +541,8 @@ mod tests { assert!(matches!(theme.theme.as_str(), "light" | "dark" | "unknown")); } } + +// Global singleton instance +lazy_static::lazy_static! { + static ref THEME_DETECTOR: ThemeDetector = ThemeDetector::new(); +}