Files
donutbrowser/src-tauri/src/browser_runner.rs
T

2853 lines
100 KiB
Rust

use crate::browser::{create_browser, BrowserType, ProxySettings};
use crate::camoufox_manager::{CamoufoxConfig, CamoufoxManager};
use crate::cloud_auth::CLOUD_AUTH;
use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry;
use crate::events;
use crate::platform_browser;
use crate::profile::{BrowserProfile, ProfileManager};
use crate::proxy_manager::PROXY_MANAGER;
use crate::wayfern_manager::{WayfernConfig, WayfernManager};
use serde::Serialize;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use sysinfo::System;
pub struct BrowserRunner {
pub profile_manager: &'static ProfileManager,
pub downloaded_browsers_registry: &'static DownloadedBrowsersRegistry,
auto_updater: &'static crate::auto_updater::AutoUpdater,
camoufox_manager: &'static CamoufoxManager,
wayfern_manager: &'static WayfernManager,
}
impl BrowserRunner {
fn new() -> Self {
Self {
profile_manager: ProfileManager::instance(),
downloaded_browsers_registry: DownloadedBrowsersRegistry::instance(),
auto_updater: crate::auto_updater::AutoUpdater::instance(),
camoufox_manager: CamoufoxManager::instance(),
wayfern_manager: WayfernManager::instance(),
}
}
pub fn instance() -> &'static BrowserRunner {
&BROWSER_RUNNER
}
pub fn get_binaries_dir(&self) -> PathBuf {
crate::app_dirs::binaries_dir()
}
/// Refresh cloud proxy credentials if the profile uses a cloud or cloud-derived proxy,
/// then resolve the proxy settings.
async fn resolve_proxy_with_refresh(&self, proxy_id: Option<&String>) -> Option<ProxySettings> {
let proxy_id = proxy_id?;
if PROXY_MANAGER.is_cloud_or_derived(proxy_id) {
log::info!("Refreshing cloud proxy credentials before launch for proxy {proxy_id}");
CLOUD_AUTH.sync_cloud_proxy().await;
}
PROXY_MANAGER.get_proxy_settings_by_id(proxy_id)
}
/// Get the executable path for a browser profile
/// This is a common helper to eliminate code duplication across the codebase
pub fn get_browser_executable_path(
&self,
profile: &BrowserProfile,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
// Create browser instance to get executable path
let browser_type = crate::browser::BrowserType::from_str(&profile.browser)
.map_err(|e| format!("Invalid browser type: {e}"))?;
let browser = crate::browser::create_browser(browser_type);
// Construct browser directory path: binaries/<browser>/<version>/
let mut browser_dir = self.get_binaries_dir();
browser_dir.push(&profile.browser);
browser_dir.push(&profile.version);
// Get platform-specific executable path
browser
.get_executable_path(&browser_dir)
.map_err(|e| format!("Failed to get executable path for {}: {e}", profile.browser).into())
}
pub async fn launch_browser(
&self,
app_handle: tauri::AppHandle,
profile: &BrowserProfile,
url: Option<String>,
local_proxy_settings: Option<&ProxySettings>,
) -> Result<BrowserProfile, Box<dyn std::error::Error + Send + Sync>> {
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<String>,
local_proxy_settings: Option<&ProxySettings>,
remote_debugging_port: Option<u16>,
headless: bool,
) -> Result<BrowserProfile, Box<dyn std::error::Error + Send + Sync>> {
// Handle Camoufox profiles using CamoufoxManager
if profile.browser == "camoufox" {
// Get or create camoufox config
let mut camoufox_config = profile.camoufox_config.clone().unwrap_or_else(|| {
log::info!(
"No camoufox config found for profile {}, using default",
profile.name
);
CamoufoxConfig::default()
});
// Always start a local proxy for Camoufox (for traffic monitoring and geoip support)
// Refresh cloud proxy credentials if needed before resolving
let mut upstream_proxy = self
.resolve_proxy_with_refresh(profile.proxy_id.as_ref())
.await;
// If profile has a VPN instead of proxy, start VPN worker and use it as upstream
if upstream_proxy.is_none() {
if let Some(ref vpn_id) = profile.vpn_id {
match crate::vpn_worker_runner::start_vpn_worker(vpn_id).await {
Ok(vpn_worker) => {
if let Some(port) = vpn_worker.local_port {
upstream_proxy = Some(ProxySettings {
proxy_type: "socks5".to_string(),
host: "127.0.0.1".to_string(),
port,
username: None,
password: None,
});
log::info!("VPN worker started for Camoufox profile on port {}", port);
}
}
Err(e) => {
return Err(format!("Failed to start VPN worker: {e}").into());
}
}
}
}
log::info!(
"Starting local proxy for Camoufox profile: {} (upstream: {})",
profile.name,
upstream_proxy
.as_ref()
.map(|p| format!("{}:{}", p.host, p.port))
.unwrap_or_else(|| "DIRECT".to_string())
);
// Start the proxy and get local proxy settings
// If proxy startup fails, DO NOT launch Camoufox - it requires local proxy
let profile_id_str = profile.id.to_string();
let local_proxy = PROXY_MANAGER
.start_proxy(
app_handle.clone(),
upstream_proxy.as_ref(),
0, // Use 0 as temporary PID, will be updated later
Some(&profile_id_str),
profile.proxy_bypass_rules.clone(),
)
.await
.map_err(|e| {
let error_msg = format!("Failed to start local proxy for Camoufox: {e}");
log::error!("{}", error_msg);
error_msg
})?;
// Format proxy URL for camoufox - always use HTTP for the local proxy
let proxy_url = format!("http://{}:{}", local_proxy.host, local_proxy.port);
// Set proxy in camoufox config
camoufox_config.proxy = Some(proxy_url);
// Ensure geoip is always enabled for proper geolocation spoofing
if camoufox_config.geoip.is_none() {
camoufox_config.geoip = Some(serde_json::Value::Bool(true));
}
log::info!(
"Configured local proxy for Camoufox: {:?}, geoip: {:?}",
camoufox_config.proxy,
camoufox_config.geoip
);
// Check if we need to generate a new fingerprint on every launch
let mut updated_profile = profile.clone();
if camoufox_config.randomize_fingerprint_on_launch == Some(true) {
log::info!(
"Generating random fingerprint for Camoufox profile: {}",
profile.name
);
// Create a config copy without the existing fingerprint to force generation of a new one
let mut config_for_generation = camoufox_config.clone();
config_for_generation.fingerprint = None;
// Generate a new fingerprint
let new_fingerprint = self
.camoufox_manager
.generate_fingerprint_config(&app_handle, profile, &config_for_generation)
.await
.map_err(|e| format!("Failed to generate random fingerprint: {e}"))?;
log::info!(
"New fingerprint generated, length: {} chars",
new_fingerprint.len()
);
// Update the config with the new fingerprint for launching
camoufox_config.fingerprint = Some(new_fingerprint.clone());
// Save the updated fingerprint to the profile so it persists
// We need to preserve all existing config fields and only update the fingerprint
let mut updated_camoufox_config =
updated_profile.camoufox_config.clone().unwrap_or_default();
updated_camoufox_config.fingerprint = Some(new_fingerprint);
// Preserve the randomize flag so it persists across launches
updated_camoufox_config.randomize_fingerprint_on_launch = Some(true);
// Preserve the OS setting so it's used for future fingerprint generation
if camoufox_config.os.is_some() {
updated_camoufox_config.os = camoufox_config.os.clone();
}
updated_profile.camoufox_config = Some(updated_camoufox_config.clone());
log::info!(
"Updated profile camoufox_config with new fingerprint for profile: {}, fingerprint length: {}",
profile.name,
updated_camoufox_config.fingerprint.as_ref().map(|f| f.len()).unwrap_or(0)
);
}
// Create ephemeral dir for ephemeral profiles
let override_profile_path = if profile.ephemeral {
let dir = crate::ephemeral_dirs::create_ephemeral_dir(&profile.id.to_string())
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { e.into() })?;
Some(dir)
} else {
None
};
// Install extensions if an extension group is assigned
if updated_profile.extension_group_id.is_some() {
let profiles_dir = self.profile_manager.get_profiles_dir();
let ext_profile_path = if let Some(ref override_path) = override_profile_path {
override_path.clone()
} else {
updated_profile.get_profile_data_path(&profiles_dir)
};
let mgr = crate::extension_manager::EXTENSION_MANAGER.lock().unwrap();
match mgr.install_extensions_for_profile(&updated_profile, &ext_profile_path) {
Ok(paths) => {
if !paths.is_empty() {
log::info!(
"Installed {} Firefox extensions for profile: {}",
paths.len(),
updated_profile.name
);
}
}
Err(e) => {
log::warn!("Failed to install extensions for Camoufox profile: {e}");
}
}
}
// Launch Camoufox browser
log::info!("Launching Camoufox for profile: {}", profile.name);
let camoufox_result = self
.camoufox_manager
.launch_camoufox_profile(
app_handle.clone(),
updated_profile.clone(),
camoufox_config,
url,
override_profile_path,
)
.await
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
format!("Failed to launch Camoufox: {e}").into()
})?;
// For server-based Camoufox, we use the process_id
let process_id = camoufox_result.processId.unwrap_or(0);
log::info!("Camoufox launched successfully with PID: {process_id}");
// Update profile with the process info from camoufox result
updated_profile.process_id = Some(process_id);
updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs());
// Update the proxy manager with the correct PID
if let Err(e) = PROXY_MANAGER.update_proxy_pid(0, process_id) {
log::warn!("Warning: Failed to update proxy PID mapping: {e}");
} else {
log::info!("Updated proxy PID mapping from temp (0) to actual PID: {process_id}");
}
// Save the updated profile (includes new fingerprint if randomize is enabled)
log::info!(
"Saving profile {} with camoufox_config fingerprint length: {}",
updated_profile.name,
updated_profile
.camoufox_config
.as_ref()
.and_then(|c| c.fingerprint.as_ref())
.map(|f| f.len())
.unwrap_or(0)
);
self.save_process_info(&updated_profile)?;
// Ensure tag suggestions include any tags from this profile
let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| {
let _ = tm.rebuild_from_profiles(&self.profile_manager.list_profiles().unwrap_or_default());
});
log::info!(
"Successfully saved profile with process info: {}",
updated_profile.name
);
// Emit profiles-changed to trigger frontend to reload profiles from disk
// This ensures the UI displays the newly generated fingerprint
if let Err(e) = events::emit_empty("profiles-changed") {
log::warn!("Warning: Failed to emit profiles-changed event: {e}");
}
log::info!(
"Emitting profile events for successful Camoufox launch: {}",
updated_profile.name
);
// Emit profile update event to frontend
if let Err(e) = events::emit("profile-updated", &updated_profile) {
log::warn!("Warning: Failed to emit profile update event: {e}");
}
// Emit minimal running changed event to frontend with a small delay
#[derive(Serialize)]
struct RunningChangedPayload {
id: String,
is_running: bool,
}
let payload = RunningChangedPayload {
id: updated_profile.id.to_string(),
is_running: updated_profile.process_id.is_some(),
};
if let Err(e) = events::emit("profile-running-changed", &payload) {
log::warn!("Warning: Failed to emit profile running changed event: {e}");
} else {
log::info!(
"Successfully emitted profile-running-changed event for Camoufox {}: running={}",
updated_profile.name,
payload.is_running
);
}
return Ok(updated_profile);
}
// Handle Wayfern profiles using WayfernManager
if profile.browser == "wayfern" {
// Get or create wayfern config
let mut wayfern_config = profile.wayfern_config.clone().unwrap_or_else(|| {
log::info!(
"No wayfern config found for profile {}, using default",
profile.name
);
WayfernConfig::default()
});
// Always start a local proxy for Wayfern (for traffic monitoring and geoip support)
// Refresh cloud proxy credentials if needed before resolving
let mut upstream_proxy = self
.resolve_proxy_with_refresh(profile.proxy_id.as_ref())
.await;
// If profile has a VPN instead of proxy, start VPN worker and use it as upstream
if upstream_proxy.is_none() {
if let Some(ref vpn_id) = profile.vpn_id {
match crate::vpn_worker_runner::start_vpn_worker(vpn_id).await {
Ok(vpn_worker) => {
if let Some(port) = vpn_worker.local_port {
upstream_proxy = Some(ProxySettings {
proxy_type: "socks5".to_string(),
host: "127.0.0.1".to_string(),
port,
username: None,
password: None,
});
log::info!("VPN worker started for Wayfern profile on port {}", port);
}
}
Err(e) => {
return Err(format!("Failed to start VPN worker: {e}").into());
}
}
}
}
log::info!(
"Starting local proxy for Wayfern profile: {} (upstream: {})",
profile.name,
upstream_proxy
.as_ref()
.map(|p| format!("{}:{}", p.host, p.port))
.unwrap_or_else(|| "DIRECT".to_string())
);
// Start the proxy and get local proxy settings
// If proxy startup fails, DO NOT launch Wayfern - it requires local proxy
let profile_id_str = profile.id.to_string();
let local_proxy = PROXY_MANAGER
.start_proxy(
app_handle.clone(),
upstream_proxy.as_ref(),
0, // Use 0 as temporary PID, will be updated later
Some(&profile_id_str),
profile.proxy_bypass_rules.clone(),
)
.await
.map_err(|e| {
let error_msg = format!("Failed to start local proxy for Wayfern: {e}");
log::error!("{}", error_msg);
error_msg
})?;
// Format proxy URL for wayfern - always use HTTP for the local proxy
let proxy_url = format!("http://{}:{}", local_proxy.host, local_proxy.port);
// Set proxy in wayfern config
wayfern_config.proxy = Some(proxy_url);
log::info!(
"Configured local proxy for Wayfern: {:?}",
wayfern_config.proxy
);
// Check if we need to generate a new fingerprint on every launch
let mut updated_profile = profile.clone();
if wayfern_config.randomize_fingerprint_on_launch == Some(true) {
log::info!(
"Generating random fingerprint for Wayfern profile: {}",
profile.name
);
// Create a config copy without the existing fingerprint to force generation of a new one
let mut config_for_generation = wayfern_config.clone();
config_for_generation.fingerprint = None;
// Generate a new fingerprint
let new_fingerprint = self
.wayfern_manager
.generate_fingerprint_config(&app_handle, profile, &config_for_generation)
.await
.map_err(|e| format!("Failed to generate random fingerprint: {e}"))?;
log::info!(
"New fingerprint generated, length: {} chars",
new_fingerprint.len()
);
// Update the config with the new fingerprint for launching
wayfern_config.fingerprint = Some(new_fingerprint.clone());
// Save the updated fingerprint to the profile so it persists
let mut updated_wayfern_config = updated_profile.wayfern_config.clone().unwrap_or_default();
updated_wayfern_config.fingerprint = Some(new_fingerprint);
updated_wayfern_config.randomize_fingerprint_on_launch = Some(true);
if wayfern_config.os.is_some() {
updated_wayfern_config.os = wayfern_config.os.clone();
}
updated_profile.wayfern_config = Some(updated_wayfern_config.clone());
log::info!(
"Updated profile wayfern_config with new fingerprint for profile: {}, fingerprint length: {}",
profile.name,
updated_wayfern_config.fingerprint.as_ref().map(|f| f.len()).unwrap_or(0)
);
}
// Create ephemeral dir for ephemeral profiles
if profile.ephemeral {
crate::ephemeral_dirs::create_ephemeral_dir(&profile.id.to_string())
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { e.into() })?;
}
// Launch Wayfern browser
log::info!("Launching Wayfern for profile: {}", profile.name);
// Get profile path for Wayfern
let profiles_dir = self.profile_manager.get_profiles_dir();
let profile_data_path =
crate::ephemeral_dirs::get_effective_profile_path(&updated_profile, &profiles_dir);
let profile_path_str = profile_data_path.to_string_lossy().to_string();
// Install extensions if an extension group is assigned
let mut extension_paths = Vec::new();
if updated_profile.extension_group_id.is_some() {
let mgr = crate::extension_manager::EXTENSION_MANAGER.lock().unwrap();
match mgr.install_extensions_for_profile(&updated_profile, &profile_data_path) {
Ok(paths) => {
if !paths.is_empty() {
log::info!(
"Prepared {} Chromium extensions for profile: {}",
paths.len(),
updated_profile.name
);
}
extension_paths = paths;
}
Err(e) => {
log::warn!("Failed to install extensions for Wayfern profile: {e}");
}
}
}
// Get proxy URL from config
let proxy_url = wayfern_config.proxy.as_deref();
let wayfern_result = self
.wayfern_manager
.launch_wayfern(
&app_handle,
&updated_profile,
&profile_path_str,
&wayfern_config,
url.as_deref(),
proxy_url,
profile.ephemeral,
&extension_paths,
)
.await
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
format!("Failed to launch Wayfern: {e}").into()
})?;
// Get the process ID from launch result
let process_id = wayfern_result.processId.unwrap_or(0);
log::info!("Wayfern launched successfully with PID: {process_id}");
// Update profile with the process info
updated_profile.process_id = Some(process_id);
updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs());
// Update the proxy manager with the correct PID
if let Err(e) = PROXY_MANAGER.update_proxy_pid(0, process_id) {
log::warn!("Warning: Failed to update proxy PID mapping: {e}");
} else {
log::info!("Updated proxy PID mapping from temp (0) to actual PID: {process_id}");
}
// Save the updated profile
log::info!(
"Saving profile {} with wayfern_config fingerprint length: {}",
updated_profile.name,
updated_profile
.wayfern_config
.as_ref()
.and_then(|c| c.fingerprint.as_ref())
.map(|f| f.len())
.unwrap_or(0)
);
self.save_process_info(&updated_profile)?;
let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| {
let _ = tm.rebuild_from_profiles(&self.profile_manager.list_profiles().unwrap_or_default());
});
log::info!(
"Successfully saved profile with process info: {}",
updated_profile.name
);
// Emit profiles-changed to trigger frontend to reload profiles from disk
if let Err(e) = events::emit_empty("profiles-changed") {
log::warn!("Warning: Failed to emit profiles-changed event: {e}");
}
log::info!(
"Emitting profile events for successful Wayfern launch: {}",
updated_profile.name
);
// Emit profile update event to frontend
if let Err(e) = events::emit("profile-updated", &updated_profile) {
log::warn!("Warning: Failed to emit profile update event: {e}");
}
// Emit minimal running changed event to frontend
#[derive(Serialize)]
struct RunningChangedPayload {
id: String,
is_running: bool,
}
let payload = RunningChangedPayload {
id: updated_profile.id.to_string(),
is_running: updated_profile.process_id.is_some(),
};
if let Err(e) = events::emit("profile-running-changed", &payload) {
log::warn!("Warning: Failed to emit profile running changed event: {e}");
} else {
log::info!(
"Successfully emitted profile-running-changed event for Wayfern {}: running={}",
updated_profile.name,
payload.is_running
);
}
return Ok(updated_profile);
}
// Create browser instance
let browser_type = BrowserType::from_str(&profile.browser)
.map_err(|_| format!("Invalid browser type: {}", profile.browser))?;
let browser = create_browser(browser_type.clone());
// Get executable path using common helper
let executable_path = self
.get_browser_executable_path(profile)
.expect("Failed to get executable path");
log::info!("Executable path: {executable_path:?}");
// Prepare the executable (set permissions, etc.)
if let Err(e) = browser.prepare_executable(&executable_path) {
log::warn!("Warning: Failed to prepare executable: {e}");
// Continue anyway, the error might not be critical
}
// Refresh cloud proxy credentials if needed before resolving
let _stored_proxy_settings = self
.resolve_proxy_with_refresh(profile.proxy_id.as_ref())
.await;
// Use provided local proxy for Chromium-based browsers launch arguments
let proxy_for_launch_args: Option<&ProxySettings> = local_proxy_settings;
// Get profile data path and launch arguments
let profiles_dir = self.profile_manager.get_profiles_dir();
let profile_data_path = profile.get_profile_data_path(&profiles_dir);
let browser_args = browser
.create_launch_args(
&profile_data_path.to_string_lossy(),
proxy_for_launch_args,
url,
remote_debugging_port,
headless,
)
.expect("Failed to create launch arguments");
// Launch browser using platform-specific method
let child = {
#[cfg(target_os = "macos")]
{
platform_browser::macos::launch_browser_process(&executable_path, &browser_args).await?
}
#[cfg(target_os = "windows")]
{
platform_browser::windows::launch_browser_process(&executable_path, &browser_args).await?
}
#[cfg(target_os = "linux")]
{
platform_browser::linux::launch_browser_process(&executable_path, &browser_args).await?
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
{
return Err("Unsupported platform for browser launching".into());
}
};
let launcher_pid = child.id();
log::info!(
"Launched browser with launcher PID: {} for profile: {} (ID: {})",
launcher_pid,
profile.name,
profile.id
);
// On macOS, when launching via `open -a`, the child PID is the `open` helper.
// Resolve and store the actual browser PID for all browser types.
let actual_pid = {
#[cfg(target_os = "macos")]
{
// Give the browser a moment to start
tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await;
let system = System::new_all();
let profiles_dir = self.profile_manager.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 mut resolved_pid = launcher_pid;
for (pid, process) in system.processes() {
let cmd = process.cmd();
if cmd.is_empty() {
continue;
}
// Determine if this process matches the intended browser type
let exe_name_lower = process.name().to_string_lossy().to_lowercase();
let is_correct_browser = match profile.browser.as_str() {
"firefox" => {
exe_name_lower.contains("firefox")
&& !exe_name_lower.contains("developer")
&& !exe_name_lower.contains("camoufox")
}
"firefox-developer" => {
// More flexible detection for Firefox Developer Edition
(exe_name_lower.contains("firefox") && exe_name_lower.contains("developer"))
|| (exe_name_lower.contains("firefox")
&& cmd.iter().any(|arg| {
let arg_str = arg.to_str().unwrap_or("");
arg_str.contains("Developer")
|| arg_str.contains("developer")
|| arg_str.contains("FirefoxDeveloperEdition")
|| arg_str.contains("firefox-developer")
}))
|| exe_name_lower == "firefox" // Firefox Developer might just show as "firefox"
}
"zen" => exe_name_lower.contains("zen"),
"chromium" => exe_name_lower.contains("chromium") || exe_name_lower.contains("chrome"),
"brave" => exe_name_lower.contains("brave") || exe_name_lower.contains("Brave"),
_ => false,
};
if !is_correct_browser {
continue;
}
// Check for profile path match
let profile_path_match = if matches!(
profile.browser.as_str(),
"firefox" | "firefox-developer" | "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 {
resolved_pid = pid_u32;
break;
}
}
}
resolved_pid
}
#[cfg(not(target_os = "macos"))]
{
launcher_pid
}
};
// Update profile with process info and save
let mut updated_profile = profile.clone();
updated_profile.process_id = Some(actual_pid);
updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs());
self.save_process_info(&updated_profile)?;
let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| {
let _ = tm.rebuild_from_profiles(&self.profile_manager.list_profiles().unwrap_or_default());
});
// Apply proxy settings if needed (for Firefox-based browsers)
if profile.proxy_id.is_some()
&& matches!(
browser_type,
BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen
)
{
// Proxy settings for Firefox-based browsers are applied via user.js file
// which is already handled in the profile creation process
}
log::info!(
"Emitting profile events for successful launch: {} (ID: {})",
updated_profile.name,
updated_profile.id
);
// Emit profile update event to frontend
if let Err(e) = events::emit("profile-updated", &updated_profile) {
log::warn!("Warning: Failed to emit profile update event: {e}");
}
// Emit minimal running changed event to frontend with a small delay to ensure UI consistency
#[derive(Serialize)]
struct RunningChangedPayload {
id: String,
is_running: bool,
}
let payload = RunningChangedPayload {
id: updated_profile.id.to_string(),
is_running: updated_profile.process_id.is_some(),
};
if let Err(e) = events::emit("profile-running-changed", &payload) {
log::warn!("Warning: Failed to emit profile running changed event: {e}");
} else {
log::info!(
"Successfully emitted profile-running-changed event for {}: running={}",
updated_profile.name,
payload.is_running
);
}
Ok(updated_profile)
}
pub async fn open_url_in_existing_browser(
&self,
app_handle: tauri::AppHandle,
profile: &BrowserProfile,
url: &str,
_internal_proxy_settings: Option<&ProxySettings>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Handle Camoufox profiles using CamoufoxManager
if profile.browser == "camoufox" {
// Get the profile path based on the UUID
let profiles_dir = self.profile_manager.get_profiles_dir();
let profile_data_path =
crate::ephemeral_dirs::get_effective_profile_path(profile, &profiles_dir);
let profile_path_str = profile_data_path.to_string_lossy();
// Check if the process is running
match self
.camoufox_manager
.find_camoufox_by_profile(&profile_path_str)
.await
{
Ok(Some(_camoufox_process)) => {
log::info!(
"Opening URL in existing Camoufox process for profile: {} (ID: {})",
profile.name,
profile.id
);
// Get Camoufox executable path and use Firefox-like remote mechanism
let executable_path = self
.get_browser_executable_path(profile)
.map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?;
// Launch Camoufox with -profile and -new-tab to open URL in existing instance
// This works because we no longer use -no-remote flag
let output = std::process::Command::new(&executable_path)
.arg("-profile")
.arg(&*profile_path_str)
.arg("-new-tab")
.arg(url)
.output()
.map_err(|e| format!("Failed to execute Camoufox: {e}"))?;
if output.status.success() {
log::info!("Successfully opened URL in existing Camoufox instance");
return Ok(());
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
log::warn!("Camoufox -new-tab command failed: {stderr}");
return Err(
format!("Failed to open URL in existing Camoufox instance: {stderr}").into(),
);
}
}
Ok(None) => {
return Err("Camoufox browser is not running".into());
}
Err(e) => {
return Err(format!("Error checking Camoufox process: {e}").into());
}
}
}
// Handle Wayfern profiles using WayfernManager
if profile.browser == "wayfern" {
let profiles_dir = self.profile_manager.get_profiles_dir();
let profile_data_path =
crate::ephemeral_dirs::get_effective_profile_path(profile, &profiles_dir);
let profile_path_str = profile_data_path.to_string_lossy();
// Check if the process is running
match self
.wayfern_manager
.find_wayfern_by_profile(&profile_path_str)
.await
{
Some(_wayfern_process) => {
log::info!(
"Opening URL in existing Wayfern process for profile: {} (ID: {})",
profile.name,
profile.id
);
// Use CDP to open URL in a new tab
self
.wayfern_manager
.open_url_in_tab(&profile_path_str, url)
.await?;
return Ok(());
}
None => {
return Err("Wayfern browser is not running".into());
}
}
}
// Use the comprehensive browser status check for non-camoufox/wayfern browsers
let is_running = self
.check_browser_status(app_handle.clone(), profile)
.await?;
if !is_running {
return Err("Browser is not running".into());
}
// Get the updated profile with current PID
let profiles = self
.profile_manager
.list_profiles()
.expect("Failed to list profiles");
let updated_profile = profiles
.into_iter()
.find(|p| p.id == profile.id)
.unwrap_or_else(|| profile.clone());
// Ensure we have a valid process ID
if updated_profile.process_id.is_none() {
return Err("No valid process ID found for the browser".into());
}
let browser_type = BrowserType::from_str(&updated_profile.browser)
.map_err(|_| format!("Invalid browser type: {}", updated_profile.browser))?;
// Get browser directory for all platforms - path structure: binaries/<browser>/<version>/
let mut browser_dir = self.get_binaries_dir();
browser_dir.push(&updated_profile.browser);
browser_dir.push(&updated_profile.version);
match browser_type {
BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen => {
#[cfg(target_os = "macos")]
{
let profiles_dir = self.profile_manager.get_profiles_dir();
return platform_browser::macos::open_url_in_existing_browser_firefox_like(
&updated_profile,
url,
browser_type,
&browser_dir,
&profiles_dir,
)
.await;
}
#[cfg(target_os = "windows")]
{
let profiles_dir = self.profile_manager.get_profiles_dir();
return platform_browser::windows::open_url_in_existing_browser_firefox_like(
&updated_profile,
url,
browser_type,
&browser_dir,
&profiles_dir,
)
.await;
}
#[cfg(target_os = "linux")]
{
let profiles_dir = self.profile_manager.get_profiles_dir();
return platform_browser::linux::open_url_in_existing_browser_firefox_like(
&updated_profile,
url,
browser_type,
&browser_dir,
&profiles_dir,
)
.await;
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
return Err("Unsupported platform".into());
}
BrowserType::Camoufox => {
// Camoufox URL opening is handled differently
Err("URL opening in existing Camoufox instance is not supported".into())
}
BrowserType::Wayfern => {
// Wayfern URL opening is handled differently
Err("URL opening in existing Wayfern instance is not supported".into())
}
BrowserType::Chromium | BrowserType::Brave => {
#[cfg(target_os = "macos")]
{
let profiles_dir = self.profile_manager.get_profiles_dir();
return platform_browser::macos::open_url_in_existing_browser_chromium(
&updated_profile,
url,
browser_type,
&browser_dir,
&profiles_dir,
)
.await;
}
#[cfg(target_os = "windows")]
{
let profiles_dir = self.profile_manager.get_profiles_dir();
return platform_browser::windows::open_url_in_existing_browser_chromium(
&updated_profile,
url,
browser_type,
&browser_dir,
&profiles_dir,
)
.await;
}
#[cfg(target_os = "linux")]
{
let profiles_dir = self.profile_manager.get_profiles_dir();
return platform_browser::linux::open_url_in_existing_browser_chromium(
&updated_profile,
url,
browser_type,
&browser_dir,
&profiles_dir,
)
.await;
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
return Err("Unsupported platform".into());
}
}
}
pub async fn launch_browser_with_debugging(
&self,
app_handle: tauri::AppHandle,
profile: &BrowserProfile,
url: Option<String>,
remote_debugging_port: Option<u16>,
headless: bool,
) -> Result<BrowserProfile, Box<dyn std::error::Error + Send + Sync>> {
// Always start a local proxy for API launches
// 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;
let profile_id_str = profile.id.to_string();
// Start local proxy - if this fails, DO NOT launch browser
let internal_proxy = PROXY_MANAGER
.start_proxy(
app_handle.clone(),
upstream_proxy.as_ref(),
temp_pid,
Some(&profile_id_str),
profile.proxy_bypass_rules.clone(),
)
.await
.map_err(|e| {
let error_msg = format!("Failed to start local proxy: {e}");
log::error!("{}", error_msg);
error_msg
})?;
let internal_proxy_settings = Some(internal_proxy.clone());
// Configure Firefox profiles to use local proxy
{
// For Firefox-based browsers, apply PAC/user.js to point to the local proxy
if matches!(
profile.browser.as_str(),
"firefox" | "firefox-developer" | "zen"
) {
let profiles_dir = self.profile_manager.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
.profile_manager
.apply_proxy_settings_to_profile(&profile_path, &dummy_upstream, Some(&internal_proxy))
.map_err(|e| format!("Failed to update profile proxy: {e}"))?;
}
}
let result = self
.launch_browser_internal(
app_handle.clone(),
profile,
url,
internal_proxy_settings.as_ref(),
remote_debugging_port,
headless,
)
.await;
// Update proxy with correct PID if launch succeeded
if let Ok(ref updated_profile) = result {
if let Some(actual_pid) = updated_profile.process_id {
let _ = PROXY_MANAGER.update_proxy_pid(temp_pid, actual_pid);
}
}
result
}
pub async fn launch_or_open_url(
&self,
app_handle: tauri::AppHandle,
profile: &BrowserProfile,
url: Option<String>,
internal_proxy_settings: Option<&ProxySettings>,
) -> Result<BrowserProfile, Box<dyn std::error::Error + Send + Sync>> {
log::info!(
"launch_or_open_url called for profile: {} (ID: {})",
profile.name,
profile.id
);
// Get the most up-to-date profile data
let profiles = self
.profile_manager
.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());
log::info!(
"Checking browser status for profile: {} (ID: {})",
updated_profile.name,
updated_profile.id
);
// Check if browser is already running
let is_running = self
.check_browser_status(app_handle.clone(), &updated_profile)
.await
.map_err(|e| format!("Failed to check browser status: {e}"))?;
// Get the updated profile again after status check (PID might have been updated)
let profiles = self
.profile_manager
.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());
log::info!(
"Browser status check - Profile: {} (ID: {}), Running: {}, URL: {:?}, PID: {:?}",
final_profile.name,
final_profile.id,
is_running,
url,
final_profile.process_id
);
if is_running && url.is_some() {
// Browser is running and we have a URL to open
if let Some(url_ref) = url.as_ref() {
log::info!("Opening URL in existing browser: {url_ref}");
match self
.open_url_in_existing_browser(
app_handle.clone(),
&final_profile,
url_ref,
internal_proxy_settings,
)
.await
{
Ok(()) => {
log::info!("Successfully opened URL in existing browser");
Ok(final_profile)
}
Err(e) => {
log::info!("Failed to open URL in existing browser: {e}");
// Fall back to launching a new instance
log::info!(
"Falling back to new instance for browser: {}",
final_profile.browser
);
// Fallback to launching a new instance for other browsers
self
.launch_browser_internal(
app_handle.clone(),
&final_profile,
url,
internal_proxy_settings,
None,
false,
)
.await
}
}
} else {
// This case shouldn't happen since we checked is_some() above, but handle it gracefully
log::info!("URL was unexpectedly None, launching new browser instance");
self
.launch_browser(
app_handle.clone(),
&final_profile,
url,
internal_proxy_settings,
)
.await
}
} else {
// Browser is not running or no URL provided, launch new instance
if !is_running {
log::info!("Launching new browser instance - browser not running");
} else {
log::info!("Launching new browser instance - no URL provided");
}
self
.launch_browser_internal(
app_handle.clone(),
&final_profile,
url,
internal_proxy_settings,
None,
false,
)
.await
}
}
fn save_process_info(
&self,
profile: &BrowserProfile,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Use the regular save_profile method which handles the UUID structure
self.profile_manager.save_profile(profile).map_err(|e| {
let error_string = e.to_string();
Box::new(std::io::Error::other(error_string)) as Box<dyn std::error::Error + Send + Sync>
})
}
pub async fn check_browser_status(
&self,
app_handle: tauri::AppHandle,
profile: &BrowserProfile,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
self
.profile_manager
.check_browser_status(app_handle, profile)
.await
}
pub async fn kill_browser_process(
&self,
app_handle: tauri::AppHandle,
profile: &BrowserProfile,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Handle Camoufox profiles using CamoufoxManager
if profile.browser == "camoufox" {
// Search by profile path to find the running Camoufox instance
let profiles_dir = self.profile_manager.get_profiles_dir();
let profile_data_path =
crate::ephemeral_dirs::get_effective_profile_path(profile, &profiles_dir);
let profile_path_str = profile_data_path.to_string_lossy();
log::info!(
"Attempting to kill Camoufox process for profile: {} (ID: {})",
profile.name,
profile.id
);
// Stop the proxy associated with this profile first
let profile_id_str = profile.id.to_string();
if let Err(e) = PROXY_MANAGER
.stop_proxy_by_profile_id(app_handle.clone(), &profile_id_str)
.await
{
log::warn!(
"Warning: Failed to stop proxy for profile {}: {e}",
profile_id_str
);
}
let mut process_actually_stopped = false;
match self
.camoufox_manager
.find_camoufox_by_profile(&profile_path_str)
.await
{
Ok(Some(camoufox_process)) => {
log::info!(
"Found Camoufox process: {} (PID: {:?})",
camoufox_process.id,
camoufox_process.processId
);
match self
.camoufox_manager
.stop_camoufox(&app_handle, &camoufox_process.id)
.await
{
Ok(stopped) => {
if let Some(pid) = camoufox_process.processId {
if stopped {
// Verify the process actually died by checking after a short delay
use tokio::time::{sleep, Duration};
sleep(Duration::from_millis(500)).await;
use sysinfo::{Pid, System};
let system = System::new_all();
process_actually_stopped = system.process(Pid::from(pid as usize)).is_none();
if process_actually_stopped {
log::info!(
"Successfully stopped Camoufox process: {} (PID: {:?}) - verified process is dead",
camoufox_process.id,
pid
);
} else {
log::warn!(
"Camoufox stop command returned success but process {} (PID: {:?}) is still running - forcing kill",
camoufox_process.id,
pid
);
// Force kill the process
#[cfg(target_os = "macos")]
{
use crate::platform_browser;
if let Err(e) = platform_browser::macos::kill_browser_process_impl(
pid,
Some(&profile_path_str),
)
.await
{
log::error!("Failed to force kill Camoufox process {}: {}", pid, e);
} else {
// Verify the process is actually dead after force kill
use tokio::time::{sleep, Duration};
sleep(Duration::from_millis(500)).await;
use sysinfo::{Pid, System};
let system = System::new_all();
process_actually_stopped =
system.process(Pid::from(pid as usize)).is_none();
if process_actually_stopped {
log::info!(
"Successfully force killed Camoufox process {} (PID: {:?})",
camoufox_process.id,
pid
);
}
}
}
#[cfg(target_os = "linux")]
{
use crate::platform_browser;
if let Err(e) = platform_browser::linux::kill_browser_process_impl(
pid,
Some(&profile_path_str),
)
.await
{
log::error!("Failed to force kill Camoufox process {}: {}", pid, e);
} else {
// Verify the process is actually dead after force kill
use tokio::time::{sleep, Duration};
sleep(Duration::from_millis(500)).await;
use sysinfo::{Pid, System};
let system = System::new_all();
process_actually_stopped =
system.process(Pid::from(pid as usize)).is_none();
if process_actually_stopped {
log::info!(
"Successfully force killed Camoufox process {} (PID: {:?})",
camoufox_process.id,
pid
);
}
}
}
#[cfg(target_os = "windows")]
{
use crate::platform_browser;
if let Err(e) =
platform_browser::windows::kill_browser_process_impl(pid).await
{
log::error!("Failed to force kill Camoufox process {}: {}", pid, e);
} else {
// Verify the process is actually dead after force kill
use tokio::time::{sleep, Duration};
sleep(Duration::from_millis(500)).await;
use sysinfo::{Pid, System};
let system = System::new_all();
process_actually_stopped =
system.process(Pid::from(pid as usize)).is_none();
if process_actually_stopped {
log::info!(
"Successfully force killed Camoufox process {} (PID: {:?})",
camoufox_process.id,
pid
);
}
}
}
}
} else {
// stop_camoufox returned false, try to force kill the process
log::warn!(
"Camoufox stop command returned false for process {} (PID: {:?}) - attempting force kill",
camoufox_process.id,
pid
);
#[cfg(target_os = "macos")]
{
use crate::platform_browser;
if let Err(e) = platform_browser::macos::kill_browser_process_impl(
pid,
Some(&profile_path_str),
)
.await
{
log::error!("Failed to force kill Camoufox process {}: {}", pid, e);
} else {
// Verify the process is actually dead after force kill
use tokio::time::{sleep, Duration};
sleep(Duration::from_millis(500)).await;
use sysinfo::{Pid, System};
let system = System::new_all();
process_actually_stopped = system.process(Pid::from(pid as usize)).is_none();
if process_actually_stopped {
log::info!(
"Successfully force killed Camoufox process {} (PID: {:?})",
camoufox_process.id,
pid
);
}
}
}
#[cfg(target_os = "linux")]
{
use crate::platform_browser;
if let Err(e) = platform_browser::linux::kill_browser_process_impl(
pid,
Some(&profile_path_str),
)
.await
{
log::error!("Failed to force kill Camoufox process {}: {}", pid, e);
} else {
// Verify the process is actually dead after force kill
use tokio::time::{sleep, Duration};
sleep(Duration::from_millis(500)).await;
use sysinfo::{Pid, System};
let system = System::new_all();
process_actually_stopped = system.process(Pid::from(pid as usize)).is_none();
if process_actually_stopped {
log::info!(
"Successfully force killed Camoufox process {} (PID: {:?})",
camoufox_process.id,
pid
);
}
}
}
#[cfg(target_os = "windows")]
{
use crate::platform_browser;
if let Err(e) = platform_browser::windows::kill_browser_process_impl(pid).await
{
log::error!("Failed to force kill Camoufox process {}: {}", pid, e);
} else {
// Verify the process is actually dead after force kill
use tokio::time::{sleep, Duration};
sleep(Duration::from_millis(500)).await;
use sysinfo::{Pid, System};
let system = System::new_all();
process_actually_stopped = system.process(Pid::from(pid as usize)).is_none();
if process_actually_stopped {
log::info!(
"Successfully force killed Camoufox process {} (PID: {:?})",
camoufox_process.id,
pid
);
}
}
}
}
} else {
// No PID available, assume stopped if stop_camoufox returned true
process_actually_stopped = stopped;
if !stopped {
log::warn!(
"Failed to stop Camoufox process {} but no PID available for force kill",
camoufox_process.id
);
}
}
}
Err(e) => {
log::error!(
"Error stopping Camoufox process {}: {}",
camoufox_process.id,
e
);
// Try to force kill if we have a PID
if let Some(pid) = camoufox_process.processId {
log::info!(
"Attempting force kill after stop_camoufox error for PID: {}",
pid
);
#[cfg(target_os = "macos")]
{
use crate::platform_browser;
if let Err(kill_err) =
platform_browser::macos::kill_browser_process_impl(pid, Some(&profile_path_str))
.await
{
log::error!(
"Failed to force kill Camoufox process {}: {}",
pid,
kill_err
);
} else {
use tokio::time::{sleep, Duration};
sleep(Duration::from_millis(500)).await;
use sysinfo::{Pid, System};
let system = System::new_all();
process_actually_stopped = system.process(Pid::from(pid as usize)).is_none();
}
}
#[cfg(target_os = "linux")]
{
use crate::platform_browser;
if let Err(kill_err) =
platform_browser::linux::kill_browser_process_impl(pid, Some(&profile_path_str))
.await
{
log::error!(
"Failed to force kill Camoufox process {}: {}",
pid,
kill_err
);
} else {
use tokio::time::{sleep, Duration};
sleep(Duration::from_millis(500)).await;
use sysinfo::{Pid, System};
let system = System::new_all();
process_actually_stopped = system.process(Pid::from(pid as usize)).is_none();
}
}
#[cfg(target_os = "windows")]
{
use crate::platform_browser;
if let Err(kill_err) =
platform_browser::windows::kill_browser_process_impl(pid).await
{
log::error!(
"Failed to force kill Camoufox process {}: {}",
pid,
kill_err
);
} else {
use tokio::time::{sleep, Duration};
sleep(Duration::from_millis(500)).await;
use sysinfo::{Pid, System};
let system = System::new_all();
process_actually_stopped = system.process(Pid::from(pid as usize)).is_none();
}
}
}
}
}
}
Ok(None) => {
log::info!(
"No running Camoufox process found for profile: {} (ID: {})",
profile.name,
profile.id
);
process_actually_stopped = true; // No process found, consider it stopped
}
Err(e) => {
log::error!(
"Error finding Camoufox process for profile {}: {}",
profile.name,
e
);
}
}
// If process wasn't confirmed stopped, return an error
if !process_actually_stopped {
log::error!(
"Failed to stop Camoufox process for profile: {} (ID: {}) - process may still be running",
profile.name,
profile.id
);
return Err(
format!(
"Failed to stop Camoufox process for profile {} - process may still be running",
profile.name
)
.into(),
);
}
// Clear the process ID from the profile
let mut updated_profile = profile.clone();
updated_profile.process_id = None;
// Check for pending updates and apply them for Camoufox profiles too
if let Ok(Some(pending_update)) = self
.auto_updater
.get_pending_update(&profile.browser, &profile.version)
{
log::info!(
"Found pending update for Camoufox profile {}: {} -> {}",
profile.name,
profile.version,
pending_update.new_version
);
// Update the profile to the new version
match self.profile_manager.update_profile_version(
&app_handle,
&profile.id.to_string(),
&pending_update.new_version,
) {
Ok(updated_profile_after_update) => {
log::info!(
"Successfully updated Camoufox profile {} from version {} to {}",
profile.name,
profile.version,
pending_update.new_version
);
updated_profile = updated_profile_after_update;
// Remove the pending update from the auto updater state
if let Err(e) = self
.auto_updater
.dismiss_update_notification(&pending_update.id)
{
log::warn!("Warning: Failed to dismiss pending update notification: {e}");
}
}
Err(e) => {
log::error!(
"Failed to apply pending update for Camoufox profile {}: {}",
profile.name,
e
);
// Continue with the original profile update (just clearing process_id)
}
}
}
self
.save_process_info(&updated_profile)
.map_err(|e| format!("Failed to update profile: {e}"))?;
log::info!(
"Emitting profile events for successful Camoufox kill: {}",
updated_profile.name
);
// Emit profile update event to frontend
if let Err(e) = events::emit("profile-updated", &updated_profile) {
log::warn!("Warning: Failed to emit profile update event: {e}");
}
// Emit minimal running changed event to frontend immediately
#[derive(Serialize)]
struct RunningChangedPayload {
id: String,
is_running: bool,
}
let payload = RunningChangedPayload {
id: updated_profile.id.to_string(),
is_running: false, // Explicitly set to false since we just killed it
};
if let Err(e) = events::emit("profile-running-changed", &payload) {
log::warn!("Warning: Failed to emit profile running changed event: {e}");
} else {
log::info!(
"Successfully emitted profile-running-changed event for Camoufox {}: running={}",
updated_profile.name,
payload.is_running
);
}
if profile.ephemeral {
crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string());
}
log::info!(
"Camoufox process cleanup completed for profile: {} (ID: {})",
profile.name,
profile.id
);
// Consolidate browser versions after stopping a browser
if let Ok(consolidated) = self
.downloaded_browsers_registry
.consolidate_browser_versions(&app_handle)
{
if !consolidated.is_empty() {
log::info!("Post-stop version consolidation results:");
for action in &consolidated {
log::info!(" {action}");
}
}
}
return Ok(());
}
// Handle Wayfern profiles using WayfernManager
if profile.browser == "wayfern" {
let profiles_dir = self.profile_manager.get_profiles_dir();
let profile_data_path =
crate::ephemeral_dirs::get_effective_profile_path(profile, &profiles_dir);
let profile_path_str = profile_data_path.to_string_lossy();
log::info!(
"Attempting to kill Wayfern process for profile: {} (ID: {})",
profile.name,
profile.id
);
// Stop the proxy associated with this profile first
let profile_id_str = profile.id.to_string();
if let Err(e) = PROXY_MANAGER
.stop_proxy_by_profile_id(app_handle.clone(), &profile_id_str)
.await
{
log::warn!(
"Warning: Failed to stop proxy for profile {}: {e}",
profile_id_str
);
}
let mut process_actually_stopped = false;
match self
.wayfern_manager
.find_wayfern_by_profile(&profile_path_str)
.await
{
Some(wayfern_process) => {
log::info!(
"Found Wayfern process: {} (PID: {:?})",
wayfern_process.id,
wayfern_process.processId
);
match self.wayfern_manager.stop_wayfern(&wayfern_process.id).await {
Ok(_) => {
if let Some(pid) = wayfern_process.processId {
// Verify the process actually died by checking after a short delay
use tokio::time::{sleep, Duration};
sleep(Duration::from_millis(500)).await;
use sysinfo::{Pid, System};
let system = System::new_all();
process_actually_stopped = system.process(Pid::from(pid as usize)).is_none();
if process_actually_stopped {
log::info!(
"Successfully stopped Wayfern process: {} (PID: {:?}) - verified process is dead",
wayfern_process.id,
pid
);
} else {
log::warn!(
"Wayfern stop command returned success but process {} (PID: {:?}) is still running - forcing kill",
wayfern_process.id,
pid
);
// Force kill the process
#[cfg(target_os = "macos")]
{
use crate::platform_browser;
if let Err(e) = platform_browser::macos::kill_browser_process_impl(
pid,
Some(&profile_path_str),
)
.await
{
log::error!("Failed to force kill Wayfern process {}: {}", pid, e);
} else {
sleep(Duration::from_millis(500)).await;
let system = System::new_all();
process_actually_stopped = system.process(Pid::from(pid as usize)).is_none();
if process_actually_stopped {
log::info!(
"Successfully force killed Wayfern process {} (PID: {:?})",
wayfern_process.id,
pid
);
}
}
}
#[cfg(target_os = "linux")]
{
use crate::platform_browser;
if let Err(e) = platform_browser::linux::kill_browser_process_impl(
pid,
Some(&profile_path_str),
)
.await
{
log::error!("Failed to force kill Wayfern process {}: {}", pid, e);
} else {
sleep(Duration::from_millis(500)).await;
let system = System::new_all();
process_actually_stopped = system.process(Pid::from(pid as usize)).is_none();
if process_actually_stopped {
log::info!(
"Successfully force killed Wayfern process {} (PID: {:?})",
wayfern_process.id,
pid
);
}
}
}
#[cfg(target_os = "windows")]
{
use crate::platform_browser;
if let Err(e) = platform_browser::windows::kill_browser_process_impl(pid).await
{
log::error!("Failed to force kill Wayfern process {}: {}", pid, e);
} else {
sleep(Duration::from_millis(500)).await;
let system = System::new_all();
process_actually_stopped = system.process(Pid::from(pid as usize)).is_none();
if process_actually_stopped {
log::info!(
"Successfully force killed Wayfern process {} (PID: {:?})",
wayfern_process.id,
pid
);
}
}
}
}
} else {
process_actually_stopped = true;
}
}
Err(e) => {
log::error!(
"Error stopping Wayfern process {}: {}",
wayfern_process.id,
e
);
// Try to force kill if we have a PID
if let Some(pid) = wayfern_process.processId {
log::info!(
"Attempting force kill after stop_wayfern error for PID: {}",
pid
);
#[cfg(target_os = "macos")]
{
use crate::platform_browser;
if let Err(kill_err) =
platform_browser::macos::kill_browser_process_impl(pid, Some(&profile_path_str))
.await
{
log::error!("Failed to force kill Wayfern process {}: {}", pid, kill_err);
} else {
use tokio::time::{sleep, Duration};
sleep(Duration::from_millis(500)).await;
use sysinfo::{Pid, System};
let system = System::new_all();
process_actually_stopped = system.process(Pid::from(pid as usize)).is_none();
}
}
#[cfg(target_os = "linux")]
{
use crate::platform_browser;
if let Err(kill_err) =
platform_browser::linux::kill_browser_process_impl(pid, Some(&profile_path_str))
.await
{
log::error!("Failed to force kill Wayfern process {}: {}", pid, kill_err);
} else {
use tokio::time::{sleep, Duration};
sleep(Duration::from_millis(500)).await;
use sysinfo::{Pid, System};
let system = System::new_all();
process_actually_stopped = system.process(Pid::from(pid as usize)).is_none();
}
}
#[cfg(target_os = "windows")]
{
use crate::platform_browser;
if let Err(kill_err) =
platform_browser::windows::kill_browser_process_impl(pid).await
{
log::error!("Failed to force kill Wayfern process {}: {}", pid, kill_err);
} else {
use tokio::time::{sleep, Duration};
sleep(Duration::from_millis(500)).await;
use sysinfo::{Pid, System};
let system = System::new_all();
process_actually_stopped = system.process(Pid::from(pid as usize)).is_none();
}
}
}
}
}
}
None => {
log::info!(
"No running Wayfern process found for profile: {} (ID: {})",
profile.name,
profile.id
);
process_actually_stopped = true;
}
}
// If process wasn't confirmed stopped, return an error
if !process_actually_stopped {
log::error!(
"Failed to stop Wayfern process for profile: {} (ID: {}) - process may still be running",
profile.name,
profile.id
);
return Err(
format!(
"Failed to stop Wayfern process for profile {} - process may still be running",
profile.name
)
.into(),
);
}
// Clear the process ID from the profile
let mut updated_profile = profile.clone();
updated_profile.process_id = None;
// Check for pending updates and apply them
if let Ok(Some(pending_update)) = self
.auto_updater
.get_pending_update(&profile.browser, &profile.version)
{
log::info!(
"Found pending update for Wayfern profile {}: {} -> {}",
profile.name,
profile.version,
pending_update.new_version
);
match self.profile_manager.update_profile_version(
&app_handle,
&profile.id.to_string(),
&pending_update.new_version,
) {
Ok(updated_profile_after_update) => {
log::info!(
"Successfully updated Wayfern profile {} from version {} to {}",
profile.name,
profile.version,
pending_update.new_version
);
updated_profile = updated_profile_after_update;
if let Err(e) = self
.auto_updater
.dismiss_update_notification(&pending_update.id)
{
log::warn!("Warning: Failed to dismiss pending update notification: {e}");
}
}
Err(e) => {
log::error!(
"Failed to apply pending update for Wayfern profile {}: {}",
profile.name,
e
);
}
}
}
self
.save_process_info(&updated_profile)
.map_err(|e| format!("Failed to update profile: {e}"))?;
log::info!(
"Emitting profile events for successful Wayfern kill: {}",
updated_profile.name
);
// Emit profile update event to frontend
if let Err(e) = events::emit("profile-updated", &updated_profile) {
log::warn!("Warning: Failed to emit profile update event: {e}");
}
// Emit minimal running changed event
#[derive(Serialize)]
struct RunningChangedPayload {
id: String,
is_running: bool,
}
let payload = RunningChangedPayload {
id: updated_profile.id.to_string(),
is_running: false,
};
if let Err(e) = events::emit("profile-running-changed", &payload) {
log::warn!("Warning: Failed to emit profile running changed event: {e}");
} else {
log::info!(
"Successfully emitted profile-running-changed event for Wayfern {}: running={}",
updated_profile.name,
payload.is_running
);
}
if profile.ephemeral {
crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string());
}
log::info!(
"Wayfern process cleanup completed for profile: {} (ID: {})",
profile.name,
profile.id
);
// Consolidate browser versions after stopping a browser
if let Ok(consolidated) = self
.downloaded_browsers_registry
.consolidate_browser_versions(&app_handle)
{
if !consolidated.is_empty() {
log::info!("Post-stop version consolidation results:");
for action in &consolidated {
log::info!(" {action}");
}
}
}
return Ok(());
}
// For non-camoufox/wayfern browsers, use the existing logic
let pid = if let Some(pid) = profile.process_id {
// First verify the stored PID is still valid and belongs to our profile
let system = System::new_all();
if let Some(process) = system.process(sysinfo::Pid::from(pid as usize)) {
let cmd = process.cmd();
let exe_name = process.name().to_string_lossy();
// Verify this process is actually our browser
let is_correct_browser = match profile.browser.as_str() {
"firefox" => {
exe_name.contains("firefox")
&& !exe_name.contains("developer")
&& !exe_name.contains("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"
}
"zen" => exe_name.contains("zen"),
"chromium" => exe_name.contains("chromium") || exe_name.contains("chrome"),
"brave" => exe_name.contains("brave") || exe_name.contains("Brave"),
_ => false,
};
if is_correct_browser {
// Verify profile path match
let profiles_dir = self.profile_manager.get_profiles_dir();
let profile_data_path = profile.get_profile_data_path(&profiles_dir);
let profile_data_path_str = profile_data_path.to_string_lossy();
let profile_path_match = if matches!(
profile.browser.as_str(),
"firefox" | "firefox-developer" | "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 {
log::info!(
"Verified stored PID {} is valid for profile {} (ID: {})",
pid,
profile.name,
profile.id
);
pid
} else {
log::info!("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 {
log::info!("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 {
log::info!(
"Stored PID {} is no longer valid for profile {} (ID: {}), searching for correct process",
pid,
profile.name,
profile.id
);
// Fall through to search for correct process
self.find_browser_process_by_profile(profile)?
}
} else {
// No stored PID, search for the process
self.find_browser_process_by_profile(profile)?
};
log::info!("Attempting to kill browser process with PID: {pid}");
// Stop any associated proxy first
if let Err(e) = PROXY_MANAGER.stop_proxy(app_handle.clone(), pid).await {
log::warn!("Warning: Failed to stop proxy for PID {pid}: {e}");
}
#[cfg(target_os = "macos")]
{
let profiles_dir = self.profile_manager.get_profiles_dir();
let profile_data_path = profile.get_profile_data_path(&profiles_dir);
let profile_path_str = profile_data_path.to_string_lossy().to_string();
platform_browser::macos::kill_browser_process_impl(pid, Some(&profile_path_str)).await?;
}
#[cfg(target_os = "windows")]
platform_browser::windows::kill_browser_process_impl(pid).await?;
#[cfg(target_os = "linux")]
{
let profiles_dir = self.profile_manager.get_profiles_dir();
let profile_data_path = profile.get_profile_data_path(&profiles_dir);
let profile_path_str = profile_data_path.to_string_lossy().to_string();
platform_browser::linux::kill_browser_process_impl(pid, Some(&profile_path_str)).await?;
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
return Err("Unsupported platform".into());
let system = System::new_all();
if system.process(sysinfo::Pid::from(pid as usize)).is_some() {
log::error!(
"Browser process {} is still running after kill attempt for profile: {} (ID: {})",
pid,
profile.name,
profile.id
);
return Err(
format!(
"Browser process {} is still running after kill attempt",
pid
)
.into(),
);
}
log::info!(
"Verified browser process {} is terminated for profile: {} (ID: {})",
pid,
profile.name,
profile.id
);
// Clear the process ID from the profile
let mut updated_profile = profile.clone();
updated_profile.process_id = None;
// Check for pending updates and apply them
if let Ok(Some(pending_update)) = self
.auto_updater
.get_pending_update(&profile.browser, &profile.version)
{
log::info!(
"Found pending update for profile {}: {} -> {}",
profile.name,
profile.version,
pending_update.new_version
);
// Update the profile to the new version
match self.profile_manager.update_profile_version(
&app_handle,
&profile.id.to_string(),
&pending_update.new_version,
) {
Ok(updated_profile_after_update) => {
log::info!(
"Successfully updated profile {} from version {} to {}",
profile.name,
profile.version,
pending_update.new_version
);
updated_profile = updated_profile_after_update;
// Remove the pending update from the auto updater state
if let Err(e) = self
.auto_updater
.dismiss_update_notification(&pending_update.id)
{
log::warn!("Warning: Failed to dismiss pending update notification: {e}");
}
}
Err(e) => {
log::error!(
"Failed to apply pending update for profile {}: {}",
profile.name,
e
);
// Continue with the original profile update (just clearing process_id)
}
}
}
self
.save_process_info(&updated_profile)
.map_err(|e| format!("Failed to update profile: {e}"))?;
log::info!(
"Emitting profile events for successful kill: {}",
updated_profile.name
);
// Emit profile update event to frontend
if let Err(e) = events::emit("profile-updated", &updated_profile) {
log::warn!("Warning: Failed to emit profile update event: {e}");
}
// Emit minimal running changed event to frontend immediately
#[derive(Serialize)]
struct RunningChangedPayload {
id: String,
is_running: bool,
}
let payload = RunningChangedPayload {
id: updated_profile.id.to_string(),
is_running: false, // Explicitly set to false since we just killed it
};
if let Err(e) = events::emit("profile-running-changed", &payload) {
log::warn!("Warning: Failed to emit profile running changed event: {e}");
} else {
log::info!(
"Successfully emitted profile-running-changed event for {}: running={}",
updated_profile.name,
payload.is_running
);
}
// Consolidate browser versions after stopping a browser
if let Ok(consolidated) = self
.downloaded_browsers_registry
.consolidate_browser_versions(&app_handle)
{
if !consolidated.is_empty() {
log::info!("Post-stop version consolidation results:");
for action in &consolidated {
log::info!(" {action}");
}
}
}
Ok(())
}
/// Helper method to find browser process by profile path
fn find_browser_process_by_profile(
&self,
profile: &BrowserProfile,
) -> Result<u32, Box<dyn std::error::Error + Send + Sync>> {
let system = System::new_all();
let profiles_dir = self.profile_manager.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();
log::info!(
"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("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"
}
"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" | "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();
log::info!(
"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(),
)
}
pub async fn open_url_with_profile(
&self,
app_handle: tauri::AppHandle,
profile_id: String,
url: String,
) -> Result<(), String> {
// Get the profile by name
let profiles = self
.profile_manager
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
let profile = profiles
.into_iter()
.find(|p| p.id.to_string() == profile_id)
.ok_or_else(|| format!("Profile '{profile_id}' not found"))?;
if profile.is_cross_os() {
return Err(format!(
"Cannot open URL with profile '{}': it was created on {} and is not supported on this system",
profile.name,
profile.host_os.as_deref().unwrap_or("unknown")
));
}
log::info!("Opening URL '{url}' with profile '{profile_id}'");
// Use launch_or_open_url which handles both launching new instances and opening in existing ones
self
.launch_or_open_url(app_handle, &profile, Some(url.clone()), None)
.await
.map_err(|e| {
log::info!("Failed to open URL with profile '{profile_id}': {e}");
format!("Failed to open URL with profile: {e}")
})?;
log::info!("Successfully opened URL '{url}' with profile '{profile_id}'");
Ok(())
}
}
#[tauri::command]
pub async fn launch_browser_profile(
app_handle: tauri::AppHandle,
profile: BrowserProfile,
url: Option<String>,
) -> Result<BrowserProfile, String> {
log::info!(
"Launch request received for profile: {} (ID: {})",
profile.name,
profile.id
);
if profile.is_cross_os() {
return Err(format!(
"Cannot launch profile '{}': it was created on {} and is not supported on this system",
profile.name,
profile.host_os.as_deref().unwrap_or("unknown")
));
}
let browser_runner = BrowserRunner::instance();
// Store the internal proxy settings for passing to launch_browser
let mut internal_proxy_settings: Option<ProxySettings> = None;
// Resolve the most up-to-date profile from disk by ID to avoid using stale proxy_id/browser state
let profile_for_launch = match browser_runner
.profile_manager
.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);
}
};
log::info!(
"Resolved profile for launch: {} (ID: {})",
profile_for_launch.name,
profile_for_launch.id
);
// Always start a local proxy before launching (non-Camoufox/Wayfern handled here; they have their own flow)
// This ensures all traffic goes through the local proxy for monitoring and future features
if profile.browser != "camoufox" && profile.browser != "wayfern" {
// Determine upstream proxy if configured; otherwise use DIRECT (no upstream)
let mut upstream_proxy = profile_for_launch
.proxy_id
.as_ref()
.and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id));
// If profile has a VPN instead of proxy, start VPN worker and use it as upstream
if upstream_proxy.is_none() {
if let Some(ref vpn_id) = profile_for_launch.vpn_id {
match crate::vpn_worker_runner::start_vpn_worker(vpn_id).await {
Ok(vpn_worker) => {
if let Some(port) = vpn_worker.local_port {
upstream_proxy = Some(ProxySettings {
proxy_type: "socks5".to_string(),
host: "127.0.0.1".to_string(),
port,
username: None,
password: None,
});
log::info!("VPN worker started for profile on port {}", port);
}
}
Err(e) => {
return Err(format!("Failed to start VPN worker: {e}"));
}
}
}
}
// Use a temporary PID (1) to start the proxy, we'll update it after browser launch
let temp_pid = 1u32;
let profile_id_str = profile.id.to_string();
// Always start a local proxy, even if there's no upstream proxy
// This allows for traffic monitoring and future features
match PROXY_MANAGER
.start_proxy(
app_handle.clone(),
upstream_proxy.as_ref(),
temp_pid,
Some(&profile_id_str),
profile_for_launch.proxy_bypass_rules.clone(),
)
.await
{
Ok(internal_proxy) => {
// Use internal proxy for subsequent launch
internal_proxy_settings = Some(internal_proxy.clone());
// For Firefox-based browsers, always apply PAC/user.js to point to the local proxy
if matches!(
profile_for_launch.browser.as_str(),
"firefox" | "firefox-developer" | "zen"
) {
let profiles_dir = browser_runner.profile_manager.get_profiles_dir();
let profile_path = profiles_dir
.join(profile_for_launch.id.to_string())
.join("profile");
// Provide a dummy upstream (ignored when internal proxy is provided)
let dummy_upstream = ProxySettings {
proxy_type: "http".to_string(),
host: "127.0.0.1".to_string(),
port: internal_proxy.port,
username: None,
password: None,
};
browser_runner
.profile_manager
.apply_proxy_settings_to_profile(&profile_path, &dummy_upstream, Some(&internal_proxy))
.map_err(|e| format!("Failed to update profile proxy: {e}"))?;
}
log::info!(
"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) => {
let error_msg = format!("Failed to start local proxy: {e}");
log::error!("{}", error_msg);
// DO NOT launch browser if proxy startup fails - all browsers must use local proxy
return Err(error_msg);
}
}
}
log::info!(
"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| {
log::info!("Browser launch failed for profile: {}, error: {}", profile_for_launch.name, e);
// Emit a failure event to clear loading states in the frontend
#[derive(serde::Serialize)]
struct RunningChangedPayload {
id: String,
is_running: bool,
}
let payload = RunningChangedPayload {
id: profile_for_launch.id.to_string(),
is_running: false,
};
if let Err(e) = events::emit("profile-running-changed", &payload) {
log::warn!("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::<std::io::Error>() {
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}")
})?;
log::info!(
"Browser launch completed for profile: {} (ID: {})",
updated_profile.name,
updated_profile.id
);
// Now update the proxy with the correct PID if we have one
if let Some(actual_pid) = updated_profile.process_id {
// Update the proxy manager with the correct PID (we always started with temp pid 1 for non-Camoufox)
let _ = PROXY_MANAGER.update_proxy_pid(1u32, actual_pid);
}
Ok(updated_profile)
}
#[tauri::command]
pub fn check_browser_exists(browser_str: String, version: String) -> bool {
// This is an alias for is_browser_downloaded to provide clearer semantics for auto-updates
let runner = BrowserRunner::instance();
runner
.downloaded_browsers_registry
.is_browser_downloaded(&browser_str, &version)
}
#[tauri::command]
pub async fn kill_browser_profile(
app_handle: tauri::AppHandle,
profile: BrowserProfile,
) -> Result<(), String> {
log::info!(
"Kill request received for profile: {} (ID: {})",
profile.name,
profile.id
);
let browser_runner = BrowserRunner::instance();
match browser_runner
.kill_browser_process(app_handle.clone(), &profile)
.await
{
Ok(()) => {
log::info!(
"Successfully killed browser profile: {} (ID: {})",
profile.name,
profile.id
);
// Auto-update non-running profiles and cleanup unused binaries
let browser_for_update = profile.browser.clone();
let app_handle_for_update = app_handle.clone();
tauri::async_runtime::spawn(async move {
let registry = crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance();
let mut versions = registry.get_downloaded_versions(&browser_for_update);
if !versions.is_empty() {
versions.sort_by(|a, b| crate::api_client::compare_versions(b, a));
let latest_version = &versions[0];
let auto_updater = crate::auto_updater::AutoUpdater::instance();
match auto_updater
.auto_update_profile_versions(
&app_handle_for_update,
&browser_for_update,
latest_version,
)
.await
{
Ok(updated) => {
if !updated.is_empty() {
log::info!(
"Auto-updated {} profiles after stop: {:?}",
updated.len(),
updated
);
}
}
Err(e) => {
log::error!("Failed to auto-update profile versions after stop: {e}");
}
}
}
match registry.cleanup_unused_binaries() {
Ok(cleaned) => {
if !cleaned.is_empty() {
log::info!("Cleaned up unused binaries after stop: {:?}", cleaned);
}
}
Err(e) => {
log::error!("Failed to cleanup unused binaries after stop: {e}");
}
}
});
Ok(())
}
Err(e) => {
log::info!("Failed to kill browser profile {}: {}", profile.name, e);
// Emit a failure event to clear loading states in the frontend
#[derive(serde::Serialize)]
struct RunningChangedPayload {
id: String,
is_running: bool,
}
// On kill failure, we assume the process is still running
let payload = RunningChangedPayload {
id: profile.id.to_string(),
is_running: true,
};
if let Err(e) = events::emit("profile-running-changed", &payload) {
log::warn!("Warning: Failed to emit profile running changed event: {e}");
}
Err(format!("Failed to kill browser: {e}"))
}
}
}
pub async fn launch_browser_profile_with_debugging(
app_handle: tauri::AppHandle,
profile: BrowserProfile,
url: Option<String>,
remote_debugging_port: Option<u16>,
headless: bool,
) -> Result<BrowserProfile, String> {
if profile.is_cross_os() {
return Err(format!(
"Cannot launch profile '{}': it was created on {} and is not supported on this system",
profile.name,
profile.host_os.as_deref().unwrap_or("unknown")
));
}
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}"))
}
#[tauri::command]
pub async fn open_url_with_profile(
app_handle: tauri::AppHandle,
profile_id: String,
url: String,
) -> Result<(), String> {
let browser_runner = BrowserRunner::instance();
browser_runner
.open_url_with_profile(app_handle, profile_id, url)
.await
}
// Global singleton instance
lazy_static::lazy_static! {
static ref BROWSER_RUNNER: BrowserRunner = BrowserRunner::new();
}