feat: ephemeral profiles

This commit is contained in:
zhom
2026-02-22 10:17:25 +04:00
parent 2e193987df
commit 3732d3a6e1
21 changed files with 354 additions and 33 deletions
+1
View File
@@ -605,6 +605,7 @@ async fn create_profile(
camoufox_config,
wayfern_config,
request.group_id.clone(),
false,
)
.await
{
+1
View File
@@ -522,6 +522,7 @@ mod tests {
sync_enabled: false,
last_sync: None,
host_os: None,
ephemeral: false,
}
}
+35 -5
View File
@@ -229,6 +229,15 @@ impl BrowserRunner {
log::warn!("Failed to configure Camoufox search engine: {e}");
}
// 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
};
// Launch Camoufox browser
log::info!("Launching Camoufox for profile: {}", profile.name);
let camoufox_result = self
@@ -238,6 +247,7 @@ impl BrowserRunner {
updated_profile.clone(),
camoufox_config,
url,
override_profile_path,
)
.await
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
@@ -441,12 +451,19 @@ impl BrowserRunner {
);
}
// 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 = updated_profile.get_profile_data_path(&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();
// Get proxy URL from config
@@ -461,6 +478,7 @@ impl BrowserRunner {
&wayfern_config,
url.as_deref(),
proxy_url,
profile.ephemeral,
)
.await
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
@@ -793,7 +811,8 @@ impl BrowserRunner {
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 = profile.get_profile_data_path(&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
@@ -847,7 +866,8 @@ impl BrowserRunner {
// Handle Wayfern profiles using WayfernManager
if profile.browser == "wayfern" {
let profiles_dir = self.profile_manager.get_profiles_dir();
let profile_data_path = profile.get_profile_data_path(&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
@@ -1245,7 +1265,8 @@ impl BrowserRunner {
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 = profile.get_profile_data_path(&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!(
@@ -1663,6 +1684,10 @@ impl BrowserRunner {
);
}
if profile.ephemeral {
crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string());
}
log::info!(
"Camoufox process cleanup completed for profile: {} (ID: {})",
profile.name,
@@ -1688,7 +1713,8 @@ impl BrowserRunner {
// Handle Wayfern profiles using WayfernManager
if profile.browser == "wayfern" {
let profiles_dir = self.profile_manager.get_profiles_dir();
let profile_data_path = profile.get_profile_data_path(&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!(
@@ -1981,6 +2007,10 @@ impl BrowserRunner {
);
}
if profile.ephemeral {
crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string());
}
log::info!(
"Wayfern process cleanup completed for profile: {} (ID: {})",
profile.name,
+25 -2
View File
@@ -582,10 +582,15 @@ impl CamoufoxManager {
profile: BrowserProfile,
config: CamoufoxConfig,
url: Option<String>,
override_profile_path: Option<std::path::PathBuf>,
) -> Result<CamoufoxLaunchResult, String> {
// Get profile path
let profiles_dir = self.get_profiles_dir();
let profile_path = profile.get_profile_data_path(&profiles_dir);
let profile_path = if let Some(ref override_path) = override_profile_path {
override_path.clone()
} else {
let profiles_dir = self.get_profiles_dir();
profile.get_profile_data_path(&profiles_dir)
};
let profile_path_str = profile_path.to_string_lossy();
// Check if there's already a running instance for this profile
@@ -597,6 +602,24 @@ impl CamoufoxManager {
// Clean up any dead instances before launching
let _ = self.cleanup_dead_instances().await;
// For ephemeral profiles, write Firefox prefs to keep all data inside the profile dir
if override_profile_path.is_some() {
let cache_dir = profile_path.join("cache2");
let user_js_path = profile_path.join("user.js");
let prefs = format!(
concat!(
"user_pref(\"browser.cache.disk.parent_directory\", \"{}\");\n",
"user_pref(\"browser.cache.disk.enable\", false);\n",
"user_pref(\"browser.cache.memory.enable\", true);\n",
"user_pref(\"browser.privatebrowsing.autostart\", true);\n",
),
cache_dir.to_string_lossy().replace('\\', "\\\\"),
);
if let Err(e) = std::fs::write(&user_js_path, prefs) {
log::warn!("Failed to write ephemeral user.js: {e}");
}
}
self
.launch_camoufox(
&app_handle,
+159
View File
@@ -0,0 +1,159 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use crate::profile::BrowserProfile;
lazy_static::lazy_static! {
static ref EPHEMERAL_DIRS: Mutex<HashMap<String, PathBuf>> = Mutex::new(HashMap::new());
}
pub fn create_ephemeral_dir(profile_id: &str) -> Result<PathBuf, String> {
let dir_name = format!("donut-ephemeral-{profile_id}");
let dir_path = std::env::temp_dir().join(dir_name);
std::fs::create_dir_all(&dir_path).map_err(|e| format!("Failed to create ephemeral dir: {e}"))?;
EPHEMERAL_DIRS
.lock()
.map_err(|e| format!("Failed to lock ephemeral dirs: {e}"))?
.insert(profile_id.to_string(), dir_path.clone());
log::info!(
"Created ephemeral dir for profile {}: {}",
profile_id,
dir_path.display()
);
Ok(dir_path)
}
pub fn get_ephemeral_dir(profile_id: &str) -> Option<PathBuf> {
EPHEMERAL_DIRS.lock().ok()?.get(profile_id).cloned()
}
pub fn remove_ephemeral_dir(profile_id: &str) {
let dir = EPHEMERAL_DIRS
.lock()
.ok()
.and_then(|mut map| map.remove(profile_id));
if let Some(dir_path) = dir {
if dir_path.exists() {
if let Err(e) = std::fs::remove_dir_all(&dir_path) {
log::warn!("Failed to remove ephemeral dir {}: {e}", dir_path.display());
} else {
log::info!(
"Removed ephemeral dir for profile {}: {}",
profile_id,
dir_path.display()
);
}
}
}
}
pub fn cleanup_stale_dirs() {
let temp_dir = std::env::temp_dir();
let entries = match std::fs::read_dir(&temp_dir) {
Ok(entries) => entries,
Err(e) => {
log::warn!("Failed to read temp dir for ephemeral cleanup: {e}");
return;
}
};
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
if name.starts_with("donut-ephemeral-") && entry.path().is_dir() {
if let Err(e) = std::fs::remove_dir_all(entry.path()) {
log::warn!(
"Failed to clean up stale ephemeral dir {}: {e}",
entry.path().display()
);
} else {
log::info!("Cleaned up stale ephemeral dir: {}", entry.path().display());
}
}
}
}
}
pub fn get_effective_profile_path(profile: &BrowserProfile, profiles_dir: &Path) -> PathBuf {
if profile.ephemeral {
if let Some(dir) = get_ephemeral_dir(&profile.id.to_string()) {
return dir;
}
}
profile.get_profile_data_path(profiles_dir)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_profile(id: uuid::Uuid, ephemeral: bool) -> BrowserProfile {
BrowserProfile {
id,
name: "test".to_string(),
browser: "camoufox".to_string(),
version: "1.0".to_string(),
proxy_id: None,
vpn_id: None,
process_id: None,
last_launch: None,
release_type: "stable".to_string(),
camoufox_config: None,
wayfern_config: None,
group_id: None,
tags: Vec::new(),
note: None,
sync_enabled: false,
last_sync: None,
host_os: None,
ephemeral,
}
}
#[test]
fn test_ephemeral_dir_lifecycle() {
// Test create, get, effective path, remove, and cleanup all in sequence
// to avoid race conditions between parallel tests.
// 1. Create and get
let profile_id = uuid::Uuid::new_v4();
let id_str = profile_id.to_string();
let dir = create_ephemeral_dir(&id_str).unwrap();
assert!(dir.is_dir());
assert_eq!(get_ephemeral_dir(&id_str), Some(dir.clone()));
// 2. Effective path for ephemeral profile returns ephemeral dir
let ephemeral_profile = make_test_profile(profile_id, true);
let profiles_dir = std::env::temp_dir().join("test_profiles_ephemeral");
assert_eq!(
get_effective_profile_path(&ephemeral_profile, &profiles_dir),
dir
);
// 3. Remove cleans up dir and map entry
remove_ephemeral_dir(&id_str);
assert!(!dir.exists());
assert!(get_ephemeral_dir(&id_str).is_none());
// 4. Effective path for persistent profile returns normal path
let persistent_profile = make_test_profile(uuid::Uuid::new_v4(), false);
let expected = persistent_profile.get_profile_data_path(&profiles_dir);
assert_eq!(
get_effective_profile_path(&persistent_profile, &profiles_dir),
expected
);
// 5. Cleanup stale dirs
let stale_id = uuid::Uuid::new_v4().to_string();
let stale_dir = std::env::temp_dir().join(format!("donut-ephemeral-{stale_id}"));
std::fs::create_dir_all(&stale_dir).unwrap();
assert!(stale_dir.exists());
cleanup_stale_dirs();
assert!(!stale_dir.exists());
}
}
+4
View File
@@ -21,6 +21,7 @@ mod camoufox_manager;
mod default_browser;
mod downloaded_browsers_registry;
mod downloader;
mod ephemeral_dirs;
mod extraction;
mod geoip_downloader;
mod group_manager;
@@ -759,6 +760,9 @@ pub fn run() {
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_macos_permissions::init())
.setup(|app| {
// Clean up stale ephemeral profile dirs from previous sessions
ephemeral_dirs::cleanup_stale_dirs();
// Start the daemon for tray icon
if let Err(e) = daemon_spawn::ensure_daemon_running() {
log::warn!("Failed to start daemon: {e}");
+38 -10
View File
@@ -48,6 +48,7 @@ impl ProfileManager {
camoufox_config: Option<CamoufoxConfig>,
wayfern_config: Option<WayfernConfig>,
group_id: Option<String>,
ephemeral: bool,
) -> Result<BrowserProfile, Box<dyn std::error::Error>> {
if proxy_id.is_some() && vpn_id.is_some() {
return Err("Cannot set both proxy_id and vpn_id".into());
@@ -72,7 +73,9 @@ impl ProfileManager {
// Create profile directory with UUID and profile subdirectory
create_dir_all(&profile_uuid_dir)?;
create_dir_all(&profile_data_dir)?;
if !ephemeral {
create_dir_all(&profile_data_dir)?;
}
// For Camoufox profiles, generate fingerprint during creation
let final_camoufox_config = if browser == "camoufox" {
@@ -162,6 +165,7 @@ impl ProfileManager {
sync_enabled: false,
last_sync: None,
host_os: None,
ephemeral: false,
};
match self
@@ -277,6 +281,7 @@ impl ProfileManager {
sync_enabled: false,
last_sync: None,
host_os: None,
ephemeral: false,
};
match self
@@ -324,6 +329,7 @@ impl ProfileManager {
sync_enabled: false,
last_sync: None,
host_os: Some(get_host_os()),
ephemeral,
};
// Save profile info
@@ -337,16 +343,19 @@ impl ProfileManager {
log::info!("Profile '{name}' created successfully with ID: {profile_id}");
// Create user.js with common Firefox preferences and apply proxy settings if provided
if let Some(proxy_id_ref) = &proxy_id {
if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) {
self.apply_proxy_settings_to_profile(&profile_data_dir, &proxy_settings, None)?;
// Skip for ephemeral profiles since the data dir is created at launch time
if !ephemeral {
if let Some(proxy_id_ref) = &proxy_id {
if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) {
self.apply_proxy_settings_to_profile(&profile_data_dir, &proxy_settings, None)?;
} else {
// Proxy ID provided but not found, disable proxy
self.disable_proxy_settings_in_profile(&profile_data_dir)?;
}
} else {
// Proxy ID provided but not found, disable proxy
// Create user.js with common Firefox preferences but no proxy
self.disable_proxy_settings_in_profile(&profile_data_dir)?;
}
} else {
// Create user.js with common Firefox preferences but no proxy
self.disable_proxy_settings_in_profile(&profile_data_dir)?;
}
// Emit profile creation event
@@ -842,6 +851,7 @@ impl ProfileManager {
sync_enabled: false,
last_sync: None,
host_os: Some(get_host_os()),
ephemeral: false,
};
self.save_profile(&new_profile)?;
@@ -1290,7 +1300,8 @@ impl ProfileManager {
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
let launcher = self.camoufox_manager;
let profiles_dir = self.get_profiles_dir();
let profile_data_path = profile.get_profile_data_path(&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 there's a running Camoufox instance for this profile
@@ -1334,6 +1345,10 @@ impl ProfileManager {
}
Ok(None) => {
// No running instance found, clear process ID if set and stop proxy
if profile.ephemeral {
crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string());
}
let profiles_dir = self.get_profiles_dir();
let profile_uuid_dir = profiles_dir.join(profile.id.to_string());
let metadata_file = profile_uuid_dir.join("metadata.json");
@@ -1364,6 +1379,10 @@ impl ProfileManager {
Err(e) => {
// Error checking status, assume not running and clear process ID
log::warn!("Warning: Failed to check Camoufox status: {e}");
if profile.ephemeral {
crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string());
}
let profiles_dir = self.get_profiles_dir();
let profile_uuid_dir = profiles_dir.join(profile.id.to_string());
let metadata_file = profile_uuid_dir.join("metadata.json");
@@ -1405,7 +1424,8 @@ impl ProfileManager {
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
let manager = self.wayfern_manager;
let profiles_dir = self.get_profiles_dir();
let profile_data_path = profile.get_profile_data_path(&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 there's a running Wayfern instance for this profile
@@ -1449,6 +1469,10 @@ impl ProfileManager {
}
None => {
// No running instance found, clear process ID if set
if profile.ephemeral {
crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string());
}
let profiles_dir = self.get_profiles_dir();
let profile_uuid_dir = profiles_dir.join(profile.id.to_string());
let metadata_file = profile_uuid_dir.join("metadata.json");
@@ -1870,6 +1894,7 @@ pub async fn create_browser_profile_with_group(
camoufox_config: Option<CamoufoxConfig>,
wayfern_config: Option<WayfernConfig>,
group_id: Option<String>,
ephemeral: bool,
) -> Result<BrowserProfile, String> {
let profile_manager = ProfileManager::instance();
profile_manager
@@ -1884,6 +1909,7 @@ pub async fn create_browser_profile_with_group(
camoufox_config,
wayfern_config,
group_id,
ephemeral,
)
.await
.map_err(|e| format!("Failed to create profile: {e}"))
@@ -1984,6 +2010,7 @@ pub async fn create_browser_profile_new(
camoufox_config: Option<CamoufoxConfig>,
wayfern_config: Option<WayfernConfig>,
group_id: Option<String>,
ephemeral: Option<bool>,
) -> Result<BrowserProfile, String> {
let fingerprint_os = camoufox_config
.as_ref()
@@ -2010,6 +2037,7 @@ pub async fn create_browser_profile_new(
camoufox_config,
wayfern_config,
group_id,
ephemeral.unwrap_or(false),
)
.await
}
+2
View File
@@ -45,6 +45,8 @@ pub struct BrowserProfile {
pub last_sync: Option<u64>, // Timestamp of last successful sync (epoch seconds)
#[serde(default)]
pub host_os: Option<String>, // OS where profile was created ("macos", "windows", "linux")
#[serde(default)]
pub ephemeral: bool,
}
pub fn default_release_type() -> String {
+1
View File
@@ -557,6 +557,7 @@ impl ProfileImporter {
sync_enabled: false,
last_sync: None,
host_os: Some(crate::profile::types::get_host_os()),
ephemeral: false,
};
// Save the profile metadata
+8
View File
@@ -380,6 +380,7 @@ impl WayfernManager {
Ok(fingerprint_json)
}
#[allow(clippy::too_many_arguments)]
pub async fn launch_wayfern(
&self,
_app_handle: &AppHandle,
@@ -388,6 +389,7 @@ impl WayfernManager {
config: &WayfernConfig,
url: Option<&str>,
proxy_url: Option<&str>,
ephemeral: bool,
) -> Result<WayfernLaunchResult, Box<dyn std::error::Error + Send + Sync>> {
let executable_path = if let Some(path) = &config.executable_path {
let p = PathBuf::from(path);
@@ -432,6 +434,11 @@ impl WayfernManager {
args.push(format!("--proxy-server={proxy}"));
}
if ephemeral {
args.push(format!("--disk-cache-dir={}/cache", profile_path));
args.push("--incognito".to_string());
}
// Don't add URL to args - we'll navigate via CDP after setting fingerprint
// This ensures fingerprint is applied at navigation commit time
@@ -713,6 +720,7 @@ impl WayfernManager {
config,
url,
proxy_url,
profile.ephemeral,
)
.await
}