refactor: migrate to singleton pattern

This commit is contained in:
zhom
2025-08-02 16:29:40 +04:00
parent 57a36a5fc2
commit 2c57920d44
17 changed files with 501 additions and 377 deletions
+3
View File
@@ -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",
+2 -4
View File
@@ -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<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
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?;
+5 -6
View File
@@ -53,7 +53,7 @@ impl AutoUpdater {
let mut browser_versions: HashMap<String, Vec<BrowserVersionInfo>> = 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<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
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<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
// 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);
+31 -10
View File
@@ -789,17 +789,33 @@ impl Browser for CamoufoxBrowser {
}
}
// Factory function to create browser instances
pub fn create_browser(browser_type: BrowserType) -> Box<dyn Browser> {
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<dyn Browser> {
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<dyn Browser> {
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();
}
+68 -112
View File
@@ -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<Vec<String>, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
let profile_manager = ProfileManager::new();
let profile_manager = ProfileManager::instance();
profile_manager.save_profile(profile)
}
pub fn list_profiles(&self) -> Result<Vec<BrowserProfile>, Box<dyn std::error::Error>> {
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<dyn std::error::Error + Send + Sync> {
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<dyn std::error::Error + Send + Sync> {
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<dyn std::error::Error + Send + Sync>> {
// 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<dyn std::error::Error>> {
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<bool, Box<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
// 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<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
// 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<String>,
camoufox_config: Option<CamoufoxConfig>,
) -> Result<BrowserProfile, String> {
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<Vec<BrowserProfile>, 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<String>,
) -> Result<BrowserProfile, String> {
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<ProxySettings> = 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<String>,
) -> Result<BrowserProfile, String> {
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<BrowserProfile, String> {
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<bool, String> {
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<BrowserProfile, String> {
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<String, String> {
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<Vec<String>, 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<Vec<(String, String, String)>, 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<Vec<(String, String, String)>, S
pub async fn ensure_all_binaries_exist(
app_handle: tauri::AppHandle,
) -> Result<Vec<String>, 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();
}
+21 -24
View File
@@ -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<Vec<String>> {
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<Vec<BrowserVersionInfo>> {
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<BrowserVersionInfo> = 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<BrowserVersionsResult, Box<dyn std::error::Error + Send + Sync>> {
// 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<String> = 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<usize, Box<dyn std::error::Error + Send + Sync>> {
// Get existing cached versions
let existing_versions = self
.api_client()
.api_client
.load_cached_versions(browser)
.unwrap_or_default();
let existing_set: HashSet<String> = 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<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client()
.api_client
.fetch_firefox_releases_with_caching(no_caching)
.await
}
@@ -844,7 +841,7 @@ impl BrowserVersionService {
no_caching: bool,
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client()
.api_client
.fetch_firefox_developer_releases_with_caching(no_caching)
.await
}
@@ -862,7 +859,7 @@ impl BrowserVersionService {
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client()
.api_client
.fetch_mullvad_releases_with_caching(no_caching)
.await
}
@@ -886,7 +883,7 @@ impl BrowserVersionService {
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client()
.api_client
.fetch_zen_releases_with_caching(no_caching)
.await
}
@@ -904,7 +901,7 @@ impl BrowserVersionService {
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client()
.api_client
.fetch_brave_releases_with_caching(no_caching)
.await
}
@@ -922,7 +919,7 @@ impl BrowserVersionService {
no_caching: bool,
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client()
.api_client
.fetch_chromium_releases_with_caching(no_caching)
.await
}
@@ -940,7 +937,7 @@ impl BrowserVersionService {
no_caching: bool,
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client()
.api_client
.fetch_tor_releases_with_caching(no_caching)
.await
}
@@ -958,7 +955,7 @@ impl BrowserVersionService {
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client()
.api_client
.fetch_camoufox_releases_with_caching(no_caching)
.await
}
+33 -39
View File
@@ -120,18 +120,8 @@ pub struct CamoufoxNodecarLauncher {
inner: Arc<AsyncMutex<CamoufoxNodecarLauncherInner>>,
}
// 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<String>,
) -> Result<CamoufoxLaunchResult, String> {
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<String>,
) -> Result<CamoufoxLaunchResult, String> {
// 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();
}
+84 -47
View File
@@ -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<bool, String> {
#[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<bool, String> {
#[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(())
}
+10 -1
View File
@@ -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();
}
+102 -61
View File
@@ -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<String, HashMap<String, DownloadedBrowserInfo>>, // browser -> version -> info
}
pub struct DownloadedBrowsersRegistry {
data: Mutex<RegistryData>,
}
impl DownloadedBrowsersRegistry {
pub fn new() -> Self {
Self::default()
fn new() -> Self {
Self {
data: Mutex::new(RegistryData::default()),
}
}
pub fn load() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
pub fn instance() -> &'static DownloadedBrowsersRegistry {
&DOWNLOADED_BROWSERS_REGISTRY
}
pub fn load(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let registry_path = Self::get_registry_path()?;
if !registry_path.exists() {
return Ok(Self::new());
return Ok(());
}
let content = fs::read_to_string(&registry_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<dyn std::error::Error + Send + Sync>> {
@@ -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(&registry_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<DownloadedBrowserInfo> {
self.browsers.get_mut(browser)?.remove(version)
pub fn remove_browser(&self, browser: &str, version: &str) -> Option<DownloadedBrowserInfo> {
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<String> {
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<dyn std::error::Error + Send + Sync>> {
@@ -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<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
@@ -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<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
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<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
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<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
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"));
+22 -19
View File
@@ -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();
}
+10 -1
View File
@@ -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();
}
+3 -3
View File
@@ -204,7 +204,7 @@ pub async fn get_profile_groups() -> Result<Vec<ProfileGroup>, String> {
#[tauri::command]
pub async fn get_groups_with_profile_counts() -> Result<Vec<GroupWithCount>, 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<String>,
group_id: Option<String>,
) -> 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<String>) -> 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}"))
+4 -4
View File
@@ -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 {
+75 -33
View File
@@ -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<bool, Box<dyn std::error::Error + Send + Sync>> {
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();
}
+16 -10
View File
@@ -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<String, Box<dyn std::error::Error>> {
// 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<Vec<DetectedProfile>, 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();
}
+12 -3
View File
@@ -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();
}