From 1e5664e3b219820f5e7b65f31d0352e9bb1be61c Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Tue, 19 Aug 2025 09:49:39 +0400 Subject: [PATCH] feat: launch browsers via api and expose them to selenium --- src-tauri/src/browser.rs | 125 ++++++++++++++++++++++++++++---- src-tauri/src/browser_runner.rs | 116 ++++++++++++++++++++++++++++- 2 files changed, 224 insertions(+), 17 deletions(-) diff --git a/src-tauri/src/browser.rs b/src-tauri/src/browser.rs index 064612a..9842c0b 100644 --- a/src-tauri/src/browser.rs +++ b/src-tauri/src/browser.rs @@ -58,6 +58,8 @@ pub trait Browser: Send + Sync { profile_path: &str, proxy_settings: Option<&ProxySettings>, url: Option, + remote_debugging_port: Option, + headless: bool, ) -> Result, Box>; fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool; fn prepare_executable(&self, executable_path: &Path) -> Result<(), Box>; @@ -557,11 +559,23 @@ impl Browser for FirefoxBrowser { profile_path: &str, _proxy_settings: Option<&ProxySettings>, url: Option, + remote_debugging_port: Option, + headless: bool, ) -> Result, Box> { let mut args = vec!["-profile".to_string(), profile_path.to_string()]; - // Only use -no-remote for browsers that require it for security (Mullvad, Tor) - // Regular Firefox browsers can use remote commands for better URL handling + // Add remote debugging if requested + if let Some(port) = remote_debugging_port { + args.push("--start-debugger-server".to_string()); + args.push(port.to_string()); + } + + // Add headless mode if requested + if headless { + args.push("--headless".to_string()); + } + + // Use -no-remote for browsers that require it for security (Mullvad, Tor) or when remote debugging match self.browser_type { BrowserType::MullvadBrowser | BrowserType::TorBrowser => { args.push("-no-remote".to_string()); @@ -570,7 +584,11 @@ impl Browser for FirefoxBrowser { | BrowserType::FirefoxDeveloper | BrowserType::Zen | BrowserType::Camoufox => { - // Don't use -no-remote so we can communicate with existing instances + // Use -no-remote when remote debugging to avoid conflicts + if remote_debugging_port.is_some() { + args.push("-no-remote".to_string()); + } + // Don't use -no-remote for normal launches so we can communicate with existing instances } _ => {} } @@ -659,6 +677,8 @@ impl Browser for ChromiumBrowser { profile_path: &str, proxy_settings: Option<&ProxySettings>, url: Option, + remote_debugging_port: Option, + headless: bool, ) -> Result, Box> { let mut args = vec![ format!("--user-data-dir={}", profile_path), @@ -670,9 +690,19 @@ impl Browser for ChromiumBrowser { "--disable-updater".to_string(), ]; + // Add remote debugging if requested + if let Some(port) = remote_debugging_port { + args.push("--remote-debugging-address=0.0.0.0".to_string()); + args.push(format!("--remote-debugging-port={port}")); + } + + // Add headless mode if requested + if headless { + args.push("--headless".to_string()); + } + // Add proxy configuration if provided if let Some(proxy) = proxy_settings { - // Apply proxy settings args.push(format!( "--proxy-server=http://{}:{}", proxy.host, proxy.port @@ -758,6 +788,8 @@ impl Browser for CamoufoxBrowser { profile_path: &str, _proxy_settings: Option<&ProxySettings>, url: Option, + remote_debugging_port: Option, + headless: bool, ) -> Result, Box> { // For Camoufox, we handle launching through the camoufox launcher // This method won't be used directly, but we provide basic Firefox args as fallback @@ -767,6 +799,17 @@ impl Browser for CamoufoxBrowser { "-no-remote".to_string(), ]; + // Add remote debugging if requested + if let Some(port) = remote_debugging_port { + args.push("--start-debugger-server".to_string()); + args.push(port.to_string()); + } + + // Add headless mode if requested + if headless { + args.push("--headless".to_string()); + } + if let Some(url) = url { args.push(url); } @@ -962,15 +1005,15 @@ mod tests { #[test] fn test_firefox_launch_args() { - // Test regular Firefox (should not use -no-remote) + // Test regular Firefox (should not use -no-remote for normal launch) let browser = FirefoxBrowser::new(BrowserType::Firefox); let args = browser - .create_launch_args("/path/to/profile", None, None) + .create_launch_args("/path/to/profile", None, None, None, false) .expect("Failed to create launch args for Firefox"); assert_eq!(args, vec!["-profile", "/path/to/profile"]); assert!( !args.contains(&"-no-remote".to_string()), - "Firefox should not use -no-remote" + "Firefox should not use -no-remote for normal launch" ); let args = browser @@ -978,6 +1021,8 @@ mod tests { "/path/to/profile", None, Some("https://example.com".to_string()), + None, + false, ) .expect("Failed to create launch args for Firefox with URL"); assert_eq!( @@ -985,29 +1030,55 @@ mod tests { vec!["-profile", "/path/to/profile", "https://example.com"] ); - // Test Mullvad Browser (should use -no-remote) + // Test Firefox with remote debugging (should use -no-remote) + let args = browser + .create_launch_args("/path/to/profile", None, None, Some(9222), false) + .expect("Failed to create launch args for Firefox with remote debugging"); + assert!( + args.contains(&"-no-remote".to_string()), + "Firefox should use -no-remote for remote debugging" + ); + assert!( + args.contains(&"--start-debugger-server".to_string()), + "Firefox should include debugger server arg" + ); + assert!( + args.contains(&"9222".to_string()), + "Firefox should include debugging port" + ); + + // Test Mullvad Browser (should always use -no-remote) let browser = FirefoxBrowser::new(BrowserType::MullvadBrowser); let args = browser - .create_launch_args("/path/to/profile", None, None) + .create_launch_args("/path/to/profile", None, None, None, false) .expect("Failed to create launch args for Mullvad Browser"); assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]); - // Test Tor Browser (should use -no-remote) + // Test Tor Browser (should always use -no-remote) let browser = FirefoxBrowser::new(BrowserType::TorBrowser); let args = browser - .create_launch_args("/path/to/profile", None, None) + .create_launch_args("/path/to/profile", None, None, None, false) .expect("Failed to create launch args for Tor Browser"); assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]); - // Test Zen Browser (should not use -no-remote) + // Test Zen Browser (should not use -no-remote for normal launch) let browser = FirefoxBrowser::new(BrowserType::Zen); let args = browser - .create_launch_args("/path/to/profile", None, None) + .create_launch_args("/path/to/profile", None, None, None, false) .expect("Failed to create launch args for Zen Browser"); assert_eq!(args, vec!["-profile", "/path/to/profile"]); assert!( !args.contains(&"-no-remote".to_string()), - "Zen Browser should not use -no-remote" + "Zen Browser should not use -no-remote for normal launch" + ); + + // Test headless mode + let args = browser + .create_launch_args("/path/to/profile", None, None, None, true) + .expect("Failed to create launch args for Zen Browser headless"); + assert!( + args.contains(&"--headless".to_string()), + "Browser should include headless flag when requested" ); } @@ -1015,7 +1086,7 @@ mod tests { fn test_chromium_launch_args() { let browser = ChromiumBrowser::new(BrowserType::Chromium); let args = browser - .create_launch_args("/path/to/profile", None, None) + .create_launch_args("/path/to/profile", None, None, None, false) .expect("Failed to create launch args for Chromium"); // Test that basic required arguments are present @@ -1043,6 +1114,8 @@ mod tests { "/path/to/profile", None, Some("https://example.com".to_string()), + None, + false, ) .expect("Failed to create launch args for Chromium with URL"); assert!( @@ -1055,6 +1128,28 @@ mod tests { args_with_url.last().expect("Args should not be empty"), "https://example.com" ); + + // Test remote debugging + let args_with_debug = browser + .create_launch_args("/path/to/profile", None, None, Some(9222), false) + .expect("Failed to create launch args for Chromium with remote debugging"); + assert!( + args_with_debug.contains(&"--remote-debugging-port=9222".to_string()), + "Chromium args should contain remote debugging port" + ); + assert!( + args_with_debug.contains(&"--remote-debugging-address=0.0.0.0".to_string()), + "Chromium args should contain remote debugging address" + ); + + // Test headless mode + let args_headless = browser + .create_launch_args("/path/to/profile", None, None, None, true) + .expect("Failed to create launch args for Chromium headless"); + assert!( + args_headless.contains(&"--headless".to_string()), + "Chromium args should contain headless flag when requested" + ); } #[test] diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 31995d5..c010d9b 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -161,6 +161,20 @@ impl BrowserRunner { 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(); @@ -336,6 +350,8 @@ impl BrowserRunner { &profile_data_path.to_string_lossy(), proxy_for_launch_args, url, + remote_debugging_port, + headless, ) .expect("Failed to create launch arguments"); @@ -774,6 +790,86 @@ impl BrowserRunner { } } + 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, @@ -863,7 +959,7 @@ impl BrowserRunner { _ => { 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 + self.launch_browser_internal(app_handle.clone(), &final_profile, url, internal_proxy_settings, None, false).await } } } @@ -888,11 +984,13 @@ impl BrowserRunner { println!("Launching new browser instance - no URL provided"); } self - .launch_browser( + .launch_browser_internal( app_handle.clone(), &final_profile, url, internal_proxy_settings, + None, + false, ) .await } @@ -2272,6 +2370,20 @@ pub async fn ensure_all_binaries_exist( .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::*;