use crate::browser_runner::BrowserProfile; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use tauri::AppHandle; use tauri_plugin_shell::ShellExt; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CamoufoxConfig { pub os: Option>, pub block_images: Option, pub block_webrtc: Option, pub block_webgl: Option, pub disable_coop: Option, pub geoip: Option, // Can be String or bool pub country: Option, pub timezone: Option, pub latitude: Option, pub longitude: Option, pub humanize: Option, pub humanize_duration: Option, pub headless: Option, pub locale: Option>, pub addons: Option>, pub fonts: Option>, pub custom_fonts_only: Option, pub exclude_addons: Option>, pub screen_min_width: Option, pub screen_max_width: Option, pub screen_min_height: Option, pub screen_max_height: Option, pub window_width: Option, pub window_height: Option, pub ff_version: Option, pub main_world_eval: Option, pub webgl_vendor: Option, pub webgl_renderer: Option, pub proxy: Option, pub enable_cache: Option, pub virtual_display: Option, pub debug: Option, pub additional_args: Option>, pub env_vars: Option>, pub firefox_prefs: Option>, } impl Default for CamoufoxConfig { fn default() -> Self { Self { os: None, block_images: None, block_webrtc: None, block_webgl: None, disable_coop: None, geoip: None, country: None, timezone: None, latitude: None, longitude: None, humanize: None, humanize_duration: None, headless: None, locale: None, addons: None, fonts: None, custom_fonts_only: None, exclude_addons: None, screen_min_width: None, screen_max_width: None, screen_min_height: None, screen_max_height: None, window_width: None, window_height: None, ff_version: None, main_world_eval: None, webgl_vendor: None, webgl_renderer: None, proxy: None, enable_cache: Some(true), // Cache enabled by default virtual_display: None, debug: None, additional_args: None, env_vars: None, firefox_prefs: None, } } } #[derive(Debug, Clone, Serialize, Deserialize)] #[allow(non_snake_case)] pub struct CamoufoxLaunchResult { pub id: String, pub pid: Option, #[serde(alias = "executable_path")] pub executablePath: String, #[serde(alias = "profile_path")] pub profilePath: String, pub url: Option, } pub struct CamoufoxLauncher { app_handle: AppHandle, } impl CamoufoxLauncher { pub fn new(app_handle: AppHandle) -> Self { Self { app_handle } } /// Launch Camoufox browser with the specified configuration pub async fn launch_camoufox( &self, executable_path: &str, profile_path: &str, config: &CamoufoxConfig, url: Option<&str>, ) -> Result> { println!("Launching Camoufox with executable: {executable_path}"); println!("Profile path: {profile_path}"); println!("URL: {url:?}"); // Use Tauri's sidecar to call nodecar let mut sidecar = self .app_handle .shell() .sidecar("nodecar") .map_err(|e| format!("Failed to create nodecar sidecar: {e}"))? .arg("camoufox") .arg("launch") .arg("--executable-path") .arg(executable_path) .arg("--profile-path") .arg(profile_path); // Add URL if provided if let Some(url) = url { sidecar = sidecar.arg("--url").arg(url); } // Add configuration options if let Some(os_list) = &config.os { sidecar = sidecar.arg("--os").arg(os_list.join(",")); } if config.block_images.unwrap_or(false) { sidecar = sidecar.arg("--block-images"); } if config.block_webrtc.unwrap_or(false) { sidecar = sidecar.arg("--block-webrtc"); } if config.block_webgl.unwrap_or(false) { sidecar = sidecar.arg("--block-webgl"); } if config.disable_coop.unwrap_or(false) { sidecar = sidecar.arg("--disable-coop"); } if let Some(geoip) = &config.geoip { match geoip { serde_json::Value::String(s) => { sidecar = sidecar.arg("--geoip").arg(s); } serde_json::Value::Bool(b) => { sidecar = sidecar .arg("--geoip") .arg(if *b { "auto" } else { "false" }); } _ => { sidecar = sidecar.arg("--geoip").arg(geoip.to_string()); } } } if let Some(country) = &config.country { sidecar = sidecar.arg("--country").arg(country); } if let Some(timezone) = &config.timezone { sidecar = sidecar.arg("--timezone").arg(timezone); } if let Some(latitude) = config.latitude { if let Some(longitude) = config.longitude { sidecar = sidecar.arg("--latitude").arg(latitude.to_string()); sidecar = sidecar.arg("--longitude").arg(longitude.to_string()); } } if let Some(humanize) = config.humanize { if humanize { if let Some(duration) = config.humanize_duration { sidecar = sidecar.arg("--humanize").arg(duration.to_string()); } else { sidecar = sidecar.arg("--humanize"); } } } if config.headless.unwrap_or(false) { sidecar = sidecar.arg("--headless"); } if let Some(locale_list) = &config.locale { sidecar = sidecar.arg("--locale").arg(locale_list.join(",")); } if let Some(addons_list) = &config.addons { sidecar = sidecar.arg("--addons").arg(addons_list.join(",")); } if let Some(fonts_list) = &config.fonts { sidecar = sidecar.arg("--fonts").arg(fonts_list.join(",")); } if config.custom_fonts_only.unwrap_or(false) { sidecar = sidecar.arg("--custom-fonts-only"); } if let Some(exclude_addons_list) = &config.exclude_addons { sidecar = sidecar .arg("--exclude-addons") .arg(exclude_addons_list.join(",")); } // Screen size configuration if let Some(width) = config.screen_min_width { sidecar = sidecar.arg("--screen-min-width").arg(width.to_string()); } if let Some(width) = config.screen_max_width { sidecar = sidecar.arg("--screen-max-width").arg(width.to_string()); } if let Some(height) = config.screen_min_height { sidecar = sidecar.arg("--screen-min-height").arg(height.to_string()); } if let Some(height) = config.screen_max_height { sidecar = sidecar.arg("--screen-max-height").arg(height.to_string()); } if let Some(width) = config.window_width { sidecar = sidecar.arg("--window-width").arg(width.to_string()); } if let Some(height) = config.window_height { sidecar = sidecar.arg("--window-height").arg(height.to_string()); } // Advanced options if let Some(ff_version) = config.ff_version { sidecar = sidecar.arg("--ff-version").arg(ff_version.to_string()); } if config.main_world_eval.unwrap_or(false) { sidecar = sidecar.arg("--main-world-eval"); } if let Some(vendor) = &config.webgl_vendor { if let Some(renderer) = &config.webgl_renderer { sidecar = sidecar.arg("--webgl-vendor").arg(vendor); sidecar = sidecar.arg("--webgl-renderer").arg(renderer); } } if let Some(proxy) = &config.proxy { sidecar = sidecar.arg("--proxy").arg(proxy); } // Cache is enabled by default, only add flag if disabled if !config.enable_cache.unwrap_or(true) { sidecar = sidecar.arg("--disable-cache"); } if let Some(virtual_display) = &config.virtual_display { sidecar = sidecar.arg("--virtual-display").arg(virtual_display); } if config.debug.unwrap_or(false) { sidecar = sidecar.arg("--debug"); } if let Some(args) = &config.additional_args { sidecar = sidecar.arg("--args").arg(args.join(",")); } if let Some(env_vars) = &config.env_vars { let env_json = serde_json::to_string(env_vars) .map_err(|e| format!("Failed to serialize environment variables: {e}"))?; sidecar = sidecar.arg("--env").arg(env_json); } if let Some(firefox_prefs) = &config.firefox_prefs { let prefs_json = serde_json::to_string(firefox_prefs) .map_err(|e| format!("Failed to serialize Firefox preferences: {e}"))?; sidecar = sidecar.arg("--firefox-prefs").arg(prefs_json); } // Execute the command println!("Executing nodecar command..."); let output = sidecar .output() .await .map_err(|e| format!("Failed to execute nodecar command: {e}"))?; // Check the command status first if !output.status.success() { let error_msg = String::from_utf8_lossy(&output.stderr); let stdout_msg = String::from_utf8_lossy(&output.stdout); return Err( format!( "Failed to launch Camoufox: Command failed with status {:?}\nstderr: {}\nstdout: {}", output.status, error_msg, stdout_msg ) .into(), ); } // Parse the JSON response let stdout = String::from_utf8_lossy(&output.stdout); println!("Nodecar stdout: {stdout}"); // Try to parse the JSON response let result: CamoufoxLaunchResult = serde_json::from_str(&stdout) .map_err(|e| format!("Failed to parse nodecar response as JSON: {e}\nResponse: {stdout}"))?; println!("Successfully launched Camoufox with ID: {}", result.id); Ok(result) } /// Stop a Camoufox process by ID pub async fn stop_camoufox( &self, id: &str, ) -> Result> { println!("Stopping Camoufox process with ID: {id}"); // First, we need to find the process to get its executable and profile paths let processes = self.list_camoufox_processes().await?; let target_process = processes.iter().find(|p| p.id == id); if let Some(process) = target_process { println!( "Found process to stop: executable={}, profile={}", process.executablePath, process.profilePath ); let sidecar = self .app_handle .shell() .sidecar("nodecar") .map_err(|e| format!("Failed to create nodecar sidecar: {e}"))? .arg("camoufox") .arg("stop") .arg("--executable-path") .arg(&process.executablePath) .arg("--profile-path") .arg(&process.profilePath) .arg("--id") .arg(id); let output = sidecar .output() .await .map_err(|e| format!("Failed to execute nodecar stop command: {e}"))?; if !output.status.success() { let error_msg = String::from_utf8_lossy(&output.stderr); let stdout_msg = String::from_utf8_lossy(&output.stdout); println!("Failed to stop Camoufox process - stderr: {error_msg}, stdout: {stdout_msg}"); return Err(format!("Failed to stop Camoufox process: {error_msg}").into()); } let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); println!("Stop command result: {stdout}"); // Parse the JSON response which contains a "success" field let response: serde_json::Value = serde_json::from_str(&stdout) .map_err(|e| format!("Failed to parse stop response as JSON: {e}\nResponse: {stdout}"))?; let success = response .get("success") .and_then(|v| v.as_bool()) .ok_or_else(|| { format!("Invalid response format - missing or invalid 'success' field: {stdout}") })?; if success { println!("Successfully stopped Camoufox process: {id}"); } else { println!("Failed to stop Camoufox process: {id} (process may not exist)"); } Ok(success) } else { println!("Camoufox process with ID {id} not found in running processes"); // If we can't find the process, it might already be stopped Ok(false) } } /// List all Camoufox processes pub async fn list_camoufox_processes( &self, ) -> Result, Box> { println!("Listing Camoufox processes..."); // For the list command, we need to provide dummy executable-path and profile-path // even though they're not used by the list action let sidecar = self .app_handle .shell() .sidecar("nodecar") .map_err(|e| format!("Failed to create nodecar sidecar: {e}"))? .arg("camoufox") .arg("list") .arg("--executable-path") .arg("/dummy/path") // Dummy path since list doesn't use it .arg("--profile-path") .arg("/dummy/profile"); // Dummy path since list doesn't use it let output = sidecar .output() .await .map_err(|e| format!("Failed to execute nodecar list command: {e}"))?; if !output.status.success() { let error_msg = String::from_utf8_lossy(&output.stderr); return Err(format!("Failed to list Camoufox processes: {error_msg}").into()); } let stdout = String::from_utf8_lossy(&output.stdout); println!("List command result: {stdout}"); // Parse the response as an array of process info let processes: Vec = serde_json::from_str(&stdout).map_err(|e| format!("Failed to parse list response: {e}"))?; // Convert to CamoufoxLaunchResult format let mut results = Vec::new(); for process in processes { // Handle both camelCase and snake_case formats from nodecar let id = process.get("id").and_then(|v| v.as_str()); // Try both formats for executable path let executable_path = process .get("executable_path") .and_then(|v| v.as_str()) .or_else(|| process.get("executablePath").and_then(|v| v.as_str())); // Try both formats for profile path let profile_path = process .get("profile_path") .and_then(|v| v.as_str()) .or_else(|| process.get("profilePath").and_then(|v| v.as_str())); if let (Some(id), Some(executable_path), Some(profile_path)) = (id, executable_path, profile_path) { let pid = process .get("pid") .and_then(|v| v.as_u64()) .map(|v| v as u32); let url = process .get("url") .and_then(|v| v.as_str()) .map(|s| s.to_string()); results.push(CamoufoxLaunchResult { id: id.to_string(), pid, executablePath: executable_path.to_string(), profilePath: profile_path.to_string(), url, }); } else { println!("Skipping malformed process entry: {process:?}"); } } println!("Parsed {} valid Camoufox processes", results.len()); Ok(results) } /// Find Camoufox process by profile path (for integration with browser_runner) pub async fn find_camoufox_by_profile( &self, profile_path: &str, ) -> Result, Box> { println!("Looking for Camoufox process with profile path: {profile_path}"); let processes = self.list_camoufox_processes().await?; println!("Found {} running Camoufox processes", processes.len()); for process in &processes { println!( "Checking process with profile path: {}", process.profilePath ); } // Convert both paths to canonical form for comparison let target_path = std::path::Path::new(profile_path) .canonicalize() .unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf()); for process in &processes { println!( "Comparing target path: {} with process path: {}", target_path.display(), process.profilePath ); // Try multiple comparison methods let process_path = std::path::Path::new(&process.profilePath) .canonicalize() .unwrap_or_else(|_| std::path::Path::new(&process.profilePath).to_path_buf()); // Method 1: Canonical path comparison if process_path == target_path { println!("Found match using canonical path comparison"); return Ok(Some(process.clone())); } // Method 2: Direct string comparison if process.profilePath == profile_path { println!("Found match using direct string comparison"); return Ok(Some(process.clone())); } // Method 3: Compare as strings after canonicalization if process_path.to_string_lossy() == target_path.to_string_lossy() { println!("Found match using canonical string comparison"); return Ok(Some(process.clone())); } // Method 4: Compare file names if full paths don't match if let (Some(process_file), Some(target_file)) = (process_path.file_name(), target_path.file_name()) { if process_file == target_file { // If the parent directories also match, it's likely the same profile if let (Some(process_parent), Some(target_parent)) = (process_path.parent(), target_path.parent()) { if process_parent == target_parent { println!("Found match using parent directory and file name comparison"); return Ok(Some(process.clone())); } } } } // Method 5: Check if either path contains the other (for symlinks or different representations) let process_path_str = process_path.to_string_lossy(); let target_path_str = target_path.to_string_lossy(); if process_path_str.contains(target_path_str.as_ref()) || target_path_str.contains(process_path_str.as_ref()) { println!("Found match using path containment check"); return Ok(Some(process.clone())); } } println!("No matching Camoufox process found for profile path: {profile_path}"); Ok(None) } } pub async fn launch_camoufox_profile( app_handle: AppHandle, profile: BrowserProfile, config: CamoufoxConfig, url: Option, ) -> Result { let launcher = CamoufoxLauncher::new(app_handle); // Get the executable path for Camoufox let browser_runner = crate::browser_runner::BrowserRunner::new(); let binaries_dir = browser_runner.get_binaries_dir(); let browser_dir = binaries_dir.join("camoufox").join(&profile.version); // Get executable path let browser = crate::browser::create_browser(crate::browser::BrowserType::Camoufox); let executable_path = browser .get_executable_path(&browser_dir) .map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?; // Get profile path let profiles_dir = browser_runner.get_profiles_dir(); let profile_path = profile.get_profile_data_path(&profiles_dir); launcher .launch_camoufox( &executable_path.to_string_lossy(), &profile_path.to_string_lossy(), &config, url.as_deref(), ) .await .map_err(|e| format!("Failed to launch Camoufox: {e}")) }