use crate::browser_runner::BrowserRunner; use crate::profile::BrowserProfile; use reqwest::Client; use serde::{Deserialize, Serialize}; use serde_json::json; use std::collections::HashMap; use std::path::PathBuf; use std::process::Stdio; use std::sync::Arc; use std::time::Duration; use tauri::AppHandle; use tokio::process::Command as TokioCommand; use tokio::sync::Mutex as AsyncMutex; use tokio_tungstenite::{connect_async, tungstenite::Message}; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct WayfernConfig { #[serde(default)] pub fingerprint: Option, #[serde(default)] pub randomize_fingerprint_on_launch: Option, #[serde(default)] pub os: Option, #[serde(default)] pub screen_max_width: Option, #[serde(default)] pub screen_max_height: Option, #[serde(default)] pub screen_min_width: Option, #[serde(default)] pub screen_min_height: Option, #[serde(default)] pub geoip: Option, // For compatibility with shared config form #[serde(default)] pub block_images: Option, // For compatibility with shared config form #[serde(default)] pub block_webrtc: Option, #[serde(default)] pub block_webgl: Option, #[serde(default)] pub executable_path: Option, #[serde(default, skip_serializing)] pub proxy: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[allow(non_snake_case)] pub struct WayfernLaunchResult { pub id: String, #[serde(alias = "process_id")] pub processId: Option, #[serde(alias = "profile_path")] pub profilePath: Option, pub url: Option, pub cdp_port: Option, } #[derive(Debug)] struct WayfernInstance { #[allow(dead_code)] id: String, process_id: Option, profile_path: Option, url: Option, cdp_port: Option, } struct WayfernManagerInner { instances: HashMap, } pub struct WayfernManager { inner: Arc>, http_client: Client, } #[derive(Debug, Deserialize)] struct CdpTarget { #[serde(rename = "type")] target_type: String, #[serde(rename = "webSocketDebuggerUrl")] websocket_debugger_url: Option, } impl WayfernManager { fn new() -> Self { Self { inner: Arc::new(AsyncMutex::new(WayfernManagerInner { instances: HashMap::new(), })), http_client: Client::new(), } } pub fn instance() -> &'static WayfernManager { &WAYFERN_MANAGER } #[allow(dead_code)] pub fn get_profiles_dir(&self) -> PathBuf { crate::app_dirs::profiles_dir() } #[allow(dead_code)] fn get_binaries_dir(&self) -> PathBuf { crate::app_dirs::binaries_dir() } async fn find_free_port() -> Result> { let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; let port = listener.local_addr()?.port(); drop(listener); Ok(port) } /// Normalize fingerprint data from Wayfern CDP format to our storage format. /// Wayfern returns fields like fonts, webglParameters as JSON strings which we keep as-is. fn normalize_fingerprint(fingerprint: serde_json::Value) -> serde_json::Value { // Our storage format matches what Wayfern returns: // - fonts, plugins, mimeTypes, voices are JSON strings // - webglParameters, webgl2Parameters, etc. are JSON strings // The form displays them as JSON text areas, so no conversion needed. fingerprint } /// Denormalize fingerprint data from our storage format to Wayfern CDP format. /// Wayfern expects certain fields as JSON strings. fn denormalize_fingerprint(fingerprint: serde_json::Value) -> serde_json::Value { // Our storage format matches what Wayfern expects: // - fonts, plugins, mimeTypes, voices are JSON strings // - webglParameters, webgl2Parameters, etc. are JSON strings // So no conversion is needed fingerprint } async fn wait_for_cdp_ready( &self, port: u16, ) -> Result<(), Box> { let url = format!("http://127.0.0.1:{port}/json/version"); let max_attempts = 50; let delay = Duration::from_millis(100); for attempt in 0..max_attempts { match self.http_client.get(&url).send().await { Ok(resp) if resp.status().is_success() => { log::info!("CDP ready on port {port} after {attempt} attempts"); return Ok(()); } _ => { tokio::time::sleep(delay).await; } } } Err(format!("CDP not ready after {max_attempts} attempts on port {port}").into()) } async fn get_cdp_targets( &self, port: u16, ) -> Result, Box> { let url = format!("http://127.0.0.1:{port}/json"); let resp = self.http_client.get(&url).send().await?; let targets: Vec = resp.json().await?; Ok(targets) } async fn send_cdp_command( &self, ws_url: &str, method: &str, params: serde_json::Value, ) -> Result> { let (mut ws_stream, _) = connect_async(ws_url).await?; let command = json!({ "id": 1, "method": method, "params": params }); use futures_util::sink::SinkExt; use futures_util::stream::StreamExt; ws_stream .send(Message::Text(command.to_string().into())) .await?; while let Some(msg) = ws_stream.next().await { match msg? { Message::Text(text) => { let response: serde_json::Value = serde_json::from_str(text.as_str())?; if response.get("id") == Some(&json!(1)) { if let Some(error) = response.get("error") { return Err(format!("CDP error: {}", error).into()); } return Ok(response.get("result").cloned().unwrap_or(json!({}))); } } Message::Close(_) => break, _ => {} } } Err("No response received from CDP".into()) } pub async fn generate_fingerprint_config( &self, _app_handle: &AppHandle, profile: &BrowserProfile, config: &WayfernConfig, ) -> Result> { let executable_path = if let Some(path) = &config.executable_path { let p = PathBuf::from(path); if p.exists() { p } else { log::warn!("Stored Wayfern executable path does not exist: {path}, falling back to dynamic resolution"); BrowserRunner::instance() .get_browser_executable_path(profile) .map_err(|e| format!("Failed to get Wayfern executable path: {e}"))? } } else { BrowserRunner::instance() .get_browser_executable_path(profile) .map_err(|e| format!("Failed to get Wayfern executable path: {e}"))? }; let port = Self::find_free_port().await?; log::info!("Launching headless Wayfern on port {port} for fingerprint generation"); let temp_profile_dir = std::env::temp_dir().join(format!("wayfern_fingerprint_{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&temp_profile_dir)?; let mut cmd = TokioCommand::new(&executable_path); cmd .arg("--headless=new") .arg(format!("--remote-debugging-port={port}")) .arg("--remote-debugging-address=127.0.0.1") .arg(format!("--user-data-dir={}", temp_profile_dir.display())) .arg("--disable-gpu") .arg("--no-first-run") .arg("--no-default-browser-check") .arg("--disable-background-mode") .arg("--use-mock-keychain") .arg("--password-store=basic") .arg("--disable-features=DialMediaRouteProvider") .stdout(Stdio::null()) .stderr(Stdio::null()); let child = cmd.spawn()?; let child_id = child.id(); let cleanup = || async { if let Some(id) = child_id { #[cfg(unix)] { use nix::sys::signal::{kill, Signal}; use nix::unistd::Pid; let _ = kill(Pid::from_raw(id as i32), Signal::SIGTERM); } #[cfg(windows)] { use std::os::windows::process::CommandExt; const CREATE_NO_WINDOW: u32 = 0x08000000; let _ = std::process::Command::new("taskkill") .args(["/PID", &id.to_string(), "/F"]) .creation_flags(CREATE_NO_WINDOW) .output(); } } let _ = std::fs::remove_dir_all(&temp_profile_dir); }; if let Err(e) = self.wait_for_cdp_ready(port).await { cleanup().await; return Err(e); } let targets = match self.get_cdp_targets(port).await { Ok(t) => t, Err(e) => { cleanup().await; return Err(e); } }; let page_target = targets .iter() .find(|t| t.target_type == "page" && t.websocket_debugger_url.is_some()); let ws_url = match page_target { Some(target) => target.websocket_debugger_url.as_ref().unwrap().clone(), None => { cleanup().await; return Err("No page target found for CDP".into()); } }; let os = config .os .as_deref() .unwrap_or(if cfg!(target_os = "macos") { "macos" } else if cfg!(target_os = "linux") { "linux" } else { "windows" }); // Include wayfern token if available (enables cross-OS fingerprinting for paid users) let wayfern_token = crate::cloud_auth::CLOUD_AUTH.get_wayfern_token().await; let mut refresh_params = json!({ "operatingSystem": os }); if let Some(ref token) = wayfern_token { refresh_params .as_object_mut() .unwrap() .insert("wayfernToken".to_string(), json!(token)); } let refresh_result = self .send_cdp_command(&ws_url, "Wayfern.refreshFingerprint", refresh_params) .await; if let Err(e) = refresh_result { cleanup().await; return Err(format!("Failed to refresh fingerprint: {e}").into()); } let get_result = self .send_cdp_command(&ws_url, "Wayfern.getFingerprint", json!({})) .await; let fingerprint = match get_result { Ok(result) => { // Wayfern.getFingerprint returns { fingerprint: {...} } // We need to extract just the fingerprint object let fp = result.get("fingerprint").cloned().unwrap_or(result); // Normalize the fingerprint: convert JSON string fields to proper types let mut normalized = Self::normalize_fingerprint(fp); // Apply geolocation based on proxy IP or geoip config let geoip_option = config.geoip.as_ref(); let should_geolocate = match geoip_option { Some(serde_json::Value::Bool(false)) => false, _ => true, // Default to auto-detect }; if should_geolocate { let geo_result = async { let ip = match geoip_option { Some(serde_json::Value::String(ip_str)) => ip_str.clone(), _ => { // Auto-detect IP, optionally through proxy crate::ip_utils::fetch_public_ip(config.proxy.as_deref()) .await .map_err(|e| format!("Failed to fetch public IP: {e}"))? } }; crate::camoufox::geolocation::get_geolocation(&ip) .map_err(|e| format!("Failed to get geolocation for IP {ip}: {e}")) } .await; match geo_result { Ok(geo) => { if let Some(obj) = normalized.as_object_mut() { obj.insert("timezone".to_string(), json!(geo.timezone)); // Calculate timezone offset from IANA timezone name if let Ok(tz) = geo.timezone.parse::() { use chrono::Offset; let now = chrono::Utc::now().with_timezone(&tz); let offset_seconds = now.offset().fix().local_minus_utc(); let offset_minutes = -(offset_seconds / 60); obj.insert("timezoneOffset".to_string(), json!(offset_minutes)); } obj.insert("latitude".to_string(), json!(geo.latitude)); obj.insert("longitude".to_string(), json!(geo.longitude)); let locale_str = geo.locale.as_string(); obj.insert("language".to_string(), json!(&locale_str)); obj.insert( "languages".to_string(), json!([&locale_str, &geo.locale.language]), ); } log::info!( "Applied geolocation to Wayfern fingerprint: {} ({})", geo.locale.as_string(), geo.timezone ); } Err(e) => { log::warn!("Geolocation failed, using defaults: {e}"); if let Some(obj) = normalized.as_object_mut() { if !obj.contains_key("timezone") { obj.insert("timezone".to_string(), json!("America/New_York")); } if !obj.contains_key("timezoneOffset") { obj.insert("timezoneOffset".to_string(), json!(300)); } } } } } normalized } Err(e) => { cleanup().await; return Err(format!("Failed to get fingerprint: {e}").into()); } }; cleanup().await; let fingerprint_json = serde_json::to_string(&fingerprint) .map_err(|e| format!("Failed to serialize fingerprint: {e}"))?; log::info!( "Generated Wayfern fingerprint for OS: {}, fields: {:?}", os, fingerprint .as_object() .map(|o| o.keys().collect::>()) ); // Log timezone/geolocation fields specifically for debugging if let Some(obj) = fingerprint.as_object() { log::info!( "Generated fingerprint - timezone: {:?}, timezoneOffset: {:?}, latitude: {:?}, longitude: {:?}, language: {:?}", obj.get("timezone"), obj.get("timezoneOffset"), obj.get("latitude"), obj.get("longitude"), obj.get("language") ); } Ok(fingerprint_json) } #[allow(clippy::too_many_arguments)] pub async fn launch_wayfern( &self, _app_handle: &AppHandle, profile: &BrowserProfile, profile_path: &str, config: &WayfernConfig, url: Option<&str>, proxy_url: Option<&str>, ephemeral: bool, extension_paths: &[String], remote_debugging_port: Option, ) -> Result> { let executable_path = if let Some(path) = &config.executable_path { let p = PathBuf::from(path); if p.exists() { p } else { log::warn!("Stored Wayfern executable path does not exist: {path}, falling back to dynamic resolution"); BrowserRunner::instance() .get_browser_executable_path(profile) .map_err(|e| format!("Failed to get Wayfern executable path: {e}"))? } } else { BrowserRunner::instance() .get_browser_executable_path(profile) .map_err(|e| format!("Failed to get Wayfern executable path: {e}"))? }; let port = match remote_debugging_port { Some(p) => p, None => Self::find_free_port().await?, }; log::info!("Launching Wayfern on CDP port {port}"); let mut args = vec![ format!("--remote-debugging-port={port}"), "--remote-debugging-address=127.0.0.1".to_string(), format!("--user-data-dir={}", profile_path), "--no-first-run".to_string(), "--no-default-browser-check".to_string(), "--disable-background-mode".to_string(), "--disable-component-update".to_string(), "--disable-background-timer-throttling".to_string(), "--crash-server-url=".to_string(), "--disable-updater".to_string(), "--disable-session-crashed-bubble".to_string(), "--hide-crash-restore-bubble".to_string(), "--disable-infobars".to_string(), "--disable-quic".to_string(), "--disable-features=DialMediaRouteProvider".to_string(), "--use-mock-keychain".to_string(), "--password-store=basic".to_string(), ]; if let Some(proxy) = proxy_url { args.push(format!("--proxy-server={proxy}")); } if ephemeral { args.push("--disk-cache-size=1".to_string()); args.push("--disable-breakpad".to_string()); args.push("--disable-crash-reporter".to_string()); args.push("--no-service-autorun".to_string()); args.push("--disable-sync".to_string()); } if !extension_paths.is_empty() { args.push(format!("--load-extension={}", extension_paths.join(","))); } // Pass wayfern token as CLI flag so the browser can gate CDP features let wayfern_token = crate::cloud_auth::CLOUD_AUTH.get_wayfern_token().await; if let Some(ref token) = wayfern_token { args.push(format!("--wayfern-token={token}")); log::info!("Wayfern token passed as CLI flag (length: {})", token.len()); } else { log::warn!("No wayfern token available — CDP gated methods will be blocked"); } // Don't add URL to args - we'll navigate via CDP after setting fingerprint // This ensures fingerprint is applied at navigation commit time let mut cmd = TokioCommand::new(&executable_path); cmd.args(&args); cmd.stdout(Stdio::piped()); cmd.stderr(Stdio::piped()); let child = cmd.spawn()?; let process_id = child.id(); self.wait_for_cdp_ready(port).await?; // Get CDP targets first - needed for both fingerprint and navigation let targets = self.get_cdp_targets(port).await?; log::info!("Found {} CDP targets", targets.len()); let page_targets: Vec<_> = targets.iter().filter(|t| t.target_type == "page").collect(); log::info!("Found {} page targets", page_targets.len()); // Apply fingerprint if configured if let Some(fingerprint_json) = &config.fingerprint { log::info!( "Applying fingerprint to Wayfern browser, fingerprint length: {} chars", fingerprint_json.len() ); let stored_value: serde_json::Value = serde_json::from_str(fingerprint_json) .map_err(|e| format!("Failed to parse stored fingerprint JSON: {e}"))?; // The stored fingerprint should be the fingerprint object directly (after our fix in generate_fingerprint_config) // But for backwards compatibility, also handle the wrapped format let mut fingerprint = if stored_value.get("fingerprint").is_some() { // Old format: {"fingerprint": {...}} - extract the inner fingerprint stored_value.get("fingerprint").cloned().unwrap() } else { // New format: fingerprint object directly {...} stored_value.clone() }; // Add default timezone if not present (for profiles created before timezone was added) if let Some(obj) = fingerprint.as_object_mut() { if !obj.contains_key("timezone") { obj.insert("timezone".to_string(), json!("America/New_York")); log::info!("Added default timezone to fingerprint"); } if !obj.contains_key("timezoneOffset") { obj.insert("timezoneOffset".to_string(), json!(300)); log::info!("Added default timezoneOffset to fingerprint"); } } // Denormalize fingerprint for Wayfern CDP (convert arrays/objects to JSON strings) let mut fingerprint_for_cdp = Self::denormalize_fingerprint(fingerprint); // Normalize languages: if it's a comma-separated string, convert to array if let Some(obj) = fingerprint_for_cdp.as_object_mut() { if let Some(serde_json::Value::String(s)) = obj.get("languages").cloned() { let arr: Vec<&str> = s.split(',').map(|l| l.trim()).collect(); obj.insert("languages".to_string(), json!(arr)); } } log::info!( "Fingerprint prepared for CDP command, fields: {:?}", fingerprint_for_cdp .as_object() .map(|o| o.keys().collect::>()) ); // Log timezone and geolocation fields specifically for debugging if let Some(obj) = fingerprint_for_cdp.as_object() { log::info!( "Timezone/Geolocation fields - timezone: {:?}, timezoneOffset: {:?}, latitude: {:?}, longitude: {:?}, language: {:?}, languages: {:?}", obj.get("timezone"), obj.get("timezoneOffset"), obj.get("latitude"), obj.get("longitude"), obj.get("language"), obj.get("languages") ); } // Include wayfern token if available (enables cross-OS fingerprinting for paid users) let wayfern_token = crate::cloud_auth::CLOUD_AUTH.get_wayfern_token().await; let mut fingerprint_params = fingerprint_for_cdp.clone(); if let Some(ref token) = wayfern_token { if let Some(obj) = fingerprint_params.as_object_mut() { obj.insert("wayfernToken".to_string(), json!(token)); } } for target in &page_targets { if let Some(ws_url) = &target.websocket_debugger_url { log::info!("Applying fingerprint to target via WebSocket: {}", ws_url); // Wayfern.setFingerprint expects the fingerprint object directly, NOT wrapped match self .send_cdp_command(ws_url, "Wayfern.setFingerprint", fingerprint_params.clone()) .await { Ok(result) => log::info!( "Successfully applied fingerprint to page target: {:?}", result ), Err(e) => log::error!("Failed to apply fingerprint to target: {e}"), } } } } else { log::warn!("No fingerprint found in config, browser will use default fingerprint"); } // Set geolocation override via CDP so navigator.geolocation.getCurrentPosition() matches if let Some(fingerprint_json) = &config.fingerprint { if let Ok(fp) = serde_json::from_str::(fingerprint_json) { let fp_obj = if fp.get("fingerprint").is_some() { fp.get("fingerprint").unwrap() } else { &fp }; if let (Some(lat), Some(lng)) = ( fp_obj.get("latitude").and_then(|v| v.as_f64()), fp_obj.get("longitude").and_then(|v| v.as_f64()), ) { let accuracy = fp_obj .get("accuracy") .and_then(|v| v.as_f64()) .unwrap_or(100.0); if let Some(target) = page_targets.first() { if let Some(ws_url) = &target.websocket_debugger_url { let _ = self .send_cdp_command( ws_url, "Emulation.setGeolocationOverride", json!({ "latitude": lat, "longitude": lng, "accuracy": accuracy }), ) .await; log::info!("Set geolocation override: lat={lat}, lng={lng}"); } } } } } // Navigate to URL via CDP - fingerprint will be applied at navigation commit time if let Some(url) = url { log::info!("Navigating to URL via CDP: {}", url); if let Some(target) = page_targets.first() { if let Some(ws_url) = &target.websocket_debugger_url { match self .send_cdp_command(ws_url, "Page.navigate", json!({ "url": url })) .await { Ok(_) => log::info!("Successfully navigated to URL: {}", url), Err(e) => log::error!("Failed to navigate to URL: {e}"), } } } } let id = uuid::Uuid::new_v4().to_string(); let instance = WayfernInstance { id: id.clone(), process_id, profile_path: Some(profile_path.to_string()), url: url.map(|s| s.to_string()), cdp_port: Some(port), }; let mut inner = self.inner.lock().await; inner.instances.insert(id.clone(), instance); Ok(WayfernLaunchResult { id, processId: process_id, profilePath: Some(profile_path.to_string()), url: url.map(|s| s.to_string()), cdp_port: Some(port), }) } pub async fn stop_wayfern( &self, id: &str, ) -> Result<(), Box> { let mut inner = self.inner.lock().await; if let Some(instance) = inner.instances.remove(id) { if let Some(pid) = instance.process_id { #[cfg(unix)] { use nix::sys::signal::{kill, Signal}; use nix::unistd::Pid; let _ = kill(Pid::from_raw(pid as i32), Signal::SIGTERM); } #[cfg(windows)] { use std::os::windows::process::CommandExt; const CREATE_NO_WINDOW: u32 = 0x08000000; let _ = std::process::Command::new("taskkill") .args(["/PID", &pid.to_string(), "/F"]) .creation_flags(CREATE_NO_WINDOW) .output(); } log::info!("Stopped Wayfern instance {id} (PID: {pid})"); } } Ok(()) } /// Opens a URL in a new tab for an existing Wayfern instance using CDP. /// Returns Ok(()) if successful, or an error if the instance is not found or CDP fails. pub async fn open_url_in_tab( &self, profile_path: &str, url: &str, ) -> Result<(), Box> { let instance = self .find_wayfern_by_profile(profile_path) .await .ok_or("Wayfern instance not found for profile")?; let cdp_port = instance .cdp_port .ok_or("No CDP port available for Wayfern instance")?; // Get the browser target to create a new tab let targets = self.get_cdp_targets(cdp_port).await?; // Find a page target to get the WebSocket URL (we need any target to send commands) let page_target = targets .iter() .find(|t| t.target_type == "page" && t.websocket_debugger_url.is_some()) .ok_or("No page target found for CDP")?; let ws_url = page_target .websocket_debugger_url .as_ref() .ok_or("No WebSocket URL available")?; // Use Target.createTarget to open a new tab with the URL self .send_cdp_command(ws_url, "Target.createTarget", json!({ "url": url })) .await?; log::info!("Opened URL in new tab via CDP: {}", url); Ok(()) } pub async fn get_cdp_port(&self, profile_path: &str) -> Option { let inner = self.inner.lock().await; let target_path = std::path::Path::new(profile_path) .canonicalize() .unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf()); for instance in inner.instances.values() { if let Some(path) = &instance.profile_path { let instance_path = std::path::Path::new(path) .canonicalize() .unwrap_or_else(|_| std::path::Path::new(path).to_path_buf()); if instance_path == target_path { return instance.cdp_port; } } } None } pub async fn find_wayfern_by_profile(&self, profile_path: &str) -> Option { use sysinfo::{ProcessRefreshKind, RefreshKind, System}; let mut inner = self.inner.lock().await; // Canonicalize the target path for comparison let target_path = std::path::Path::new(profile_path) .canonicalize() .unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf()); // Find the instance with the matching profile path let mut found_id: Option = None; for (id, instance) in &inner.instances { if let Some(path) = &instance.profile_path { let instance_path = std::path::Path::new(path) .canonicalize() .unwrap_or_else(|_| std::path::Path::new(path).to_path_buf()); if instance_path == target_path { found_id = Some(id.clone()); break; } } } // If we found an instance, verify the process is still running if let Some(id) = found_id { if let Some(instance) = inner.instances.get(&id) { if let Some(pid) = instance.process_id { let system = System::new_with_specifics( RefreshKind::nothing().with_processes(ProcessRefreshKind::everything()), ); let sysinfo_pid = sysinfo::Pid::from_u32(pid); if system.process(sysinfo_pid).is_some() { return Some(WayfernLaunchResult { id: id.clone(), processId: instance.process_id, profilePath: instance.profile_path.clone(), url: instance.url.clone(), cdp_port: instance.cdp_port, }); } else { log::info!( "Wayfern process {} for profile {} is no longer running, cleaning up", pid, profile_path ); inner.instances.remove(&id); return None; } } } } // If not found in in-memory instances, scan system processes. // This handles the case where the GUI was restarted but Wayfern is still running. if let Some((pid, found_profile_path, cdp_port)) = Self::find_wayfern_process_by_profile(&target_path) { log::info!( "Found running Wayfern process (PID: {}) for profile path via system scan", pid ); let instance_id = format!("recovered_{}", pid); inner.instances.insert( instance_id.clone(), WayfernInstance { id: instance_id.clone(), process_id: Some(pid), profile_path: Some(found_profile_path.clone()), url: None, cdp_port, }, ); return Some(WayfernLaunchResult { id: instance_id, processId: Some(pid), profilePath: Some(found_profile_path), url: None, cdp_port, }); } None } /// Scan system processes to find a Wayfern/Chromium process using a specific profile path fn find_wayfern_process_by_profile( target_path: &std::path::Path, ) -> Option<(u32, String, Option)> { use sysinfo::{ProcessRefreshKind, RefreshKind, System}; let system = System::new_with_specifics( RefreshKind::nothing().with_processes(ProcessRefreshKind::everything()), ); let target_path_str = target_path.to_string_lossy(); for (pid, process) in system.processes() { let cmd = process.cmd(); if cmd.is_empty() { continue; } let exe_name = process.name().to_string_lossy().to_lowercase(); let is_chromium_like = exe_name.contains("wayfern") || exe_name.contains("chromium") || exe_name.contains("chrome"); if !is_chromium_like { continue; } // Skip child processes (renderer, GPU, utility, zygote, etc.) // Only the main browser process lacks a --type= argument let is_child = cmd .iter() .any(|a| a.to_str().is_some_and(|s| s.starts_with("--type="))); if is_child { continue; } let mut matched = false; let mut cdp_port: Option = None; for arg in cmd.iter() { if let Some(arg_str) = arg.to_str() { if let Some(dir_val) = arg_str.strip_prefix("--user-data-dir=") { let cmd_path = std::path::Path::new(dir_val) .canonicalize() .unwrap_or_else(|_| std::path::Path::new(dir_val).to_path_buf()); if cmd_path == target_path { matched = true; } } if let Some(port_val) = arg_str.strip_prefix("--remote-debugging-port=") { cdp_port = port_val.parse().ok(); } } } if matched { return Some((pid.as_u32(), target_path_str.to_string(), cdp_port)); } } None } #[allow(dead_code)] pub async fn launch_wayfern_profile( &self, app_handle: &AppHandle, profile: &BrowserProfile, config: &WayfernConfig, url: Option<&str>, proxy_url: Option<&str>, ) -> Result> { let profiles_dir = self.get_profiles_dir(); let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); let profile_path_str = profile_path.to_string_lossy().to_string(); std::fs::create_dir_all(&profile_path)?; if let Some(existing) = self.find_wayfern_by_profile(&profile_path_str).await { log::info!("Stopping existing Wayfern instance for profile"); self.stop_wayfern(&existing.id).await?; } self .launch_wayfern( app_handle, profile, &profile_path_str, config, url, proxy_url, profile.ephemeral, &[], None, ) .await } #[allow(dead_code)] pub async fn cleanup_dead_instances(&self) { use sysinfo::{ProcessRefreshKind, RefreshKind, System}; let mut inner = self.inner.lock().await; let mut dead_ids = Vec::new(); let system = System::new_with_specifics( RefreshKind::nothing().with_processes(ProcessRefreshKind::everything()), ); for (id, instance) in &inner.instances { if let Some(pid) = instance.process_id { let pid = sysinfo::Pid::from_u32(pid); if !system.processes().contains_key(&pid) { dead_ids.push(id.clone()); } } } for id in dead_ids { log::info!("Cleaning up dead Wayfern instance: {id}"); inner.instances.remove(&id); } } } lazy_static::lazy_static! { static ref WAYFERN_MANAGER: WayfernManager = WayfernManager::new(); }