use crate::platform_browser; use crate::profile::{BrowserProfile, ProfileManager}; use crate::proxy_manager::PROXY_MANAGER; use directories::BaseDirs; use std::collections::HashSet; use std::fs::create_dir_all; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{SystemTime, UNIX_EPOCH}; use sysinfo::System; use tauri::Emitter; use crate::browser::{create_browser, BrowserType, ProxySettings}; use crate::browser_version_service::{ BrowserVersionInfo, BrowserVersionService, BrowserVersionsResult, }; use crate::camoufox::CamoufoxConfig; use crate::download::{DownloadProgress, Downloader}; use crate::downloaded_browsers::DownloadedBrowsersRegistry; use crate::extraction::Extractor; // Global state to track currently downloading browser-version pairs lazy_static::lazy_static! { static ref DOWNLOADING_BROWSERS: Arc>> = Arc::new(Mutex::new(HashSet::new())); } pub struct BrowserRunner { base_dirs: BaseDirs, } impl BrowserRunner { pub fn new() -> Self { Self { base_dirs: BaseDirs::new().expect("Failed to get base directories"), } } /// Migrate old profile structure to new UUID-based structure pub async fn migrate_profiles_to_uuid(&self) -> Result, Box> { let profile_manager = ProfileManager::new(); profile_manager.migrate_profiles_to_uuid().await } // Helper function to check if a process matches TOR/Mullvad browser fn is_tor_or_mullvad_browser( &self, exe_name: &str, cmd: &[std::ffi::OsString], browser_type: &str, ) -> bool { #[cfg(target_os = "macos")] return platform_browser::macos::is_tor_or_mullvad_browser(exe_name, cmd, browser_type); #[cfg(target_os = "windows")] return platform_browser::windows::is_tor_or_mullvad_browser(exe_name, cmd, browser_type); #[cfg(target_os = "linux")] return platform_browser::linux::is_tor_or_mullvad_browser(exe_name, cmd, browser_type); #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] { let _ = (exe_name, cmd, browser_type); false } } pub fn get_binaries_dir(&self) -> PathBuf { let mut path = self.base_dirs.data_local_dir().to_path_buf(); path.push(if cfg!(debug_assertions) { "DonutBrowserDev" } else { "DonutBrowser" }); path.push("binaries"); path } pub fn get_profiles_dir(&self) -> PathBuf { let profile_manager = ProfileManager::new(); profile_manager.get_profiles_dir() } /// Internal method to cleanup unused binaries (used by auto-cleanup) pub fn cleanup_unused_binaries_internal( &self, ) -> Result, Box> { // Load current profiles let profiles = self .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; // Load registry let mut registry = crate::downloaded_browsers::DownloadedBrowsersRegistry::load()?; // Get active browser versions (all profiles) let active_versions = registry.get_active_browser_versions(&profiles); // Get running browser versions (only running profiles) let running_versions = registry.get_running_browser_versions(&profiles); // Get binaries directory let binaries_dir = self.get_binaries_dir(); // Use comprehensive cleanup that syncs registry with disk and removes unused binaries let cleaned_up = registry.comprehensive_cleanup(&binaries_dir, &active_versions, &running_versions)?; // Registry is already saved by comprehensive_cleanup Ok(cleaned_up) } fn apply_proxy_settings_to_profile( &self, profile_data_path: &Path, proxy: &ProxySettings, internal_proxy: Option<&ProxySettings>, ) -> Result<(), Box> { let profile_manager = ProfileManager::new(); 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(); profile_manager.save_profile(profile) } pub fn list_profiles(&self) -> Result, Box> { let profile_manager = ProfileManager::new(); profile_manager.list_profiles() } pub async fn launch_browser( &self, app_handle: tauri::AppHandle, profile: &BrowserProfile, url: Option, local_proxy_settings: Option<&ProxySettings>, ) -> Result> { // Handle camoufox profiles using nodecar launcher if profile.browser == "camoufox" { if let Some(mut camoufox_config) = profile.camoufox_config.clone() { // Handle proxy settings for camoufox if let Some(proxy_id) = &profile.proxy_id { if let Some(stored_proxy) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id) { println!("Starting proxy for Camoufox profile: {}", profile.name); // Start the proxy and get local proxy settings let local_proxy = PROXY_MANAGER .start_proxy( app_handle.clone(), &stored_proxy, 0, // Use 0 as temporary PID, will be updated later Some(&profile.name), ) .await .map_err(|e| format!("Failed to start proxy for Camoufox: {e}"))?; // Format proxy URL for camoufox let proxy_url = format!( "{}://{}:{}", if stored_proxy.proxy_type == "socks5" || stored_proxy.proxy_type == "socks4" { &stored_proxy.proxy_type } else { "http" }, local_proxy.host, local_proxy.port ); // Add username and password if available let proxy_url = if let (Some(username), Some(password)) = (&stored_proxy.username, &stored_proxy.password) { format!( "{}://{}:{}@{}:{}", if stored_proxy.proxy_type == "socks5" || stored_proxy.proxy_type == "socks4" { &stored_proxy.proxy_type } else { "http" }, username, password, local_proxy.host, local_proxy.port ) } else { proxy_url }; // Set proxy in camoufox config camoufox_config.proxy = Some(proxy_url); println!("Configured proxy for Camoufox: {:?}", camoufox_config.proxy); } } // Use the existing config or create a test config if none exists let final_config = if camoufox_config.timezone.is_some() || camoufox_config.screen_min_width.is_some() || camoufox_config.window_width.is_some() { camoufox_config.clone() } else { // No meaningful config provided, use test config to ensure anti-fingerprinting works println!("No Camoufox configuration provided, using test configuration"); let mut test_config = crate::camoufox::CamoufoxNodecarLauncher::create_test_config(); // Preserve any proxy settings from the original config test_config.proxy = camoufox_config.proxy.clone(); test_config.headless = camoufox_config.headless; test_config.debug = Some(true); // Enable debug for troubleshooting test_config }; // Use the nodecar camoufox launcher println!( "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() })?; // For server-based Camoufox, we use the port as a unique identifier (which is actually the PID) let process_id = camoufox_result.port.unwrap_or(0); println!("Camoufox launched successfully with PID: {process_id}"); // Update profile with the process info from camoufox result let mut updated_profile = profile.clone(); updated_profile.process_id = Some(process_id); updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()); // Save the updated profile self.save_process_info(&updated_profile)?; println!( "Updated profile with process info: {}", updated_profile.name ); // Emit profile update event to frontend if let Err(e) = app_handle.emit("profile-updated", &updated_profile) { println!("Warning: Failed to emit profile update event: {e}"); } else { println!("Emitted profile update event for: {}", updated_profile.name); } return Ok(updated_profile); } else { return Err("Camoufox profile missing configuration".into()); } } // Create browser instance let browser_type = BrowserType::from_str(&profile.browser) .map_err(|_| format!("Invalid browser type: {}", profile.browser))?; let browser = create_browser(browser_type.clone()); // Get executable path - path structure: binaries/// let mut browser_dir = self.get_binaries_dir(); browser_dir.push(&profile.browser); browser_dir.push(&profile.version); println!("Browser directory: {browser_dir:?}"); let executable_path = browser .get_executable_path(&browser_dir) .expect("Failed to get executable path"); // Prepare the executable (set permissions, etc.) if let Err(e) = browser.prepare_executable(&executable_path) { println!("Warning: Failed to prepare executable: {e}"); // Continue anyway, the error might not be critical } // For Chromium browsers, use local proxy settings if available // For Firefox browsers, proxy settings are handled via PAC files let stored_proxy_settings = profile .proxy_id .as_ref() .and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id)); let proxy_for_launch_args = match browser_type { BrowserType::Chromium | BrowserType::Brave => { local_proxy_settings.or(stored_proxy_settings.as_ref()) } _ => None, // Firefox browsers use PAC files, not launch args }; // Get profile data path and launch arguments let profiles_dir = self.get_profiles_dir(); let profile_data_path = profile.get_profile_data_path(&profiles_dir); let browser_args = browser .create_launch_args( &profile_data_path.to_string_lossy(), proxy_for_launch_args, url, ) .expect("Failed to create launch arguments"); // Launch browser using platform-specific method let child = { #[cfg(target_os = "macos")] { platform_browser::macos::launch_browser_process(&executable_path, &browser_args).await? } #[cfg(target_os = "windows")] { platform_browser::windows::launch_browser_process(&executable_path, &browser_args).await? } #[cfg(target_os = "linux")] { platform_browser::linux::launch_browser_process(&executable_path, &browser_args).await? } #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] { return Err("Unsupported platform for browser launching".into()); } }; let launcher_pid = child.id(); println!( "Launched browser with launcher PID: {} for profile: {}", launcher_pid, profile.name ); // For TOR and Mullvad browsers, we need to find the actual browser process // because they use launcher scripts that spawn the real browser process let mut actual_pid = launcher_pid; if matches!( browser_type, BrowserType::TorBrowser | BrowserType::MullvadBrowser ) { // Wait a moment for the actual browser process to start tokio::time::sleep(tokio::time::Duration::from_millis(3000)).await; // Find the actual browser process let system = System::new_all(); for (pid, process) in system.processes() { let process_name = process.name().to_str().unwrap_or(""); let process_cmd = process.cmd(); let pid_u32 = pid.as_u32(); // Skip if this is the launcher process itself if pid_u32 == launcher_pid { continue; } if self.is_tor_or_mullvad_browser(process_name, process_cmd, &profile.browser) { println!( "Found actual {} browser process: PID {} ({})", profile.browser, pid_u32, process_name ); actual_pid = pid_u32; break; } } } // Update profile with process info and save let mut updated_profile = profile.clone(); updated_profile.process_id = Some(actual_pid); updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()); self.save_process_info(&updated_profile)?; // Apply proxy settings if needed (for Firefox-based browsers) if profile.proxy_id.is_some() && matches!( browser_type, BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen | BrowserType::TorBrowser | BrowserType::MullvadBrowser ) { // Proxy settings for Firefox-based browsers are applied via user.js file // which is already handled in the profile creation process } // Start proxy if configured and needed (for Chromium-based browsers) if let Some(proxy_id) = &profile.proxy_id { if let Some(stored_proxy) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id) { println!("Starting proxy for profile: {}", profile.name); match PROXY_MANAGER .start_proxy( app_handle.clone(), &stored_proxy, actual_pid, Some(&profile.name), ) .await { Ok(_) => println!("Proxy started successfully for profile: {}", profile.name), Err(e) => println!("Warning: Failed to start proxy: {e}"), } } } // Emit profile update event to frontend if let Err(e) = app_handle.emit("profile-updated", &updated_profile) { println!("Warning: Failed to emit profile update event: {e}"); } Ok(updated_profile) } pub async fn open_url_in_existing_browser( &self, app_handle: tauri::AppHandle, profile: &BrowserProfile, url: &str, _internal_proxy_settings: Option<&ProxySettings>, ) -> Result<(), Box> { // Handle camoufox profiles using nodecar launcher if profile.browser == "camoufox" { let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::new(app_handle.clone()); // Get the profile path based on the UUID 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(); // Check if the process is running match camoufox_launcher .find_camoufox_by_profile(&profile_path_str) .await { Ok(Some(_camoufox_process)) => { println!( "Opening URL in existing Camoufox process for profile: {}", profile.name ); // For Camoufox, we need to launch a new instance with the URL since it doesn't support remote commands // This is a limitation of Camoufox's architecture return Err("Camoufox doesn't support opening URLs in existing instances. Please close the browser and launch again with the URL.".into()); } Ok(None) => { return Err("Camoufox browser is not running".into()); } Err(e) => { return Err(format!("Error checking Camoufox process: {e}").into()); } } } // Use the comprehensive browser status check for non-camoufox browsers let is_running = self .check_browser_status(app_handle.clone(), profile) .await?; if !is_running { return Err("Browser is not running".into()); } // Get the updated profile with current PID let profiles = self.list_profiles().expect("Failed to list profiles"); let updated_profile = profiles .into_iter() .find(|p| p.name == profile.name) .unwrap_or_else(|| profile.clone()); // Ensure we have a valid process ID if updated_profile.process_id.is_none() { return Err("No valid process ID found for the browser".into()); } let browser_type = BrowserType::from_str(&updated_profile.browser) .map_err(|_| format!("Invalid browser type: {}", updated_profile.browser))?; // Get browser directory for all platforms - path structure: binaries/// let mut browser_dir = self.get_binaries_dir(); browser_dir.push(&updated_profile.browser); browser_dir.push(&updated_profile.version); match browser_type { BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen => { #[cfg(target_os = "macos")] { let profiles_dir = self.get_profiles_dir(); return platform_browser::macos::open_url_in_existing_browser_firefox_like( &updated_profile, url, browser_type, &browser_dir, &profiles_dir, ) .await; } #[cfg(target_os = "windows")] { let profiles_dir = self.get_profiles_dir(); return platform_browser::windows::open_url_in_existing_browser_firefox_like( &updated_profile, url, browser_type, &browser_dir, &profiles_dir, ) .await; } #[cfg(target_os = "linux")] { let profiles_dir = self.get_profiles_dir(); return platform_browser::linux::open_url_in_existing_browser_firefox_like( &updated_profile, url, browser_type, &browser_dir, &profiles_dir, ) .await; } #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] return Err("Unsupported platform".into()); } BrowserType::MullvadBrowser | BrowserType::TorBrowser => { #[cfg(target_os = "macos")] { let profiles_dir = self.get_profiles_dir(); return platform_browser::macos::open_url_in_existing_browser_tor_mullvad( &updated_profile, url, browser_type, &browser_dir, &profiles_dir, ) .await; } #[cfg(target_os = "windows")] { let profiles_dir = self.get_profiles_dir(); return platform_browser::windows::open_url_in_existing_browser_tor_mullvad( &updated_profile, url, browser_type, &browser_dir, &profiles_dir, ) .await; } #[cfg(target_os = "linux")] { let profiles_dir = self.get_profiles_dir(); return platform_browser::linux::open_url_in_existing_browser_tor_mullvad( &updated_profile, url, browser_type, &browser_dir, &profiles_dir, ) .await; } #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] return Err("Unsupported platform".into()); } BrowserType::Chromium | BrowserType::Brave => { #[cfg(target_os = "macos")] { let profiles_dir = self.get_profiles_dir(); return platform_browser::macos::open_url_in_existing_browser_chromium( &updated_profile, url, browser_type, &browser_dir, &profiles_dir, ) .await; } #[cfg(target_os = "windows")] { let profiles_dir = self.get_profiles_dir(); return platform_browser::windows::open_url_in_existing_browser_chromium( &updated_profile, url, browser_type, &browser_dir, &profiles_dir, ) .await; } #[cfg(target_os = "linux")] { let profiles_dir = self.get_profiles_dir(); return platform_browser::linux::open_url_in_existing_browser_chromium( &updated_profile, url, browser_type, &browser_dir, &profiles_dir, ) .await; } #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] return Err("Unsupported platform".into()); } BrowserType::Camoufox => { // This should never be reached due to the early return above, but handle it just in case Err("Camoufox URL opening should be handled in the early return above".into()) } } } pub async fn launch_or_open_url( &self, app_handle: tauri::AppHandle, profile: &BrowserProfile, url: Option, internal_proxy_settings: Option<&ProxySettings>, ) -> Result> { // Get the most up-to-date profile data let profiles = self.list_profiles().expect("Failed to list profiles"); let updated_profile = profiles .into_iter() .find(|p| p.name == profile.name) .unwrap_or_else(|| profile.clone()); // Check if browser is already running let is_running = self .check_browser_status(app_handle.clone(), &updated_profile) .await?; // Get the updated profile again after status check (PID might have been updated) let profiles = self.list_profiles().expect("Failed to list profiles"); let final_profile = profiles .into_iter() .find(|p| p.name == profile.name) .unwrap_or_else(|| updated_profile.clone()); println!( "Browser status check - Profile: {}, Running: {}, URL: {:?}, PID: {:?}", final_profile.name, is_running, url, final_profile.process_id ); if is_running && url.is_some() { // Browser is running and we have a URL to open if let Some(url_ref) = url.as_ref() { println!("Opening URL in existing browser: {url_ref}"); // For TOR/Mullvad browsers, add extra verification if matches!( final_profile.browser.as_str(), "tor-browser" | "mullvad-browser" ) { println!("TOR/Mullvad browser detected - ensuring we have correct PID"); if final_profile.process_id.is_none() { println!( "ERROR: No PID found for running TOR/Mullvad browser - this should not happen" ); return Err("No PID found for running browser".into()); } } match self .open_url_in_existing_browser( app_handle.clone(), &final_profile, url_ref, internal_proxy_settings, ) .await { Ok(()) => { println!("Successfully opened URL in existing browser"); Ok(final_profile) } Err(e) => { println!("Failed to open URL in existing browser: {e}"); // For Mullvad and Tor browsers, don't fall back to new instance since they use -no-remote // and can't have multiple instances with the same profile match final_profile.browser.as_str() { "mullvad-browser" | "tor-browser" => { Err(format!( "Failed to open URL in existing {} browser. Cannot launch new instance due to profile conflict: {}", final_profile.browser, e ).into()) } _ => { println!( "Falling back to new instance for browser: {}", final_profile.browser ); // Fallback to launching a new instance for other browsers self.launch_browser(app_handle.clone(), &final_profile, url, internal_proxy_settings).await } } } } } else { // This case shouldn't happen since we checked is_some() above, but handle it gracefully println!("URL was unexpectedly None, launching new browser instance"); self .launch_browser( app_handle.clone(), &final_profile, url, internal_proxy_settings, ) .await } } else { // Browser is not running or no URL provided, launch new instance if !is_running { println!("Launching new browser instance - browser not running"); } else { println!("Launching new browser instance - no URL provided"); } self .launch_browser( app_handle.clone(), &final_profile, url, internal_proxy_settings, ) .await } } fn save_process_info( &self, profile: &BrowserProfile, ) -> Result<(), Box> { // Use the regular save_profile method which handles the UUID structure self.save_profile(profile).map_err(|e| { let error_string = e.to_string(); Box::new(std::io::Error::other(error_string)) as Box }) } pub fn delete_profile(&self, profile_name: &str) -> Result<(), Box> { let profile_manager = ProfileManager::new(); profile_manager.delete_profile(profile_name)?; // Always perform cleanup after profile deletion to remove unused binaries if let Err(e) = self.cleanup_unused_binaries_internal() { println!("Warning: Failed to cleanup unused binaries: {e}"); } Ok(()) } pub async fn check_browser_status( &self, app_handle: tauri::AppHandle, profile: &BrowserProfile, ) -> Result> { let profile_manager = ProfileManager::new(); profile_manager .check_browser_status(app_handle, profile) .await } pub async fn kill_browser_process( &self, app_handle: tauri::AppHandle, profile: &BrowserProfile, ) -> Result<(), Box> { // Handle camoufox profiles using nodecar launcher if profile.browser == "camoufox" { let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::new(app_handle.clone()); // Search by profile path to find the running Camoufox 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(); println!( "Attempting to kill Camoufox process for profile: {}", profile.name ); match camoufox_launcher .find_camoufox_by_profile(&profile_path_str) .await { Ok(Some(camoufox_process)) => { println!( "Found Camoufox process: {} (PID: {:?})", camoufox_process.id, camoufox_process.port ); match camoufox_launcher .stop_camoufox(&app_handle, &camoufox_process.id) .await { Ok(stopped) => { if stopped { println!( "Successfully stopped Camoufox process: {} (PID: {:?})", camoufox_process.id, camoufox_process.port ); } else { println!( "Failed to stop Camoufox process: {} (PID: {:?})", camoufox_process.id, camoufox_process.port ); } } Err(e) => { println!( "Error stopping Camoufox process {}: {}", camoufox_process.id, e ); } } } Ok(None) => { println!( "No running Camoufox process found for profile: {}", profile.name ); } Err(e) => { println!( "Error finding Camoufox process for profile {}: {}", profile.name, e ); } } // Clear the process ID from the profile let mut updated_profile = profile.clone(); updated_profile.process_id = None; self .save_process_info(&updated_profile) .map_err(|e| format!("Failed to update profile: {e}"))?; // Emit profile update event to frontend if let Err(e) = app_handle.emit("profile-updated", &updated_profile) { println!("Warning: Failed to emit profile update event: {e}"); } println!( "Camoufox process cleanup completed for profile: {}", profile.name ); return Ok(()); } // For non-camoufox browsers, use the existing logic let pid = if let Some(pid) = profile.process_id { pid } else { // Try to find the process by searching all processes let system = System::new_all(); let mut found_pid: Option = None; for (pid, process) in system.processes() { let cmd = process.cmd(); if cmd.len() >= 2 { // Check if this is the right browser executable first let exe_name = process.name().to_string_lossy().to_lowercase(); let is_correct_browser = match profile.browser.as_str() { "firefox" => { exe_name.contains("firefox") && !exe_name.contains("developer") && !exe_name.contains("tor") && !exe_name.contains("mullvad") && !exe_name.contains("camoufox") } "firefox-developer" => exe_name.contains("firefox") && exe_name.contains("developer"), "mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "mullvad-browser"), "tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "tor-browser"), "zen" => exe_name.contains("zen"), "chromium" => exe_name.contains("chromium"), "brave" => exe_name.contains("brave"), // Camoufox is handled via nodecar, not PID-based checking _ => false, }; if !is_correct_browser { continue; } // Check for profile path match let profiles_dir = self.get_profiles_dir(); let profile_data_path = profile.get_profile_data_path(&profiles_dir); let profile_data_path_str = profile_data_path.to_string_lossy(); let profile_path_match = cmd.iter().any(|s| { let arg = s.to_str().unwrap_or(""); // For Firefox-based browsers, check for exact profile path match if profile.browser == "camoufox" { // Camoufox uses user_data_dir like Chromium browsers arg.contains(&format!("--user-data-dir={profile_data_path_str}")) || arg == profile_data_path_str } else if profile.browser == "tor-browser" || profile.browser == "firefox" || profile.browser == "firefox-developer" || profile.browser == "mullvad-browser" || profile.browser == "zen" { arg == profile_data_path_str || arg == format!("-profile={profile_data_path_str}") } else { // For Chromium-based browsers, check for user-data-dir arg.contains(&format!("--user-data-dir={profile_data_path_str}")) || arg == profile_data_path_str } }); if profile_path_match { found_pid = Some(pid.as_u32()); break; } } } found_pid.ok_or("Browser process not found")? }; println!("Attempting to kill browser process with PID: {pid}"); // Stop any associated proxy first if let Err(e) = PROXY_MANAGER.stop_proxy(app_handle.clone(), pid).await { println!("Warning: Failed to stop proxy for PID {pid}: {e}"); } // Kill the process using platform-specific implementation #[cfg(target_os = "macos")] platform_browser::macos::kill_browser_process_impl(pid).await?; #[cfg(target_os = "windows")] platform_browser::windows::kill_browser_process_impl(pid).await?; #[cfg(target_os = "linux")] platform_browser::linux::kill_browser_process_impl(pid).await?; #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] return Err("Unsupported platform".into()); // Clear the process ID from the profile let mut updated_profile = profile.clone(); updated_profile.process_id = None; self .save_process_info(&updated_profile) .map_err(|e| format!("Failed to update profile: {e}"))?; Ok(()) } /// Check if browser binaries exist for all profiles and return missing binaries pub async fn check_missing_binaries( &self, ) -> Result, Box> { // Get all profiles let profiles = self .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; let mut missing_binaries = Vec::new(); for profile in profiles { let browser_type = match BrowserType::from_str(&profile.browser) { Ok(bt) => bt, Err(_) => { println!( "Warning: Invalid browser type '{}' for profile '{}'", profile.browser, profile.name ); continue; } }; let browser = create_browser(browser_type.clone()); let binaries_dir = self.get_binaries_dir(); println!( "binaries_dir: {binaries_dir:?} for profile: {}", profile.name ); // Check if the version is downloaded if !browser.is_version_downloaded(&profile.version, &binaries_dir) { missing_binaries.push((profile.name, profile.browser, profile.version)); } } Ok(missing_binaries) } /// Automatically download missing binaries for all profiles pub async fn ensure_all_binaries_exist( &self, 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 missing_binaries = self.check_missing_binaries().await?; let mut downloaded = Vec::new(); for (profile_name, browser, version) in missing_binaries { println!("Downloading missing binary for profile '{profile_name}': {browser} {version}"); match self .download_browser_impl(app_handle.clone(), browser.clone(), version.clone()) .await { Ok(_) => { downloaded.push(format!( "{browser} {version} (for profile '{profile_name}')" )); } Err(e) => { eprintln!("Failed to download {browser} {version} for profile '{profile_name}': {e}"); } } } Ok(downloaded) } pub async fn download_browser_impl( &self, app_handle: tauri::AppHandle, browser_str: String, version: String, ) -> Result> { // Check if this browser-version pair is already being downloaded let download_key = format!("{browser_str}-{version}"); { let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); if downloading.contains(&download_key) { return Err(format!( "Browser '{browser_str}' version '{version}' is already being downloaded. Please wait for the current download to complete." ).into()); } // Mark this browser-version pair as being downloaded downloading.insert(download_key.clone()); } let browser_type = 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}"))?; // Check if registry thinks it's downloaded, but also verify files actually exist if registry.is_browser_downloaded(&browser_str, &version) { let binaries_dir = self.get_binaries_dir(); let actually_exists = browser.is_version_downloaded(&version, &binaries_dir); if actually_exists { return Ok(version); } else { // Registry says it's downloaded but files don't exist - clean up registry println!("Registry indicates {browser_str} {version} is downloaded, but files are missing. Cleaning up registry entry."); registry.remove_browser(&browser_str, &version); registry .save() .map_err(|e| format!("Failed to save cleaned registry: {e}"))?; } } // Check if browser is supported on current platform before attempting download let version_service = BrowserVersionService::new(); if !version_service .is_browser_supported(&browser_str) .unwrap_or(false) { return Err( format!( "Browser '{}' is not supported on your platform ({} {}). Supported browsers: {}", browser_str, std::env::consts::OS, std::env::consts::ARCH, version_service.get_supported_browsers().join(", ") ) .into(), ); } let download_info = version_service .get_download_info(&browser_str, &version) .map_err(|e| format!("Failed to get download info: {e}"))?; // Create browser directory let mut browser_dir = self.get_binaries_dir(); browser_dir.push(browser_type.as_str()); browser_dir.push(&version); // Clean up any failed previous download if let Err(e) = registry.cleanup_failed_download(&browser_str, &version) { println!("Warning: Failed to cleanup previous download: {e}"); } create_dir_all(&browser_dir).map_err(|e| format!("Failed to create browser directory: {e}"))?; // Mark download as started in registry registry.mark_download_started(&browser_str, &version, browser_dir.clone()); registry .save() .map_err(|e| format!("Failed to save registry: {e}"))?; // Use the new download module let downloader = Downloader::new(); let download_path = match downloader .download_browser( &app_handle, browser_type.clone(), &version, &download_info, &browser_dir, ) .await { Ok(path) => path, Err(e) => { // Clean up failed download let _ = registry.cleanup_failed_download(&browser_str, &version); let _ = registry.save(); // Remove browser-version pair from downloading set on error { let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); downloading.remove(&download_key); } return Err(format!("Failed to download browser: {e}").into()); } }; // Use the new extraction module if download_info.is_archive { let extractor = Extractor::new(); match extractor .extract_browser( &app_handle, browser_type.clone(), &version, &download_path, &browser_dir, ) .await { Ok(_) => { // Clean up the downloaded archive if let Err(e) = std::fs::remove_file(&download_path) { println!("Warning: Could not delete archive file: {e}"); } } Err(e) => { // Clean up failed download let _ = registry.cleanup_failed_download(&browser_str, &version); let _ = registry.save(); // Remove browser-version pair from downloading set on error { let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); downloading.remove(&download_key); } return Err(format!("Failed to extract browser: {e}").into()); } } // Give filesystem a moment to settle after extraction tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; } // Emit verification progress let progress = DownloadProgress { browser: browser_str.clone(), version: version.clone(), downloaded_bytes: 0, total_bytes: None, percentage: 100.0, speed_bytes_per_sec: 0.0, eta_seconds: None, stage: "verifying".to_string(), }; let _ = app_handle.emit("download-progress", &progress); // Verify the browser was downloaded correctly println!("Verifying download for browser: {browser_str}, version: {version}"); // Use the browser's own verification method let binaries_dir = self.get_binaries_dir(); if !browser.is_version_downloaded(&version, &binaries_dir) { let _ = registry.cleanup_failed_download(&browser_str, &version); let _ = registry.save(); // Remove browser-version pair from downloading set on verification failure { let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); downloading.remove(&download_key); } 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}"))?; registry .save() .map_err(|e| format!("Failed to save registry: {e}"))?; // If this is Camoufox, automatically download GeoIP database if browser_str == "camoufox" { use crate::geoip_downloader::GeoIPDownloader; // Check if GeoIP database is already available if !GeoIPDownloader::is_geoip_database_available() { println!("Downloading GeoIP database for Camoufox..."); let geoip_downloader = GeoIPDownloader::new(); 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 } else { println!("GeoIP database downloaded successfully"); } } else { println!("GeoIP database already available"); } } // Emit completion let progress = DownloadProgress { browser: browser_str.clone(), version: version.clone(), downloaded_bytes: 0, total_bytes: None, percentage: 100.0, speed_bytes_per_sec: 0.0, eta_seconds: Some(0.0), stage: "completed".to_string(), }; let _ = app_handle.emit("download-progress", &progress); // Remove browser-version pair from downloading set { let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); downloading.remove(&download_key); } Ok(version) } /// Check if a browser version is downloaded pub fn is_browser_downloaded(&self, browser_str: &str, version: &str) -> bool { // Always check if files actually exist on disk let browser_type = match BrowserType::from_str(browser_str) { Ok(bt) => bt, Err(_) => { println!("Invalid browser type: {browser_str}"); return false; } }; let browser = create_browser(browser_type.clone()); let binaries_dir = self.get_binaries_dir(); let files_exist = browser.is_version_downloaded(version, &binaries_dir); // 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 } } } files_exist } } #[tauri::command] pub fn create_browser_profile( name: String, browser: String, version: String, release_type: String, proxy_id: Option, camoufox_config: Option, ) -> Result { let profile_manager = ProfileManager::new(); profile_manager .create_profile( &name, &browser, &version, &release_type, proxy_id, camoufox_config, ) .map_err(|e| format!("Failed to create profile: {e}")) } #[tauri::command] pub fn list_browser_profiles() -> Result, String> { let profile_manager = ProfileManager::new(); profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}")) } #[tauri::command] pub async fn launch_browser_profile( app_handle: tauri::AppHandle, profile: BrowserProfile, url: Option, ) -> Result { let browser_runner = BrowserRunner::new(); // Store the internal proxy settings for passing to launch_browser let mut internal_proxy_settings: Option = None; // If the profile has proxy settings, we need to start the proxy first // and update the profile with proxy settings before launching let profile_for_launch = profile.clone(); if let Some(proxy_id) = &profile.proxy_id { if let Some(proxy) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id) { // Use a temporary PID (1) to start the proxy, we'll update it after browser launch let temp_pid = 1u32; // Start the proxy first match PROXY_MANAGER .start_proxy(app_handle.clone(), &proxy, temp_pid, Some(&profile.name)) .await { Ok(internal_proxy) => { let browser_runner = BrowserRunner::new(); let profiles_dir = browser_runner.get_profiles_dir(); let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); // Store the internal proxy settings for later use internal_proxy_settings = Some(internal_proxy.clone()); // Apply the proxy settings with the internal proxy to the profile directory browser_runner .apply_proxy_settings_to_profile(&profile_path, &proxy, Some(&internal_proxy)) .map_err(|e| format!("Failed to update profile proxy: {e}"))?; println!("Successfully started proxy for profile: {}", profile.name); // Give the proxy a moment to fully start up tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; } Err(e) => { eprintln!("Failed to start proxy: {e}"); // Still continue with browser launch, but without proxy let browser_runner = BrowserRunner::new(); let profiles_dir = browser_runner.get_profiles_dir(); let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); // Apply proxy settings without internal proxy browser_runner .apply_proxy_settings_to_profile(&profile_path, &proxy, None) .map_err(|e| format!("Failed to update profile proxy: {e}"))?; } } } } // Launch browser or open URL in existing instance let updated_profile = browser_runner .launch_or_open_url(app_handle.clone(), &profile_for_launch, url, internal_proxy_settings.as_ref()) .await .map_err(|e| { // Check if this is an architecture compatibility issue if let Some(io_error) = e.downcast_ref::() { if io_error.kind() == std::io::ErrorKind::Other && io_error.to_string().contains("Exec format error") { return format!("Failed to launch browser: Executable format error. This browser version is not compatible with your system architecture ({}). Please try a different browser or version that supports your platform.", std::env::consts::ARCH); } } format!("Failed to launch browser or open URL: {e}") })?; // Now update the proxy with the correct PID if we have one if let Some(proxy_id) = &profile.proxy_id { if PROXY_MANAGER.get_proxy_settings_by_id(proxy_id).is_some() { if let Some(actual_pid) = updated_profile.process_id { // Update the proxy manager with the correct PID match PROXY_MANAGER.update_proxy_pid(1u32, actual_pid) { Ok(()) => { println!("Updated proxy PID mapping from temp (1) to actual PID: {actual_pid}"); } Err(e) => { eprintln!("Failed to update proxy PID mapping: {e}"); } } } } } Ok(updated_profile) } #[tauri::command] pub async fn update_profile_proxy( app_handle: tauri::AppHandle, profile_name: String, proxy_id: Option, ) -> Result { let profile_manager = ProfileManager::new(); profile_manager .update_profile_proxy(app_handle, &profile_name, proxy_id) .await .map_err(|e| format!("Failed to update profile: {e}")) } #[tauri::command] pub fn update_profile_version( profile_name: String, version: String, ) -> Result { let profile_manager = ProfileManager::new(); profile_manager .update_profile_version(&profile_name, &version) .map_err(|e| format!("Failed to update profile version: {e}")) } #[tauri::command] pub async fn check_browser_status( app_handle: tauri::AppHandle, profile: BrowserProfile, ) -> Result { let profile_manager = ProfileManager::new(); profile_manager .check_browser_status(app_handle, &profile) .await .map_err(|e| format!("Failed to check browser status: {e}")) } #[tauri::command] pub fn rename_profile( _app_handle: tauri::AppHandle, old_name: &str, new_name: &str, ) -> Result { let profile_manager = ProfileManager::new(); profile_manager .rename_profile(old_name, new_name) .map_err(|e| format!("Failed to rename profile: {e}")) } #[tauri::command] pub fn delete_profile(_app_handle: tauri::AppHandle, profile_name: String) -> Result<(), String> { let browser_runner = BrowserRunner::new(); browser_runner .delete_profile(profile_name.as_str()) .map_err(|e| format!("Failed to delete profile: {e}")) } #[tauri::command] pub fn get_supported_browsers() -> Result, String> { let service = BrowserVersionService::new(); Ok(service.get_supported_browsers()) } #[tauri::command] pub fn is_browser_supported_on_platform(browser_str: String) -> Result { let service = BrowserVersionService::new(); service .is_browser_supported(&browser_str) .map_err(|e| format!("Failed to check browser support: {e}")) } #[tauri::command] pub async fn fetch_browser_versions_cached_first( browser_str: String, ) -> Result, String> { let service = BrowserVersionService::new(); // Get cached versions immediately if available if let Some(cached_versions) = service.get_cached_browser_versions_detailed(&browser_str) { // Check if we should update cache in background if service.should_update_cache(&browser_str) { // Start background update but return cached data immediately let service_clone = BrowserVersionService::new(); let browser_str_clone = browser_str.clone(); tokio::spawn(async move { if let Err(e) = service_clone .fetch_browser_versions_detailed(&browser_str_clone, false) .await { eprintln!("Background version update failed for {browser_str_clone}: {e}"); } }); } Ok(cached_versions) } else { // No cache available, fetch fresh service .fetch_browser_versions_detailed(&browser_str, false) .await .map_err(|e| format!("Failed to fetch detailed browser versions: {e}")) } } #[tauri::command] pub async fn fetch_browser_versions_with_count_cached_first( browser_str: String, ) -> Result { let service = BrowserVersionService::new(); // Get cached versions immediately if available if let Some(cached_versions) = service.get_cached_browser_versions(&browser_str) { // Check if we should update cache in background if service.should_update_cache(&browser_str) { // Start background update but return cached data immediately let service_clone = BrowserVersionService::new(); let browser_str_clone = browser_str.clone(); tokio::spawn(async move { if let Err(e) = service_clone .fetch_browser_versions_with_count(&browser_str_clone, false) .await { eprintln!("Background version update failed for {browser_str_clone}: {e}"); } }); } // Return cached data in the expected format Ok(BrowserVersionsResult { versions: cached_versions.clone(), new_versions_count: None, // No new versions when returning cached data total_versions_count: cached_versions.len(), }) } else { // No cache available, fetch fresh service .fetch_browser_versions_with_count(&browser_str, false) .await .map_err(|e| format!("Failed to fetch browser versions: {e}")) } } #[tauri::command] pub async fn download_browser( app_handle: tauri::AppHandle, browser_str: String, version: String, ) -> Result { let browser_runner = BrowserRunner::new(); browser_runner .download_browser_impl(app_handle, browser_str, version) .await .map_err(|e| format!("Failed to download browser: {e}")) } #[tauri::command] pub fn is_browser_downloaded(browser_str: String, version: String) -> bool { let browser_runner = BrowserRunner::new(); browser_runner.is_browser_downloaded(&browser_str, &version) } #[tauri::command] pub fn check_browser_exists(browser_str: String, version: String) -> bool { // This is an alias for is_browser_downloaded to provide clearer semantics for auto-updates is_browser_downloaded(browser_str, version) } #[tauri::command] pub async fn kill_browser_profile( app_handle: tauri::AppHandle, profile: BrowserProfile, ) -> Result<(), String> { let browser_runner = BrowserRunner::new(); browser_runner .kill_browser_process(app_handle, &profile) .await .map_err(|e| format!("Failed to kill browser: {e}")) } #[tauri::command] pub fn create_browser_profile_new( name: String, browser_str: String, version: String, release_type: String, proxy_id: Option, camoufox_config: Option, ) -> Result { let browser_type = BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?; create_browser_profile( name, browser_type.as_str().to_string(), version, release_type, proxy_id, camoufox_config, ) } #[tauri::command] pub async fn update_camoufox_config( app_handle: tauri::AppHandle, profile_name: String, config: CamoufoxConfig, ) -> Result<(), String> { let profile_manager = ProfileManager::new(); profile_manager .update_camoufox_config(app_handle, &profile_name, config) .await .map_err(|e| format!("Failed to update Camoufox config: {e}")) } #[tauri::command] pub async fn fetch_browser_versions_with_count( browser_str: String, ) -> Result { let service = BrowserVersionService::new(); service .fetch_browser_versions_with_count(&browser_str, false) .await .map_err(|e| format!("Failed to fetch browser versions: {e}")) } #[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}"))?; Ok(registry.get_downloaded_versions(&browser_str)) } #[tauri::command] pub async fn get_browser_release_types( browser_str: String, ) -> Result { let service = BrowserVersionService::new(); service .get_browser_release_types(&browser_str) .await .map_err(|e| format!("Failed to get release types: {e}")) } #[tauri::command] pub async fn check_missing_binaries() -> Result, String> { let browser_runner = BrowserRunner::new(); browser_runner .check_missing_binaries() .await .map_err(|e| format!("Failed to check missing binaries: {e}")) } #[tauri::command] pub async fn ensure_all_binaries_exist( app_handle: tauri::AppHandle, ) -> Result, String> { let browser_runner = BrowserRunner::new(); browser_runner .ensure_all_binaries_exist(&app_handle) .await .map_err(|e| format!("Failed to ensure all binaries exist: {e}")) } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; fn create_test_browser_runner() -> (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(); (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(); let binaries_dir = runner.get_binaries_dir(); assert!(binaries_dir.to_string_lossy().contains("DonutBrowser")); assert!(binaries_dir.to_string_lossy().contains("binaries")); } #[test] fn test_get_profiles_dir() { let (runner, _temp_dir) = create_test_browser_runner(); let profiles_dir = runner.get_profiles_dir(); 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); } }