diff --git a/.vscode/settings.json b/.vscode/settings.json index c68891b..1f17dae 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -112,6 +112,7 @@ "peerconnection", "pids", "pixbuf", + "pkill", "plasmohq", "platformdirs", "prefs", diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 854efcb..db91b4c 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -365,8 +365,8 @@ impl BrowserRunner { let launcher_pid = child.id(); println!( - "Launched browser with launcher PID: {} for profile: {}", - launcher_pid, profile.name + "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 @@ -432,16 +432,20 @@ impl BrowserRunner { && !exe_name_lower.contains("camoufox") } "firefox-developer" => { - exe_name_lower.contains("firefox") && exe_name_lower.contains("developer") - } - "mullvad-browser" => { - self.is_tor_or_mullvad_browser(&exe_name_lower, cmd, "mullvad-browser") + // 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"), - "brave" => exe_name_lower.contains("brave"), - // Camoufox uses nodecar, not PID-based here + "chromium" => exe_name_lower.contains("chromium") || exe_name_lower.contains("chrome"), + "brave" => exe_name_lower.contains("brave") || exe_name_lower.contains("Brave"), _ => false, }; @@ -449,30 +453,48 @@ impl BrowserRunner { continue; } - // Check for profile path match in command line args - let profile_path_match = cmd.iter().any(|s| { - let arg = s.to_str().unwrap_or(""); - 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, look for user-data-dir flag or raw path - arg.contains(&format!("--user-data-dir={profile_data_path_str}")) - || arg == profile_data_path_str + // 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; - println!( - "Resolved actual macOS browser PID: {actual_pid} (launcher PID: {launcher_pid})" - ); break; } } @@ -505,8 +527,8 @@ impl BrowserRunner { } println!( - "Emitting profile events for successful launch: {}", - updated_profile.name + "Emitting profile events for successful launch: {} (ID: {})", + updated_profile.name, updated_profile.id ); // Emit profile update event to frontend @@ -560,8 +582,8 @@ impl BrowserRunner { { Ok(Some(_camoufox_process)) => { println!( - "Opening URL in existing Camoufox process for profile: {}", - profile.name + "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 @@ -590,7 +612,7 @@ impl BrowserRunner { let profiles = self.list_profiles().expect("Failed to list profiles"); let updated_profile = profiles .into_iter() - .find(|p| p.name == profile.name) + .find(|p| p.id == profile.id) .unwrap_or_else(|| profile.clone()); // Ensure we have a valid process ID @@ -750,36 +772,22 @@ impl BrowserRunner { url: Option, internal_proxy_settings: Option<&ProxySettings>, ) -> Result> { - println!("launch_or_open_url called for profile: {}", profile.name); + 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.name == profile.name) - .unwrap_or_else(|| profile.clone()); + 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: {}", updated_profile.name); + 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))?; + 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.name == profile.name) - .unwrap_or_else(|| updated_profile.clone()); + 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: {}, Running: {}, URL: {:?}, PID: {:?}", - final_profile.name, is_running, url, final_profile.process_id - ); + 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 @@ -787,27 +795,14 @@ impl BrowserRunner { 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" - ) { + 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" - ); + 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 - { + 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) @@ -819,16 +814,10 @@ impl BrowserRunner { // 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()) + 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 - ); + 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 } @@ -838,14 +827,7 @@ impl BrowserRunner { } 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 + 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 @@ -854,14 +836,7 @@ impl BrowserRunner { } else { println!("Launching new browser instance - no URL provided"); } - self - .launch_browser( - app_handle.clone(), - &final_profile, - url, - internal_proxy_settings, - ) - .await + self.launch_browser(app_handle.clone(), &final_profile, url, internal_proxy_settings).await } } @@ -876,9 +851,9 @@ impl BrowserRunner { }) } - pub fn delete_profile(&self, profile_name: &str) -> Result<(), Box> { + pub fn delete_profile(&self, profile_id: &str) -> Result<(), Box> { let profile_manager = ProfileManager::instance(); - profile_manager.delete_profile(profile_name)?; + profile_manager.delete_profile(profile_id)?; // Always perform cleanup after profile deletion to remove unused binaries if let Err(e) = self.cleanup_unused_binaries_internal() { @@ -918,20 +893,14 @@ impl BrowserRunner { 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 - ); + 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 - ); + println!("Found Camoufox process: {} (PID: {:?})", camoufox_process.id, camoufox_process.processId); match camoufox_launcher .stop_camoufox(&app_handle, &camoufox_process.id) @@ -939,50 +908,30 @@ impl BrowserRunner { { Ok(stopped) => { if stopped { - println!( - "Successfully stopped Camoufox process: {} (PID: {:?})", - camoufox_process.id, camoufox_process.processId - ); + 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 - ); + println!("Failed to stop Camoufox process: {} (PID: {:?})", camoufox_process.id, camoufox_process.processId); } } Err(e) => { - println!( - "Error stopping Camoufox process {}: {}", - camoufox_process.id, e - ); + println!("Error stopping Camoufox process {}: {}", camoufox_process.id, e); } } } Ok(None) => { - println!( - "No running Camoufox process found for profile: {}", - profile.name - ); + 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 - ); + 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}"))?; + 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 - ); + 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) { @@ -1003,87 +952,112 @@ impl BrowserRunner { 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!("Successfully emitted profile-running-changed event for Camoufox {}: running={}", updated_profile.name, payload.is_running); } - println!( - "Camoufox process cleanup completed for profile: {}", - profile.name - ); + 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 { - pid - } else { - // Try to find the process by searching all processes + // First verify the stored PID is still valid and belongs to our profile let system = System::new_all(); - let mut found_pid: Option = None; - - for (pid, process) in system.processes() { + if let Some(process) = system.process(sysinfo::Pid::from(pid as usize)) { 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, - }; + let exe_name = process.name().to_string_lossy().to_lowercase(); - if !is_correct_browser { - continue; + // 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, + }; - // Check for profile path match + 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 = 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 + + 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 { - found_pid = Some(pid.as_u32()); - break; + 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)? } - - found_pid.ok_or("Browser process not found")? + } else { + // No stored PID, search for the process + self.find_browser_process_by_profile(profile)? }; println!("Attempting to kill browser process with PID: {pid}"); @@ -1109,14 +1083,9 @@ impl BrowserRunner { // 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}"))?; + 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 - ); + 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) { @@ -1137,43 +1106,130 @@ impl BrowserRunner { 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 - ); + 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 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 - ); + 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 - ); + println!("binaries_dir: {binaries_dir:?} for profile: {}", profile.name); // Check if the version is downloaded if !browser.is_version_downloaded(&profile.version, &binaries_dir) { @@ -1189,9 +1245,7 @@ impl BrowserRunner { &self, ) -> Result> { // Get all profiles - let profiles = self - .list_profiles() - .map_err(|e| format!("Failed to list profiles: {e}"))?; + 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"); @@ -1214,11 +1268,7 @@ impl BrowserRunner { 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(", ") - ); + println!("Cleaned up {} stale registry entries: {}", cleaned_up.len(), cleaned_up.join(", ")); } } @@ -1228,14 +1278,9 @@ impl BrowserRunner { 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 - { + match self.download_browser_impl(app_handle.clone(), browser.clone(), version.clone()).await { Ok(_) => { - downloaded.push(format!( - "{browser} {version} (for profile '{profile_name}')" - )); + downloaded.push(format!("{} {} (for profile '{profile_name}')", browser, version)); } Err(e) => { eprintln!("Failed to download {browser} {version} for profile '{profile_name}': {e}"); @@ -1248,6 +1293,7 @@ impl BrowserRunner { 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 { @@ -1276,16 +1322,13 @@ impl BrowserRunner { { 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()); + 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_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 @@ -1302,62 +1345,35 @@ impl BrowserRunner { // 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}"))?; + 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(), - ); + 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}"))?; + 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(&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}"))?; + 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 - { + 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. @@ -1373,16 +1389,7 @@ impl BrowserRunner { // 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 - { + 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. } @@ -1425,12 +1432,7 @@ impl BrowserRunner { 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() - ); + 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() { @@ -1456,10 +1458,7 @@ impl BrowserRunner { 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() - )); + 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() { @@ -1469,10 +1468,7 @@ impl BrowserRunner { } } } else { - error_details.push_str(&format!( - "\nCamoufox subdirectory does not exist: {}", - camoufox_subdir.display() - )); + error_details.push_str(&format!("\nCamoufox subdirectory does not exist: {}", camoufox_subdir.display())); } } @@ -1491,9 +1487,7 @@ impl BrowserRunner { 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}"))?; + 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 { @@ -1514,11 +1508,15 @@ impl BrowserRunner { println!("Downloading GeoIP database for Camoufox..."); let geoip_downloader = GeoIPDownloader::instance(); - if let Err(e) = geoip_downloader.download_geoip_database(&app_handle).await { - eprintln!("Warning: Failed to download GeoIP database: {e}"); - // Don't fail the browser download if GeoIP download fails - } else { - println!("GeoIP database downloaded successfully"); + + 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"); @@ -1622,62 +1620,40 @@ pub async fn launch_browser_profile( profile: BrowserProfile, url: Option, ) -> Result { - println!("Launch request received for profile: {}", profile.name); + 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 name 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.name == profile.name) - .unwrap_or_else(|| profile.clone()), + // 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: {}", profile_for_launch.name); + 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)); + 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 - { + 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" - ) { + 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"); + 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 { @@ -1688,20 +1664,10 @@ pub async fn launch_browser_profile( 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}"))?; + 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()) - ); + 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}"); @@ -1709,38 +1675,37 @@ pub async fn launch_browser_profile( } } - println!("Starting browser launch for profile: {}", profile_for_launch.name); + 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, - }; - let _ = app_handle.emit("profile-running-changed", &payload); + 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); - // 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}") - })?; + // 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, + }; - println!("Browser launch completed for profile: {}", updated_profile.name); + 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 { @@ -1754,25 +1719,20 @@ pub async fn launch_browser_profile( #[tauri::command] pub async fn update_profile_proxy( app_handle: tauri::AppHandle, - profile_name: String, + profile_id: String, proxy_id: Option, ) -> Result { let profile_manager = ProfileManager::instance(); - profile_manager - .update_profile_proxy(app_handle, &profile_name, proxy_id) - .await - .map_err(|e| format!("Failed to update profile: {e}")) + 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( - profile_name: String, + profile_id: String, tags: Vec, ) -> Result { let profile_manager = ProfileManager::instance(); - profile_manager - .update_profile_tags(&profile_name, tags) - .map_err(|e| format!("Failed to update profile tags: {e}")) + profile_manager.update_profile_tags(&profile_id, tags).map_err(|e| format!("Failed to update profile tags: {e}")) } #[tauri::command] @@ -1781,30 +1741,23 @@ pub async fn check_browser_status( 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}")) + 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, + old_id: &str, new_name: &str, ) -> Result { let profile_manager = ProfileManager::instance(); - profile_manager - .rename_profile(old_name, new_name) - .map_err(|e| format!("Failed to rename profile: {e}")) + profile_manager.rename_profile(old_id, 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> { +pub fn delete_profile(_app_handle: tauri::AppHandle, profile_id: String) -> Result<(), String> { let browser_runner = BrowserRunner::instance(); - browser_runner - .delete_profile(profile_name.as_str()) - .map_err(|e| format!("Failed to delete profile: {e}")) + browser_runner.delete_profile(profile_id.as_str()).map_err(|e| format!("Failed to delete profile: {e}")) } #[tauri::command] @@ -1816,9 +1769,7 @@ pub fn get_supported_browsers() -> Result, String> { #[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}")) + service.is_browser_supported(&browser_str).map_err(|e| format!("Failed to check browser support: {e}")) } #[tauri::command] @@ -1835,10 +1786,7 @@ pub async fn fetch_browser_versions_cached_first( 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 - { + 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}"); } }); @@ -1846,10 +1794,7 @@ pub async fn fetch_browser_versions_cached_first( 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}")) + service.fetch_browser_versions_detailed(&browser_str, false).await.map_err(|e| format!("Failed to fetch detailed browser versions: {e}")) } } @@ -1867,10 +1812,7 @@ pub async fn fetch_browser_versions_with_count_cached_first( 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 - { + 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}"); } }); @@ -1884,10 +1826,7 @@ pub async fn fetch_browser_versions_with_count_cached_first( }) } 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}")) + service.fetch_browser_versions_with_count(&browser_str, false).await.map_err(|e| format!("Failed to fetch browser versions: {e}")) } } @@ -1898,10 +1837,7 @@ pub async fn download_browser( 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}")) + browser_runner.download_browser_impl(app_handle, browser_str, version).await.map_err(|e| format!("Failed to download browser: {e}")) } #[tauri::command] @@ -1913,9 +1849,7 @@ pub fn is_browser_downloaded(browser_str: String, version: String) -> bool { #[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}")) + browser_runner.get_all_tags().map_err(|e| format!("Failed to get tags: {e}")) } #[tauri::command] @@ -1929,16 +1863,13 @@ pub async fn kill_browser_profile( app_handle: tauri::AppHandle, profile: BrowserProfile, ) -> Result<(), String> { - println!("Kill request received for profile: {}", profile.name); + 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 - { + + match browser_runner.kill_browser_process(app_handle.clone(), &profile).await { Ok(()) => { - println!("Successfully killed browser profile: {}", profile.name); + println!("Successfully killed browser profile: {} (ID: {})", profile.name, profile.id); Ok(()) } Err(e) => { @@ -1955,7 +1886,10 @@ pub async fn kill_browser_profile( id: profile.id.to_string(), is_running: true, }; - let _ = app_handle.emit("profile-running-changed", &payload); + + 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}")) } @@ -1974,32 +1908,18 @@ pub async fn create_browser_profile_new( 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 + 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_name: String, + profile_id: String, config: CamoufoxConfig, ) -> Result<(), String> { let profile_manager = ProfileManager::instance(); - profile_manager - .update_camoufox_config(app_handle, &profile_name, config) - .await - .map_err(|e| format!("Failed to update Camoufox config: {e}")) + profile_manager.update_camoufox_config(app_handle, &profile_id, config).await.map_err(|e| format!("Failed to update Camoufox config: {e}")) } #[tauri::command] @@ -2007,10 +1927,7 @@ 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}")) + service.fetch_browser_versions_with_count(&browser_str, false).await.map_err(|e| format!("Failed to fetch browser versions: {e}")) } #[tauri::command] @@ -2022,18 +1939,13 @@ pub fn get_downloaded_browser_versions(browser_str: String) -> Result Result, String> { let browser_runner = BrowserRunner::instance(); - browser_runner - .check_missing_binaries() - .await - .map_err(|e| format!("Failed to check missing binaries: {e}")) + 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}")) + browser_runner.check_missing_geoip_database().map_err(|e| format!("Failed to check missing GeoIP database: {e}")) } #[tauri::command] @@ -2041,10 +1953,7 @@ 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}")) + browser_runner.ensure_all_binaries_exist(&app_handle).await.map_err(|e| format!("Failed to ensure all binaries exist: {e}")) } #[cfg(test)] diff --git a/src-tauri/src/platform_browser.rs b/src-tauri/src/platform_browser.rs index f72d019..1483927 100644 --- a/src-tauri/src/platform_browser.rs +++ b/src-tauri/src/platform_browser.rs @@ -214,6 +214,207 @@ end try Ok(()) } + pub async fn kill_browser_process_impl( + pid: u32, + ) -> Result<(), Box> { + println!("Attempting to kill browser process with PID: {pid}"); + + // For Chromium-based browsers, use immediate aggressive termination + // Chromium browsers are notoriously difficult to kill on macOS due to process spawning + + // Step 1: Immediate SIGKILL on main process (no graceful shutdown for Chromium) + println!("Starting immediate SIGKILL for PID: {pid}"); + let _ = Command::new("kill") + .args(["-KILL", &pid.to_string()]) + .output(); + + // Step 2: Comprehensive process tree termination using multiple methods simultaneously + let _ = kill_chromium_process_tree_aggressive(pid).await; + + // Step 2.5: Nuclear option - kill all Chromium processes by name pattern + let _ = kill_all_chromium_processes_by_name().await; + + // Step 3: Use multiple kill strategies in parallel + let pid_str = pid.to_string(); + + // Kill by parent PID with SIGKILL + let _ = Command::new("pkill") + .args(["-KILL", "-P", &pid_str]) + .output(); + + // Kill by process group with SIGKILL + let _ = Command::new("pkill") + .args(["-KILL", "-g", &pid_str]) + .output(); + + // Kill by session ID + let _ = Command::new("pkill") + .args(["-KILL", "-s", &pid_str]) + .output(); + + // Wait briefly for initial termination + tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; + + // Step 4: Verify and retry with pattern-based killing for common Chromium process names + use sysinfo::{Pid, System}; + let system = System::new_all(); + + // Check if main process still exists + if system.process(Pid::from(pid as usize)).is_some() { + println!("Main process {pid} still running, using pattern-based termination"); + + // Kill by common Chromium process patterns + let chromium_patterns = [ + "Chrome", "Chromium", "Brave", "chrome", "chromium", "brave", + "Google Chrome", "Brave Browser", "Chrome Helper", "Chromium Helper" + ]; + + for pattern in &chromium_patterns { + let _ = Command::new("pkill") + .args(["-KILL", "-f", pattern]) + .output(); + } + } + + // Step 5: Final aggressive cleanup - kill any remaining processes + tokio::time::sleep(tokio::time::Duration::from_millis(300)).await; + + // One more round of comprehensive killing + let _ = Command::new("pkill") + .args(["-KILL", "-P", &pid_str]) + .output(); + + let _ = Command::new("pkill") + .args(["-KILL", "-g", &pid_str]) + .output(); + + // Final verification with extended wait + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + let system = System::new_all(); + + if system.process(Pid::from(pid as usize)).is_some() { + // Last resort: try system kill command with different signals + println!("Process {pid} extremely persistent, trying system-level termination"); + + let _ = Command::new("/bin/kill") + .args(["-KILL", &pid_str]) + .output(); + + let _ = Command::new("/usr/bin/killall") + .args(["-KILL", "-m", "Chrome"]) + .output(); + + let _ = Command::new("/usr/bin/killall") + .args(["-KILL", "-m", "Chromium"]) + .output(); + + let _ = Command::new("/usr/bin/killall") + .args(["-KILL", "-m", "Brave"]) + .output(); + + tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; + let system = System::new_all(); + + if system.process(Pid::from(pid as usize)).is_some() { + println!("WARNING: Process {pid} could not be terminated despite aggressive attempts"); + // Don't return error - let the UI update anyway since we tried everything + } + } + + println!("Aggressive browser termination completed for PID: {pid}"); + Ok(()) + } + +// Helper function to kill process tree (Chromium browsers often spawn child processes) +async fn kill_chromium_process_tree_aggressive(pid: u32) -> Result<(), Box> { + println!("Killing comprehensive process tree for PID: {pid}"); + + // Get all descendant processes using recursive process tree discovery + let descendant_pids = get_all_descendant_pids(pid).await; + println!("Found {} descendant processes to terminate", descendant_pids.len()); + + // Kill all descendants first (reverse order - children before parents) + for &desc_pid in descendant_pids.iter().rev() { + if desc_pid != pid { + println!("Terminating descendant process: {desc_pid}"); + let _ = Command::new("kill") + .args(["-KILL", &desc_pid.to_string()]) + .output(); + } + } + + // No delay for initial termination + + // Force kill any remaining descendants + for &desc_pid in descendant_pids.iter().rev() { + if desc_pid != pid { + let _ = Command::new("kill") + .args(["-KILL", &desc_pid.to_string()]) + .output(); + } + } + + // Also use pkill as a backup to catch any processes we might have missed + let _ = Command::new("pkill") + .args(["-KILL", "-P", &pid.to_string()]) + .output(); + + // On macOS, also try killing by process group for Chromium browsers + let _ = Command::new("pkill") + .args(["-KILL", "-g", &pid.to_string()]) + .output(); + + Ok(()) +} + +// Helper function to kill all Chromium-related processes by name patterns +async fn kill_all_chromium_processes_by_name() -> Result<(), Box> { + println!("Killing all Chromium-related processes by name patterns"); + + let chromium_patterns = [ + "Chrome", "Chromium", "Brave", "chrome", "chromium", "brave", + "Google Chrome", "Brave Browser", "Chrome Helper", "Chromium Helper" + ]; + + for pattern in &chromium_patterns { + let _ = Command::new("pkill") + .args(["-KILL", "-f", pattern]) + .output(); + } + + Ok(()) +} + +// Recursively find all descendant processes +async fn get_all_descendant_pids(parent_pid: u32) -> Vec { + use sysinfo::{Pid, System}; + + let system = System::new_all(); + let mut descendants = Vec::new(); + let mut to_check = vec![parent_pid]; + let mut checked = std::collections::HashSet::new(); + + while let Some(current_pid) = to_check.pop() { + if checked.contains(¤t_pid) { + continue; + } + checked.insert(current_pid); + + // Find direct children of current_pid + for (pid, process) in system.processes() { + let pid_u32 = pid.as_u32(); + if let Some(parent) = process.parent() { + if parent.as_u32() == current_pid && !checked.contains(&pid_u32) { + descendants.push(pid_u32); + to_check.push(pid_u32); + } + } + } + } + + descendants +} + pub async fn open_url_in_existing_browser_tor_mullvad( profile: &BrowserProfile, url: &str, @@ -455,39 +656,6 @@ end try Ok(()) } - - pub async fn kill_browser_process_impl( - pid: u32, - ) -> Result<(), Box> { - println!("Attempting to kill browser process with PID: {pid}"); - - // First try SIGTERM (graceful shutdown) - let output = Command::new("kill") - .args(["-TERM", &pid.to_string()]) - .output() - .map_err(|e| format!("Failed to execute kill command: {e}"))?; - - if !output.status.success() { - // If SIGTERM fails, try SIGKILL (force kill) - let output = Command::new("kill") - .args(["-KILL", &pid.to_string()]) - .output()?; - - if !output.status.success() { - return Err( - format!( - "Failed to kill process {}: {}", - pid, - String::from_utf8_lossy(&output.stderr) - ) - .into(), - ); - } - } - - println!("Successfully killed browser process with PID: {pid}"); - Ok(()) - } } #[cfg(target_os = "windows")] diff --git a/src/app/page.tsx b/src/app/page.tsx index 7907e7f..ceac63a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -159,6 +159,46 @@ export default function Home() { } }, []); + // Function to check and sync profile running states with actual process status + const syncProfileRunningStates = useCallback( + async (profiles: BrowserProfile[]) => { + try { + const statusChecks = profiles.map(async (profile) => { + try { + const isRunning = await invoke("check_browser_status", { + profile, + }); + return { id: profile.id, isRunning }; + } catch (error) { + console.error( + `Failed to check status for profile ${profile.name}:`, + error, + ); + return { id: profile.id, isRunning: false }; + } + }); + + const statuses = await Promise.all(statusChecks); + + // Update running profiles state based on actual status + setRunningProfiles((prev) => { + const next = new Set(prev); + statuses.forEach(({ id, isRunning }) => { + if (isRunning) { + next.add(id); + } else { + next.delete(id); + } + }); + return next; + }); + } catch (error) { + console.error("Failed to sync profile running states:", error); + } + }, + [], + ); + // Simple profiles loader without updates check (for use as callback) const loadProfiles = useCallback(async () => { try { @@ -167,13 +207,16 @@ export default function Home() { ); setProfiles(profileList); + // Check and sync profile running status after loading profiles + await syncProfileRunningStates(profileList); + // Check for missing binaries after loading profiles await checkMissingBinaries(); } catch (err: unknown) { console.error("Failed to load profiles:", err); setError(`Failed to load profiles: ${JSON.stringify(err)}`); } - }, [checkMissingBinaries]); + }, [checkMissingBinaries, syncProfileRunningStates]); const [processingUrls, setProcessingUrls] = useState>(new Set()); @@ -219,6 +262,9 @@ export default function Home() { ); setProfiles(profileList); + // Check and sync profile running status after loading profiles + await syncProfileRunningStates(profileList); + // Check for updates after loading profiles await checkForUpdates(); await checkMissingBinaries(); @@ -226,7 +272,7 @@ export default function Home() { console.error("Failed to load profiles:", err); setError(`Failed to load profiles: ${JSON.stringify(err)}`); } - }, [checkForUpdates, checkMissingBinaries]); + }, [checkForUpdates, checkMissingBinaries, syncProfileRunningStates]); useAppUpdateNotifications();