use crate::platform_browser; use crate::profile::{BrowserProfile, ProfileManager}; use crate::proxy_manager::PROXY_MANAGER; use directories::BaseDirs; use serde::Serialize; 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_manager::{ BrowserVersionInfo, BrowserVersionManager, BrowserVersionsResult, }; use crate::camoufox::CamoufoxConfig; use crate::download::DownloadProgress; use crate::downloaded_browsers::DownloadedBrowsersRegistry; // 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 { fn new() -> Self { Self { base_dirs: BaseDirs::new().expect("Failed to get base directories"), } } pub fn instance() -> &'static BrowserRunner { &BROWSER_RUNNER } // 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::instance(); profile_manager.get_profiles_dir() } /// Get the executable path for a browser profile /// This is a common helper to eliminate code duplication across the codebase pub fn get_browser_executable_path( &self, profile: &BrowserProfile, ) -> Result> { // Create browser instance to get executable path let browser_type = crate::browser::BrowserType::from_str(&profile.browser) .map_err(|e| format!("Invalid browser type: {e}"))?; let browser = crate::browser::create_browser(browser_type); // Construct browser directory path: binaries/// let mut browser_dir = self.get_binaries_dir(); browser_dir.push(&profile.browser); browser_dir.push(&profile.version); // Get platform-specific executable path browser .get_executable_path(&browser_dir) .map_err(|e| format!("Failed to get executable path for {}: {e}", profile.browser).into()) } /// 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}"))?; // 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); // 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::instance(); profile_manager.apply_proxy_settings_to_profile(profile_data_path, proxy, internal_proxy) } pub fn save_profile(&self, profile: &BrowserProfile) -> Result<(), Box> { let profile_manager = ProfileManager::instance(); let result = profile_manager.save_profile(profile); // Update tag suggestions after any save let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| { let _ = tm.rebuild_from_profiles(&self.list_profiles().unwrap_or_default()); }); result } pub fn list_profiles(&self) -> Result, Box> { let profile_manager = ProfileManager::instance(); profile_manager.list_profiles() } pub async fn launch_browser( &self, app_handle: tauri::AppHandle, profile: &BrowserProfile, url: Option, local_proxy_settings: Option<&ProxySettings>, ) -> Result> { self .launch_browser_internal(app_handle, profile, url, local_proxy_settings, None, false) .await } async fn launch_browser_internal( &self, app_handle: tauri::AppHandle, profile: &BrowserProfile, url: Option, local_proxy_settings: Option<&ProxySettings>, remote_debugging_port: Option, headless: bool, ) -> Result> { // Check if browser is disabled due to ongoing update let auto_updater = crate::auto_updater::AutoUpdater::instance(); if auto_updater.is_browser_disabled(&profile.browser)? { return Err( format!( "{} is currently being updated. Please wait for the update to complete.", profile.browser ) .into(), ); } // Handle camoufox profiles using nodecar launcher if profile.browser == "camoufox" { // Get or create camoufox config let mut camoufox_config = profile.camoufox_config.clone().unwrap_or_else(|| { println!( "No camoufox config found for profile {}, using default", profile.name ); crate::camoufox::CamoufoxConfig::default() }); // Always start a local proxy for Camoufox (for traffic monitoring and geoip support) let upstream_proxy = profile .proxy_id .as_ref() .and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id)); println!( "Starting local proxy for Camoufox profile: {} (upstream: {})", profile.name, upstream_proxy .as_ref() .map(|p| format!("{}:{}", p.host, p.port)) .unwrap_or_else(|| "DIRECT".to_string()) ); // Start the proxy and get local proxy settings let local_proxy = PROXY_MANAGER .start_proxy( app_handle.clone(), upstream_proxy.as_ref(), 0, // Use 0 as temporary PID, will be updated later Some(&profile.name), ) .await .map_err(|e| format!("Failed to start local proxy for Camoufox: {e}"))?; // Format proxy URL for camoufox - always use HTTP for the local proxy let proxy_url = format!("http://{}:{}", local_proxy.host, local_proxy.port); // Set proxy in camoufox config camoufox_config.proxy = Some(proxy_url); // Ensure geoip is always enabled for proper geolocation spoofing if camoufox_config.geoip.is_none() { camoufox_config.geoip = Some(serde_json::Value::Bool(true)); } println!( "Configured local proxy for Camoufox: {:?}, geoip: {:?}", camoufox_config.proxy, camoufox_config.geoip ); // Use the nodecar camoufox launcher println!( "Launching Camoufox via nodecar for profile: {}", profile.name ); let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::instance(); let camoufox_result = camoufox_launcher .launch_camoufox_profile_nodecar(app_handle.clone(), profile.clone(), camoufox_config, url) .await .map_err(|e| -> Box { format!("Failed to launch camoufox via nodecar: {e}").into() })?; // For server-based Camoufox, we use the process_id let process_id = camoufox_result.processId.unwrap_or(0); 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()); // Update the proxy manager with the correct PID if let Err(e) = PROXY_MANAGER.update_proxy_pid(0, process_id) { println!("Warning: Failed to update proxy PID mapping: {e}"); } else { println!("Updated proxy PID mapping from temp (0) to actual PID: {process_id}"); } // Save the updated profile self.save_process_info(&updated_profile)?; // Ensure tag suggestions include any tags from this profile let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| { let _ = tm.rebuild_from_profiles(&self.list_profiles().unwrap_or_default()); }); println!( "Updated profile with process info: {}", updated_profile.name ); println!( "Emitting profile events for successful Camoufox launch: {}", 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}"); } // Emit minimal running changed event to frontend with a small delay #[derive(Serialize)] struct RunningChangedPayload { id: String, is_running: bool, } let payload = RunningChangedPayload { id: updated_profile.id.to_string(), is_running: updated_profile.process_id.is_some(), }; if let Err(e) = app_handle.emit("profile-running-changed", &payload) { println!("Warning: Failed to emit profile running changed event: {e}"); } else { println!( "Successfully emitted profile-running-changed event for Camoufox {}: running={}", updated_profile.name, payload.is_running ); } return Ok(updated_profile); } // 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 using common helper let executable_path = self .get_browser_executable_path(profile) .expect("Failed to get executable path"); println!("Executable path: {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 } // Get stored proxy settings for later use (removed as we handle this in proxy startup) let _stored_proxy_settings = profile .proxy_id .as_ref() .and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id)); // Use provided local proxy for Chromium-based browsers launch arguments let proxy_for_launch_args: Option<&ProxySettings> = local_proxy_settings; // 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, remote_debugging_port, headless, ) .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: {} (ID: {})", launcher_pid, profile.name, profile.id ); // 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; } } } // On macOS, when launching via `open -a`, the child PID is the `open` helper. // Resolve and store the actual browser PID for all browser types. #[cfg(target_os = "macos")] { // Give the browser a moment to start tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await; let system = System::new_all(); 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(); for (pid, process) in system.processes() { let cmd = process.cmd(); if cmd.is_empty() { continue; } // Determine if this process matches the intended browser type let exe_name_lower = process.name().to_string_lossy().to_lowercase(); let is_correct_browser = match profile.browser.as_str() { "firefox" => { exe_name_lower.contains("firefox") && !exe_name_lower.contains("developer") && !exe_name_lower.contains("tor") && !exe_name_lower.contains("mullvad") && !exe_name_lower.contains("camoufox") } "firefox-developer" => { // More flexible detection for Firefox Developer Edition (exe_name_lower.contains("firefox") && exe_name_lower.contains("developer")) || (exe_name_lower.contains("firefox") && cmd.iter().any(|arg| { let arg_str = arg.to_str().unwrap_or(""); arg_str.contains("Developer") || arg_str.contains("developer") || arg_str.contains("FirefoxDeveloperEdition") || arg_str.contains("firefox-developer") })) || exe_name_lower == "firefox" // Firefox Developer might just show as "firefox" } "mullvad-browser" => { self.is_tor_or_mullvad_browser(&exe_name_lower, cmd, "mullvad-browser") } "tor-browser" => self.is_tor_or_mullvad_browser(&exe_name_lower, cmd, "tor-browser"), "zen" => exe_name_lower.contains("zen"), "chromium" => exe_name_lower.contains("chromium") || exe_name_lower.contains("chrome"), "brave" => exe_name_lower.contains("brave") || exe_name_lower.contains("Brave"), _ => false, }; if !is_correct_browser { continue; } // Check for profile path match let profile_path_match = if matches!( profile.browser.as_str(), "firefox" | "firefox-developer" | "tor-browser" | "mullvad-browser" | "zen" ) { // Firefox-based browsers: look for -profile argument followed by path let mut found_profile_arg = false; for (i, arg) in cmd.iter().enumerate() { if let Some(arg_str) = arg.to_str() { if arg_str == "-profile" && i + 1 < cmd.len() { if let Some(next_arg) = cmd.get(i + 1).and_then(|a| a.to_str()) { if next_arg == profile_data_path_str { found_profile_arg = true; break; } } } // Also check for combined -profile=path format if arg_str == format!("-profile={profile_data_path_str}") { found_profile_arg = true; break; } // Check if the argument is the profile path directly if arg_str == profile_data_path_str { found_profile_arg = true; break; } } } found_profile_arg } else { // Chromium-based browsers: look for --user-data-dir argument cmd.iter().any(|s| { if let Some(arg) = s.to_str() { arg == format!("--user-data-dir={profile_data_path_str}") || arg == profile_data_path_str } else { false } }) }; if profile_path_match { let pid_u32 = pid.as_u32(); if pid_u32 != launcher_pid { 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)?; let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| { let _ = tm.rebuild_from_profiles(&self.list_profiles().unwrap_or_default()); }); // 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 } println!( "Emitting profile events for successful launch: {} (ID: {})", updated_profile.name, updated_profile.id ); // 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}"); } // Emit minimal running changed event to frontend with a small delay to ensure UI consistency #[derive(Serialize)] struct RunningChangedPayload { id: String, is_running: bool, } let payload = RunningChangedPayload { id: updated_profile.id.to_string(), is_running: updated_profile.process_id.is_some(), }; if let Err(e) = app_handle.emit("profile-running-changed", &payload) { println!("Warning: Failed to emit profile running changed event: {e}"); } else { println!( "Successfully emitted profile-running-changed event for {}: running={}", updated_profile.name, payload.is_running ); } 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::instance(); // 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: {} (ID: {})", profile.name, profile.id ); // 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.id == profile.id) .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_browser_with_debugging( &self, app_handle: tauri::AppHandle, profile: &BrowserProfile, url: Option, remote_debugging_port: Option, headless: bool, ) -> Result> { // Always start a local proxy for API launches let mut internal_proxy_settings: Option = None; // Determine upstream proxy if configured; otherwise use DIRECT let upstream_proxy = profile .proxy_id .as_ref() .and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id)); // Use a temporary PID (1) to start the proxy, we'll update it after browser launch let temp_pid = 1u32; match PROXY_MANAGER .start_proxy( app_handle.clone(), upstream_proxy.as_ref(), temp_pid, Some(&profile.name), ) .await { Ok(internal_proxy) => { internal_proxy_settings = Some(internal_proxy.clone()); // For Firefox-based browsers, apply PAC/user.js to point to the local proxy if matches!( profile.browser.as_str(), "firefox" | "firefox-developer" | "zen" | "tor-browser" | "mullvad-browser" ) { let profiles_dir = self.get_profiles_dir(); let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); // Provide a dummy upstream (ignored when internal proxy is provided) let dummy_upstream = ProxySettings { proxy_type: "http".to_string(), host: "127.0.0.1".to_string(), port: internal_proxy.port, username: None, password: None, }; self .apply_proxy_settings_to_profile(&profile_path, &dummy_upstream, Some(&internal_proxy)) .map_err(|e| format!("Failed to update profile proxy: {e}"))?; } } Err(e) => { eprintln!("Failed to start local proxy (will launch without it): {e}"); } } let result = self .launch_browser_internal( app_handle.clone(), profile, url, internal_proxy_settings.as_ref(), remote_debugging_port, headless, ) .await; // Update proxy with correct PID if launch succeeded if let Ok(ref updated_profile) = result { if let Some(actual_pid) = updated_profile.process_id { let _ = PROXY_MANAGER.update_proxy_pid(temp_pid, actual_pid); } } result } pub async fn launch_or_open_url( &self, app_handle: tauri::AppHandle, profile: &BrowserProfile, url: Option, internal_proxy_settings: Option<&ProxySettings>, ) -> Result> { println!( "launch_or_open_url called for profile: {} (ID: {})", profile.name, profile.id ); // Get the most up-to-date profile data let profiles = self .list_profiles() .map_err(|e| format!("Failed to list profiles in launch_or_open_url: {e}"))?; let updated_profile = profiles .into_iter() .find(|p| p.id == profile.id) .unwrap_or_else(|| profile.clone()); println!( "Checking browser status for profile: {} (ID: {})", updated_profile.name, updated_profile.id ); // Check if browser is already running let is_running = self .check_browser_status(app_handle.clone(), &updated_profile) .await .map_err(|e| format!("Failed to check browser status: {e}"))?; // Get the updated profile again after status check (PID might have been updated) let profiles = self .list_profiles() .map_err(|e| format!("Failed to list profiles after status check: {e}"))?; let final_profile = profiles .into_iter() .find(|p| p.id == profile.id) .unwrap_or_else(|| updated_profile.clone()); println!( "Browser status check - Profile: {} (ID: {}), Running: {}, URL: {:?}, PID: {:?}", final_profile.name, final_profile.id, 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_internal(app_handle.clone(), &final_profile, url, internal_proxy_settings, None, false).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_internal( app_handle.clone(), &final_profile, url, internal_proxy_settings, None, false, ) .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 }) } /// Update a profile's browser version pub fn update_profile_version( &self, app_handle: &tauri::AppHandle, profile_id: &str, version: &str, ) -> Result> { let profile_manager = ProfileManager::instance(); profile_manager .update_profile_version(app_handle, profile_id, version) .map_err(|e| format!("Failed to update profile version: {e}").into()) } pub fn delete_profile( &self, app_handle: tauri::AppHandle, profile_id: &str, ) -> Result<(), Box> { let profile_manager = ProfileManager::instance(); profile_manager.delete_profile(&app_handle, profile_id)?; // 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}"); } // Rebuild tags after deletion let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| { let _ = tm.rebuild_from_profiles(&self.list_profiles().unwrap_or_default()); }); Ok(()) } pub async fn check_browser_status( &self, app_handle: tauri::AppHandle, profile: &BrowserProfile, ) -> Result> { let profile_manager = ProfileManager::instance(); 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::instance(); // 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: {} (ID: {})", profile.name, profile.id ); 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.processId ); 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.processId ); } else { println!( "Failed to stop Camoufox process: {} (PID: {:?})", camoufox_process.id, camoufox_process.processId ); } } Err(e) => { println!( "Error stopping Camoufox process {}: {}", camoufox_process.id, e ); } } } Ok(None) => { println!( "No running Camoufox process found for profile: {} (ID: {})", profile.name, profile.id ); } 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; // Check for pending updates and apply them for Camoufox profiles too let auto_updater = crate::auto_updater::AutoUpdater::instance(); if let Ok(Some(pending_update)) = auto_updater.get_pending_update(&profile.browser, &profile.version) { println!( "Found pending update for Camoufox profile {}: {} -> {}", profile.name, profile.version, pending_update.new_version ); // Update the profile to the new version match self.update_profile_version( &app_handle, &profile.id.to_string(), &pending_update.new_version, ) { Ok(updated_profile_after_update) => { println!( "Successfully updated Camoufox profile {} from version {} to {}", profile.name, profile.version, pending_update.new_version ); updated_profile = updated_profile_after_update; // Remove the pending update from the auto updater state if let Err(e) = auto_updater.dismiss_update_notification(&pending_update.id) { eprintln!("Warning: Failed to dismiss pending update notification: {e}"); } } Err(e) => { eprintln!( "Failed to apply pending update for Camoufox profile {}: {}", profile.name, e ); // Continue with the original profile update (just clearing process_id) } } } self .save_process_info(&updated_profile) .map_err(|e| format!("Failed to update profile: {e}"))?; println!( "Emitting profile events for successful Camoufox kill: {}", 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}"); } // Emit minimal running changed event to frontend immediately #[derive(Serialize)] struct RunningChangedPayload { id: String, is_running: bool, } let payload = RunningChangedPayload { id: updated_profile.id.to_string(), is_running: false, // Explicitly set to false since we just killed it }; if let Err(e) = app_handle.emit("profile-running-changed", &payload) { println!("Warning: Failed to emit profile running changed event: {e}"); } else { println!( "Successfully emitted profile-running-changed event for Camoufox {}: running={}", updated_profile.name, payload.is_running ); } println!( "Camoufox process cleanup completed for profile: {} (ID: {})", profile.name, profile.id ); return Ok(()); } // For non-camoufox browsers, use the existing logic let pid = if let Some(pid) = profile.process_id { // First verify the stored PID is still valid and belongs to our profile let system = System::new_all(); if let Some(process) = system.process(sysinfo::Pid::from(pid as usize)) { let cmd = process.cmd(); let exe_name = process.name().to_string_lossy(); // Verify this process is actually our browser 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" => { // More flexible detection for Firefox Developer Edition (exe_name.contains("firefox") && exe_name.contains("developer")) || (exe_name.contains("firefox") && cmd.iter().any(|arg| { let arg_str = arg.to_str().unwrap_or(""); arg_str.contains("Developer") || arg_str.contains("developer") || arg_str.contains("FirefoxDeveloperEdition") || arg_str.contains("firefox-developer") })) || exe_name == "firefox" // Firefox Developer might just show as "firefox" } "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") || exe_name.contains("chrome"), "brave" => exe_name.contains("brave") || exe_name.contains("Brave"), _ => false, }; if is_correct_browser { // Verify 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 = if matches!( profile.browser.as_str(), "firefox" | "firefox-developer" | "tor-browser" | "mullvad-browser" | "zen" ) { // Firefox-based browsers: look for -profile argument followed by path let mut found_profile_arg = false; for (i, arg) in cmd.iter().enumerate() { if let Some(arg_str) = arg.to_str() { if arg_str == "-profile" && i + 1 < cmd.len() { if let Some(next_arg) = cmd.get(i + 1).and_then(|a| a.to_str()) { if next_arg == profile_data_path_str { found_profile_arg = true; break; } } } // Also check for combined -profile=path format if arg_str == format!("-profile={profile_data_path_str}") { found_profile_arg = true; break; } // Check if the argument is the profile path directly if arg_str == profile_data_path_str { found_profile_arg = true; break; } } } found_profile_arg } else { // Chromium-based browsers: look for --user-data-dir argument cmd.iter().any(|s| { if let Some(arg) = s.to_str() { arg == format!("--user-data-dir={profile_data_path_str}") || arg == profile_data_path_str } else { false } }) }; if profile_path_match { println!( "Verified stored PID {} is valid for profile {} (ID: {})", pid, profile.name, profile.id ); pid } else { println!("Stored PID {} doesn't match profile path for {} (ID: {}), searching for correct process", pid, profile.name, profile.id); // Fall through to search for correct process self.find_browser_process_by_profile(profile)? } } else { println!("Stored PID {} doesn't match browser type for {} (ID: {}), searching for correct process", pid, profile.name, profile.id); // Fall through to search for correct process self.find_browser_process_by_profile(profile)? } } else { println!( "Stored PID {} is no longer valid for profile {} (ID: {}), searching for correct process", pid, profile.name, profile.id ); // Fall through to search for correct process self.find_browser_process_by_profile(profile)? } } else { // No stored PID, search for the process self.find_browser_process_by_profile(profile)? }; 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; // Check for pending updates and apply them let auto_updater = crate::auto_updater::AutoUpdater::instance(); if let Ok(Some(pending_update)) = auto_updater.get_pending_update(&profile.browser, &profile.version) { println!( "Found pending update for profile {}: {} -> {}", profile.name, profile.version, pending_update.new_version ); // Update the profile to the new version match self.update_profile_version( &app_handle, &profile.id.to_string(), &pending_update.new_version, ) { Ok(updated_profile_after_update) => { println!( "Successfully updated profile {} from version {} to {}", profile.name, profile.version, pending_update.new_version ); updated_profile = updated_profile_after_update; // Remove the pending update from the auto updater state if let Err(e) = auto_updater.dismiss_update_notification(&pending_update.id) { eprintln!("Warning: Failed to dismiss pending update notification: {e}"); } } Err(e) => { eprintln!( "Failed to apply pending update for profile {}: {}", profile.name, e ); // Continue with the original profile update (just clearing process_id) } } } self .save_process_info(&updated_profile) .map_err(|e| format!("Failed to update profile: {e}"))?; println!( "Emitting profile events for successful kill: {}", 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}"); } // Emit minimal running changed event to frontend immediately #[derive(Serialize)] struct RunningChangedPayload { id: String, is_running: bool, } let payload = RunningChangedPayload { id: updated_profile.id.to_string(), is_running: false, // Explicitly set to false since we just killed it }; if let Err(e) = app_handle.emit("profile-running-changed", &payload) { println!("Warning: Failed to emit profile running changed event: {e}"); } else { println!( "Successfully emitted profile-running-changed event for {}: running={}", updated_profile.name, payload.is_running ); } Ok(()) } /// Helper method to find browser process by profile path fn find_browser_process_by_profile( &self, profile: &BrowserProfile, ) -> Result> { let system = System::new_all(); 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(); println!( "Searching for {} browser process with profile path: {}", profile.browser, profile_data_path_str ); for (pid, process) in system.processes() { let cmd = process.cmd(); if cmd.is_empty() { continue; } // 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" => { // More flexible detection for Firefox Developer Edition (exe_name.contains("firefox") && exe_name.contains("developer")) || (exe_name.contains("firefox") && cmd.iter().any(|arg| { let arg_str = arg.to_str().unwrap_or(""); arg_str.contains("Developer") || arg_str.contains("developer") || arg_str.contains("FirefoxDeveloperEdition") || arg_str.contains("firefox-developer") })) || exe_name == "firefox" // Firefox Developer might just show as "firefox" } "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") || exe_name.contains("chrome"), "brave" => exe_name.contains("brave") || exe_name.contains("Brave"), _ => false, }; if !is_correct_browser { continue; } // Check for profile path match with improved logic let profile_path_match = if matches!( profile.browser.as_str(), "firefox" | "firefox-developer" | "tor-browser" | "mullvad-browser" | "zen" ) { // Firefox-based browsers: look for -profile argument followed by path let mut found_profile_arg = false; for (i, arg) in cmd.iter().enumerate() { if let Some(arg_str) = arg.to_str() { if arg_str == "-profile" && i + 1 < cmd.len() { if let Some(next_arg) = cmd.get(i + 1).and_then(|a| a.to_str()) { if next_arg == profile_data_path_str { found_profile_arg = true; break; } } } // Also check for combined -profile=path format if arg_str == format!("-profile={profile_data_path_str}") { found_profile_arg = true; break; } // Check if the argument is the profile path directly if arg_str == profile_data_path_str { found_profile_arg = true; break; } } } found_profile_arg } else { // Chromium-based browsers: look for --user-data-dir argument cmd.iter().any(|s| { if let Some(arg) = s.to_str() { arg == format!("--user-data-dir={profile_data_path_str}") || arg == profile_data_path_str } else { false } }) }; if profile_path_match { let pid_u32 = pid.as_u32(); println!( "Found matching {} browser process with PID: {} for profile: {} (ID: {})", profile.browser, pid_u32, profile.name, profile.id ); return Ok(pid_u32); } } Err( format!( "No running {} browser process found for profile: {} (ID: {})", profile.browser, profile.name, profile.id ) .into(), ) } /// 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) } /// Check if GeoIP database is missing for Camoufox profiles pub fn check_missing_geoip_database( &self, ) -> Result> { // Get all profiles let profiles = self .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; // Check if there are any Camoufox profiles let has_camoufox_profiles = profiles.iter().any(|profile| profile.browser == "camoufox"); if has_camoufox_profiles { // Check if GeoIP database is available use crate::geoip_downloader::GeoIPDownloader; return Ok(!GeoIPDownloader::is_geoip_database_available()); } Ok(false) } /// 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 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(", ") ); } } 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}"); } } } // Check if GeoIP database is missing for Camoufox profiles if self.check_missing_geoip_database()? { println!("GeoIP database is missing for Camoufox profiles, downloading..."); use crate::geoip_downloader::GeoIPDownloader; let geoip_downloader = GeoIPDownloader::instance(); match geoip_downloader.download_geoip_database(app_handle).await { Ok(_) => { downloaded.push("GeoIP database for Camoufox".to_string()); println!("GeoIP database downloaded successfully"); } Err(e) => { eprintln!("Failed to download GeoIP database: {e}"); // Don't fail the entire operation if GeoIP download fails } } } 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()); // 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) { 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 = BrowserVersionManager::instance(); 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_str); browser_dir.push(&version); 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 download module let downloader = crate::download::Downloader::instance(); // Attempt to download the archive. If the download fails but an archive with the // expected filename already exists (manual download), continue using that file. let download_path: PathBuf = match downloader .download_browser( &app_handle, browser_type.clone(), &version, &download_info, &browser_dir, ) .await { Ok(path) => path, Err(e) => { // Do NOT continue with extraction on failed downloads. Partial files may exist but are invalid. // Clean registry entry and stop here so the UI can show a single, clear error. let _ = registry.remove_browser(&browser_str, &version); let _ = registry.save(); let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); downloading.remove(&download_key); return Err(format!("Failed to download browser: {e}").into()); } }; // Use the extraction module if download_info.is_archive { let extractor = crate::extraction::Extractor::instance(); match extractor .extract_browser( &app_handle, browser_type.clone(), &version, &download_path, &browser_dir, ) .await { Ok(_) => { // Do not remove the archive here. We keep it until verification succeeds. } Err(e) => { // Do not remove the archive or extracted files. Just drop the registry entry // so it won't be reported as downloaded. let _ = registry.remove_browser(&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) { // Provide detailed error information for debugging let browser_dir = binaries_dir.join(&browser_str).join(&version); let mut error_details = format!( "Browser download completed but verification failed for {} {}. Expected directory: {}", browser_str, version, browser_dir.display() ); // List what files actually exist if browser_dir.exists() { error_details.push_str("\nFiles found in directory:"); if let Ok(entries) = std::fs::read_dir(&browser_dir) { for entry in entries.flatten() { let path = entry.path(); let file_type = if path.is_dir() { "DIR" } else { "FILE" }; error_details.push_str(&format!("\n {} {}", file_type, path.display())); } } else { error_details.push_str("\n (Could not read directory contents)"); } } else { error_details.push_str("\nDirectory does not exist!"); } // For Camoufox on Linux, provide specific expected files if browser_str == "camoufox" && cfg!(target_os = "linux") { let camoufox_subdir = browser_dir.join("camoufox"); error_details.push_str("\nExpected Camoufox executable locations:"); error_details.push_str(&format!("\n {}/camoufox-bin", camoufox_subdir.display())); error_details.push_str(&format!("\n {}/camoufox", camoufox_subdir.display())); if camoufox_subdir.exists() { error_details.push_str(&format!( "\nCamoufox subdirectory exists: {}", camoufox_subdir.display() )); if let Ok(entries) = std::fs::read_dir(&camoufox_subdir) { error_details.push_str("\nFiles in camoufox subdirectory:"); for entry in entries.flatten() { let path = entry.path(); let file_type = if path.is_dir() { "DIR" } else { "FILE" }; error_details.push_str(&format!("\n {} {}", file_type, path.display())); } } } else { error_details.push_str(&format!( "\nCamoufox subdirectory does not exist: {}", camoufox_subdir.display() )); } } // Do not delete files on verification failure; keep archive for manual retry. let _ = registry.remove_browser(&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(error_details.into()); } // Mark completion in registry. If it fails (e.g., rare race during cleanup), log but continue. if let Err(e) = registry.mark_download_completed(&browser_str, &version) { eprintln!("Warning: Could not mark {browser_str} {version} as completed in registry: {e}"); } registry .save() .map_err(|e| format!("Failed to save registry: {e}"))?; // Now that verification succeeded, remove the archive file if it exists if download_info.is_archive { let archive_path = browser_dir.join(&download_info.filename); if archive_path.exists() { if let Err(e) = std::fs::remove_file(&archive_path) { println!("Warning: Could not delete archive file after verification: {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::instance(); match geoip_downloader.download_geoip_database(&app_handle).await { Ok(_) => { println!("GeoIP database downloaded successfully"); } Err(e) => { eprintln!("Failed to download GeoIP database: {e}"); // Don't fail the browser download if GeoIP download fails } } } 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 { 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 } } files_exist } pub fn get_all_tags(&self) -> Result, Box> { let tag_manager = crate::tag_manager::TAG_MANAGER.lock().unwrap(); tag_manager.get_all_tags() } } #[allow(clippy::too_many_arguments)] #[tauri::command] pub async fn create_browser_profile_with_group( app_handle: tauri::AppHandle, name: String, browser: String, version: String, release_type: String, proxy_id: Option, camoufox_config: Option, group_id: Option, ) -> Result { let profile_manager = ProfileManager::instance(); profile_manager .create_profile_with_group( &app_handle, &name, &browser, &version, &release_type, proxy_id, camoufox_config, group_id, ) .await .map_err(|e| format!("Failed to create profile: {e}")) } #[tauri::command] pub fn list_browser_profiles() -> Result, String> { let profile_manager = ProfileManager::instance(); 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 { println!( "Launch request received for profile: {} (ID: {})", profile.name, profile.id ); let browser_runner = BrowserRunner::instance(); // Store the internal proxy settings for passing to launch_browser let mut internal_proxy_settings: Option = None; // Resolve the most up-to-date profile from disk by ID to avoid using stale proxy_id/browser state let profile_for_launch = match browser_runner .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}")) { Ok(profiles) => profiles .into_iter() .find(|p| p.id == profile.id) .unwrap_or_else(|| profile.clone()), Err(e) => { return Err(e); } }; println!( "Resolved profile for launch: {} (ID: {})", profile_for_launch.name, profile_for_launch.id ); // Always start a local proxy before launching (non-Camoufox handled here; Camoufox has its own flow) if profile.browser != "camoufox" { // Determine upstream proxy if configured; otherwise use DIRECT let upstream_proxy = profile_for_launch .proxy_id .as_ref() .and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id)); // Use a temporary PID (1) to start the proxy, we'll update it after browser launch let temp_pid = 1u32; match PROXY_MANAGER .start_proxy( app_handle.clone(), upstream_proxy.as_ref(), temp_pid, Some(&profile.name), ) .await { Ok(internal_proxy) => { // Use internal proxy for subsequent launch internal_proxy_settings = Some(internal_proxy.clone()); // For Firefox-based browsers, apply PAC/user.js to point to the local proxy if matches!( profile_for_launch.browser.as_str(), "firefox" | "firefox-developer" | "zen" | "tor-browser" | "mullvad-browser" ) { let profiles_dir = browser_runner.get_profiles_dir(); let profile_path = profiles_dir .join(profile_for_launch.id.to_string()) .join("profile"); // Provide a dummy upstream (ignored when internal proxy is provided) let dummy_upstream = ProxySettings { proxy_type: "http".to_string(), host: "127.0.0.1".to_string(), port: internal_proxy.port, username: None, password: None, }; browser_runner .apply_proxy_settings_to_profile(&profile_path, &dummy_upstream, Some(&internal_proxy)) .map_err(|e| format!("Failed to update profile proxy: {e}"))?; } println!( "Local proxy prepared for profile: {} on port: {} (upstream: {})", profile_for_launch.name, internal_proxy.port, upstream_proxy .as_ref() .map(|p| format!("{}:{}", p.host, p.port)) .unwrap_or_else(|| "DIRECT".to_string()) ); } Err(e) => { eprintln!("Failed to start local proxy (will launch without it): {e}"); } } } println!( "Starting browser launch for profile: {} (ID: {})", profile_for_launch.name, profile_for_launch.id ); // 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| { println!("Browser launch failed for profile: {}, error: {}", profile_for_launch.name, e); // Emit a failure event to clear loading states in the frontend #[derive(serde::Serialize)] struct RunningChangedPayload { id: String, is_running: bool, } let payload = RunningChangedPayload { id: profile_for_launch.id.to_string(), is_running: false, }; if let Err(e) = app_handle.emit("profile-running-changed", &payload) { println!("Warning: Failed to emit profile running changed event: {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}") })?; println!( "Browser launch completed for profile: {} (ID: {})", updated_profile.name, updated_profile.id ); // Now update the proxy with the correct PID if we have one if let Some(actual_pid) = updated_profile.process_id { // Update the proxy manager with the correct PID (we always started with temp pid 1 for non-Camoufox) let _ = PROXY_MANAGER.update_proxy_pid(1u32, actual_pid); } Ok(updated_profile) } #[tauri::command] pub async fn update_profile_proxy( app_handle: tauri::AppHandle, profile_id: String, proxy_id: Option, ) -> Result { let profile_manager = ProfileManager::instance(); profile_manager .update_profile_proxy(app_handle, &profile_id, proxy_id) .await .map_err(|e| format!("Failed to update profile: {e}")) } #[tauri::command] pub fn update_profile_tags( app_handle: tauri::AppHandle, profile_id: String, tags: Vec, ) -> Result { let profile_manager = ProfileManager::instance(); profile_manager .update_profile_tags(&app_handle, &profile_id, tags) .map_err(|e| format!("Failed to update profile tags: {e}")) } #[tauri::command] pub async fn check_browser_status( app_handle: tauri::AppHandle, profile: BrowserProfile, ) -> Result { let profile_manager = ProfileManager::instance(); 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, profile_id: String, new_name: String, ) -> Result { let profile_manager = ProfileManager::instance(); profile_manager .rename_profile(&app_handle, &profile_id, &new_name) .map_err(|e| format!("Failed to rename profile: {e}")) } #[tauri::command] pub async fn delete_profile( app_handle: tauri::AppHandle, profile_id: String, ) -> Result<(), String> { let browser_runner = BrowserRunner::instance(); browser_runner .delete_profile(app_handle, &profile_id) .map_err(|e| format!("Failed to delete profile: {e}")) } #[tauri::command] pub fn get_supported_browsers() -> Result, String> { let service = BrowserVersionManager::instance(); Ok(service.get_supported_browsers()) } #[tauri::command] pub fn is_browser_supported_on_platform(browser_str: String) -> Result { let service = BrowserVersionManager::instance(); 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 = BrowserVersionManager::instance(); // 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 = BrowserVersionManager::instance(); 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 = BrowserVersionManager::instance(); // 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 = BrowserVersionManager::instance(); 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::instance(); 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::instance(); browser_runner.is_browser_downloaded(&browser_str, &version) } #[tauri::command] pub fn get_all_tags() -> Result, String> { let browser_runner = BrowserRunner::instance(); browser_runner .get_all_tags() .map_err(|e| format!("Failed to get tags: {e}")) } #[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> { println!( "Kill request received for profile: {} (ID: {})", profile.name, profile.id ); let browser_runner = BrowserRunner::instance(); match browser_runner .kill_browser_process(app_handle.clone(), &profile) .await { Ok(()) => { println!( "Successfully killed browser profile: {} (ID: {})", profile.name, profile.id ); Ok(()) } Err(e) => { println!("Failed to kill browser profile {}: {}", profile.name, e); // Emit a failure event to clear loading states in the frontend #[derive(serde::Serialize)] struct RunningChangedPayload { id: String, is_running: bool, } // On kill failure, we assume the process is still running let payload = RunningChangedPayload { id: profile.id.to_string(), is_running: true, }; if let Err(e) = app_handle.emit("profile-running-changed", &payload) { println!("Warning: Failed to emit profile running changed event: {e}"); } Err(format!("Failed to kill browser: {e}")) } } } #[allow(clippy::too_many_arguments)] #[tauri::command] pub async fn create_browser_profile_new( app_handle: tauri::AppHandle, name: String, browser_str: String, version: String, release_type: String, proxy_id: Option, camoufox_config: Option, group_id: Option, ) -> Result { let browser_type = BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?; create_browser_profile_with_group( app_handle, name, browser_type.as_str().to_string(), version, release_type, proxy_id, camoufox_config, group_id, ) .await } #[tauri::command] pub async fn update_camoufox_config( app_handle: tauri::AppHandle, profile_id: String, config: CamoufoxConfig, ) -> Result<(), String> { let profile_manager = ProfileManager::instance(); profile_manager .update_camoufox_config(app_handle, &profile_id, 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 = BrowserVersionManager::instance(); 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::instance(); Ok(registry.get_downloaded_versions(&browser_str)) } #[tauri::command] pub async fn check_missing_binaries() -> Result, String> { let browser_runner = BrowserRunner::instance(); browser_runner .check_missing_binaries() .await .map_err(|e| format!("Failed to check missing binaries: {e}")) } #[tauri::command] pub async fn check_missing_geoip_database() -> Result { let browser_runner = BrowserRunner::instance(); browser_runner .check_missing_geoip_database() .map_err(|e| format!("Failed to check missing GeoIP database: {e}")) } #[tauri::command] pub async fn ensure_all_binaries_exist( app_handle: tauri::AppHandle, ) -> Result, String> { let browser_runner = BrowserRunner::instance(); browser_runner .ensure_all_binaries_exist(&app_handle) .await .map_err(|e| format!("Failed to ensure all binaries exist: {e}")) } pub async fn launch_browser_profile_with_debugging( app_handle: tauri::AppHandle, profile: BrowserProfile, url: Option, remote_debugging_port: Option, headless: bool, ) -> Result { let browser_runner = BrowserRunner::instance(); browser_runner .launch_browser_with_debugging(app_handle, &profile, url, remote_debugging_port, headless) .await .map_err(|e| format!("Failed to launch browser with debugging: {e}")) } #[cfg(test)] mod tests { use super::*; use tempfile::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::instance(); (browser_runner, temp_dir) } #[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")); } } // Global singleton instance lazy_static::lazy_static! { static ref BROWSER_RUNNER: BrowserRunner = BrowserRunner::new(); }