From 7092f2155b6bab4ad7d0c420ce74e5f7c400cf22 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Tue, 24 Mar 2026 00:04:39 +0400 Subject: [PATCH] refactor: make sync more robust --- .vscode/settings.json | 6 ++ src-tauri/src/profile/manager.rs | 45 ---------- src-tauri/src/sync/engine.rs | 97 ++++++++++++++++++++- src-tauri/src/sync/manifest.rs | 12 ++- src-tauri/src/sync/scheduler.rs | 4 +- src-tauri/src/wayfern_manager.rs | 117 ++++++++++++++++++-------- src/components/sync-config-dialog.tsx | 38 +-------- 7 files changed, 198 insertions(+), 121 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 8c1e8f1..f0cae99 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,7 @@ "appindicator", "applescript", "asyncio", + "autocheckpoint", "autoconfig", "autologin", "biomejs", @@ -95,6 +96,7 @@ "langpack", "launchservices", "letterboxing", + "leveldb", "libappindicator", "libatk", "libayatana", @@ -114,6 +116,7 @@ "macchiato", "Matchalk", "maxminddb", + "minidumps", "minioadmin", "mmdb", "mountpoint", @@ -155,6 +158,7 @@ "plasmohq", "platformdirs", "prefs", + "presign", "PRIO", "propertylist", "psutil", @@ -179,6 +183,7 @@ "rusqlite", "rustc", "rwxr", + "safebrowsing", "SARIF", "scipy", "screeninfo", @@ -222,6 +227,7 @@ "titlebar", "tkinter", "tmpfs", + "tombstoned", "tqdm", "trackingprotection", "trailhead", diff --git a/src-tauri/src/profile/manager.rs b/src-tauri/src/profile/manager.rs index df21295..180b834 100644 --- a/src-tauri/src/profile/manager.rs +++ b/src-tauri/src/profile/manager.rs @@ -94,29 +94,6 @@ impl ProfileManager { crate::camoufox_manager::CamoufoxConfig::default() }); - // Always ensure executable_path is set to the user's binary location - if config.executable_path.is_none() { - let mut browser_dir = self.get_binaries_dir(); - browser_dir.push(browser); - browser_dir.push(version); - - #[cfg(target_os = "macos")] - let binary_path = browser_dir - .join("Camoufox.app") - .join("Contents") - .join("MacOS") - .join("camoufox"); - - #[cfg(target_os = "windows")] - let binary_path = browser_dir.join("camoufox.exe"); - - #[cfg(target_os = "linux")] - let binary_path = browser_dir.join("camoufox"); - - config.executable_path = Some(binary_path.to_string_lossy().to_string()); - log::info!("Set Camoufox executable path: {:?}", config.executable_path); - } - // Pass upstream proxy information to config for fingerprint generation if let Some(proxy_id_ref) = &proxy_id { if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { @@ -219,28 +196,6 @@ impl ProfileManager { }); // Always ensure executable_path is set to the user's binary location - if config.executable_path.is_none() { - let mut browser_dir = self.get_binaries_dir(); - browser_dir.push(browser); - browser_dir.push(version); - - #[cfg(target_os = "macos")] - let binary_path = browser_dir - .join("Chromium.app") - .join("Contents") - .join("MacOS") - .join("Chromium"); - - #[cfg(target_os = "windows")] - let binary_path = browser_dir.join("chrome.exe"); - - #[cfg(target_os = "linux")] - let binary_path = browser_dir.join("chrome"); - - config.executable_path = Some(binary_path.to_string_lossy().to_string()); - log::info!("Set Wayfern executable path: {:?}", config.executable_path); - } - // Pass upstream proxy information to config for fingerprint generation if let Some(proxy_id_ref) = &proxy_id { if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { diff --git a/src-tauri/src/sync/engine.rs b/src-tauri/src/sync/engine.rs index 123cfec..3b1933b 100644 --- a/src-tauri/src/sync/engine.rs +++ b/src-tauri/src/sync/engine.rs @@ -332,11 +332,11 @@ impl SyncEngine { ) -> SyncResult<()> { if profile.is_cross_os() { log::info!( - "Skipping file sync for cross-OS profile: {} ({})", + "Cross-OS profile: {} ({}) — syncing metadata only", profile.name, profile.id ); - return Ok(()); + return self.sync_cross_os_metadata(app_handle, profile).await; } // Skip team profiles for self-hosted sync @@ -727,6 +727,63 @@ impl SyncEngine { Ok(profile) } + /// Sync only metadata for cross-OS profiles (tags, notes, proxies, groups). + /// No browser files are synced. + async fn sync_cross_os_metadata( + &self, + app_handle: &tauri::AppHandle, + profile: &BrowserProfile, + ) -> SyncResult<()> { + let profile_id = profile.id.to_string(); + let key_prefix = Self::get_team_key_prefix(profile).await; + let profile_manager = ProfileManager::instance(); + + // Upload our metadata + self + .upload_profile_metadata(&profile_id, profile, &key_prefix) + .await?; + + // Download remote metadata and merge if remote has changes + let remote_metadata_key = format!("{}profiles/{}/metadata.json", key_prefix, profile_id); + if let Ok(remote_meta) = self.download_profile_metadata(&remote_metadata_key).await { + let mut updated = profile.clone(); + updated.name = remote_meta.name; + updated.tags = remote_meta.tags; + updated.note = remote_meta.note; + updated.proxy_id = remote_meta.proxy_id; + updated.vpn_id = remote_meta.vpn_id; + updated.group_id = remote_meta.group_id; + updated.last_sync = Some( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + ); + let _ = profile_manager.save_profile(&updated); + } + + // Sync associated entities + if let Some(proxy_id) = &profile.proxy_id { + let _ = self.sync_proxy(proxy_id, Some(app_handle)).await; + } + if let Some(group_id) = &profile.group_id { + let _ = self.sync_group(group_id, Some(app_handle)).await; + } + + let _ = events::emit("profiles-changed", ()); + let _ = events::emit( + "profile-sync-status", + serde_json::json!({ + "profile_id": profile_id, + "profile_name": profile.name, + "status": "synced" + }), + ); + + log::info!("Cross-OS profile {} metadata synced", profile_id); + Ok(()) + } + async fn upload_profile_metadata( &self, profile_id: &str, @@ -2284,6 +2341,42 @@ impl SyncEngine { .await?; } + // Verify critical files after download + let os_crypt_key_path = profile_dir.join("profile").join("os_crypt_key"); + let cookies_path = profile_dir.join("profile").join("Default").join("Cookies"); + if os_crypt_key_path.exists() { + let key_data = fs::read(&os_crypt_key_path).unwrap_or_default(); + log::info!( + "Profile {} sync: os_crypt_key present ({} bytes, sha256: {:x})", + profile_id, + key_data.len(), + { + use std::hash::{Hash, Hasher}; + let mut h = std::collections::hash_map::DefaultHasher::new(); + key_data.hash(&mut h); + h.finish() + } + ); + } else { + log::warn!( + "Profile {} sync: os_crypt_key NOT FOUND after download", + profile_id + ); + } + if cookies_path.exists() { + let cookies_meta = fs::metadata(&cookies_path).unwrap_or_else(|_| fs::metadata(".").unwrap()); + log::info!( + "Profile {} sync: Cookies present ({} bytes)", + profile_id, + cookies_meta.len() + ); + } else { + log::warn!( + "Profile {} sync: Cookies NOT FOUND after download", + profile_id + ); + } + // Set sync mode and save profile if profile.sync_mode == SyncMode::Disabled { profile.sync_mode = if manifest.encrypted { diff --git a/src-tauri/src/sync/manifest.rs b/src-tauri/src/sync/manifest.rs index 4f1088e..fac9400 100644 --- a/src-tauri/src/sync/manifest.rs +++ b/src-tauri/src/sync/manifest.rs @@ -13,7 +13,6 @@ use super::types::{SyncError, SyncResult}; /// Patterns use `**/` prefix to match at any directory depth, since the sync /// engine scans from `profiles/{uuid}/` which contains `profile/Default/...`. pub const DEFAULT_EXCLUDE_PATTERNS: &[&str] = &[ - // Chromium caches (re-downloadable / re-generated) "**/Cache/**", "**/Code Cache/**", "**/GPUCache/**", @@ -23,7 +22,6 @@ pub const DEFAULT_EXCLUDE_PATTERNS: &[&str] = &[ "**/DawnGraphiteCache/**", "**/Service Worker/CacheStorage/**", "**/Service Worker/ScriptCache/**", - // Chromium transient / volatile data "**/Session Storage/**", "**/blob_storage/**", "**/Crashpad/**", @@ -32,14 +30,12 @@ pub const DEFAULT_EXCLUDE_PATTERNS: &[&str] = &[ "**/optimization_guide_model_store/**", "**/Safe Browsing/**", "**/component_crx_cache/**", - // Firefox/Camoufox caches (re-downloadable / re-generated) "**/cache2/**", "**/startupCache/**", "**/safebrowsing/**", "**/storage/temporary/**", "**/crashes/**", "**/minidumps/**", - // Common volatile files "*.log", "*.tmp", "**/LOG", @@ -47,6 +43,14 @@ pub const DEFAULT_EXCLUDE_PATTERNS: &[&str] = &[ "**/LOCK", "**/*-journal", "**/*-wal", + "**/SingletonLock", + "**/SingletonSocket", + "**/SingletonCookie", + "**/Secure Preferences", + "**/GraphiteDawnCache/**", + "**/DawnWebGPUCache/**", + "**/BrowserMetrics*", + "**/.DS_Store", ".donut-sync/**", ]; diff --git a/src-tauri/src/sync/scheduler.rs b/src-tauri/src/sync/scheduler.rs index 574392c..f1c7f0f 100644 --- a/src-tauri/src/sync/scheduler.rs +++ b/src-tauri/src/sync/scheduler.rs @@ -264,7 +264,7 @@ impl SyncScheduler { let sync_enabled_profiles: Vec<_> = profiles .into_iter() - .filter(|p| p.is_sync_enabled() && !p.is_cross_os()) + .filter(|p| p.is_sync_enabled()) .collect(); if sync_enabled_profiles.is_empty() { @@ -418,7 +418,7 @@ impl SyncScheduler { profile_manager.list_profiles().ok().and_then(|profiles| { profiles .into_iter() - .find(|p| p.id.to_string() == profile_id && p.is_sync_enabled() && !p.is_cross_os()) + .find(|p| p.id.to_string() == profile_id && p.is_sync_enabled()) }) }; diff --git a/src-tauri/src/wayfern_manager.rs b/src-tauri/src/wayfern_manager.rs index 04054c9..3ff391b 100644 --- a/src-tauri/src/wayfern_manager.rs +++ b/src-tauri/src/wayfern_manager.rs @@ -37,8 +37,6 @@ pub struct WayfernConfig { pub block_webrtc: Option, #[serde(default)] pub block_webgl: Option, - #[serde(default)] - pub executable_path: Option, #[serde(default, skip_serializing)] pub proxy: Option, } @@ -212,21 +210,9 @@ impl WayfernManager { profile: &BrowserProfile, config: &WayfernConfig, ) -> Result> { - let executable_path = if let Some(path) = &config.executable_path { - let p = PathBuf::from(path); - if p.exists() { - p - } else { - log::warn!("Stored Wayfern executable path does not exist: {path}, falling back to dynamic resolution"); - BrowserRunner::instance() - .get_browser_executable_path(profile) - .map_err(|e| format!("Failed to get Wayfern executable path: {e}"))? - } - } else { - BrowserRunner::instance() - .get_browser_executable_path(profile) - .map_err(|e| format!("Failed to get Wayfern executable path: {e}"))? - }; + let executable_path = BrowserRunner::instance() + .get_browser_executable_path(profile) + .map_err(|e| format!("Failed to get Wayfern executable path: {e}"))?; let port = Self::find_free_port().await?; log::info!("Launching headless Wayfern on port {port} for fingerprint generation"); @@ -456,21 +442,9 @@ impl WayfernManager { extension_paths: &[String], remote_debugging_port: Option, ) -> Result> { - let executable_path = if let Some(path) = &config.executable_path { - let p = PathBuf::from(path); - if p.exists() { - p - } else { - log::warn!("Stored Wayfern executable path does not exist: {path}, falling back to dynamic resolution"); - BrowserRunner::instance() - .get_browser_executable_path(profile) - .map_err(|e| format!("Failed to get Wayfern executable path: {e}"))? - } - } else { - BrowserRunner::instance() - .get_browser_executable_path(profile) - .map_err(|e| format!("Failed to get Wayfern executable path: {e}"))? - }; + let executable_path = BrowserRunner::instance() + .get_browser_executable_path(profile) + .map_err(|e| format!("Failed to get Wayfern executable path: {e}"))?; let port = match remote_debugging_port { Some(p) => p, @@ -478,6 +452,84 @@ impl WayfernManager { }; log::info!("Launching Wayfern on CDP port {port}"); + // Diagnostic: verify critical profile files and test cookie decryption + { + let profile_path_buf = std::path::PathBuf::from(profile_path); + let key_path = profile_path_buf.join("os_crypt_key"); + let cookies_path = profile_path_buf.join("Default").join("Cookies"); + + if key_path.exists() { + let key_text = std::fs::read_to_string(&key_path).unwrap_or_default(); + log::info!( + "Pre-launch: os_crypt_key present ({} bytes, content: '{}')", + key_text.len(), + key_text.trim() + ); + } else { + log::warn!("Pre-launch: os_crypt_key NOT FOUND"); + } + + if cookies_path.exists() { + // Try to open Cookies DB and check if encrypted cookies can be decrypted + if let Ok(conn) = rusqlite::Connection::open_with_flags( + &cookies_path, + rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY, + ) { + let cookie_count: i64 = conn + .query_row( + "SELECT COUNT(*) FROM cookies WHERE length(encrypted_value) > 0", + [], + |r| r.get(0), + ) + .unwrap_or(0); + let total_count: i64 = conn + .query_row("SELECT COUNT(*) FROM cookies", [], |r| r.get(0)) + .unwrap_or(0); + log::info!( + "Pre-launch: Cookies DB has {} total cookies, {} encrypted", + total_count, + cookie_count + ); + + // Try decrypting one cookie using the cookie_manager + if let Some(encryption_key) = + crate::cookie_manager::chrome_decrypt::get_encryption_key(&profile_path_buf) + { + if let Ok(mut stmt) = conn.prepare( + "SELECT name, host_key, encrypted_value FROM cookies WHERE length(encrypted_value) > 0 LIMIT 1", + ) { + if let Ok(mut rows) = stmt.query([]) { + if let Ok(Some(row)) = rows.next() { + let name: String = row.get(0).unwrap_or_default(); + let host: String = row.get(1).unwrap_or_default(); + let encrypted: Vec = row.get(2).unwrap_or_default(); + let decrypted = + crate::cookie_manager::chrome_decrypt::decrypt( + &encrypted, + &encryption_key, + ); + match decrypted { + Some(val) => log::info!( + "Pre-launch: Cookie decryption SUCCEEDED for '{}' (host: {}, decrypted {} bytes)", + name, host, val.len() + ), + None => log::error!( + "Pre-launch: Cookie decryption FAILED for '{}' (host: {}, encrypted {} bytes)", + name, host, encrypted.len() + ), + } + } + } + } + } else { + log::error!("Pre-launch: Failed to derive encryption key from os_crypt_key"); + } + } + } else { + log::warn!("Pre-launch: Cookies NOT FOUND"); + } + } + let mut args = vec![ format!("--remote-debugging-port={port}"), "--remote-debugging-address=127.0.0.1".to_string(), @@ -492,7 +544,6 @@ impl WayfernManager { "--disable-session-crashed-bubble".to_string(), "--hide-crash-restore-bubble".to_string(), "--disable-infobars".to_string(), - "--disable-quic".to_string(), "--disable-features=DialMediaRouteProvider".to_string(), "--use-mock-keychain".to_string(), "--password-store=basic".to_string(), diff --git a/src/components/sync-config-dialog.tsx b/src/components/sync-config-dialog.tsx index 24bbc37..1430306 100644 --- a/src/components/sync-config-dialog.tsx +++ b/src/components/sync-config-dialog.tsx @@ -67,7 +67,9 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) { const [isVerifying, setIsVerifying] = useState(false); const [activeTab, setActiveTab] = useState("cloud"); - const [liveProxyUsage, setLiveProxyUsage] = useState(null); + const [_liveProxyUsage, setLiveProxyUsage] = useState( + null, + ); const [connectionStatus, setConnectionStatus] = useState< "unknown" | "testing" | "connected" | "error" @@ -300,40 +302,6 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) { })} - {liveProxyUsage && ( - <> -
- - Recurring Proxy Bandwidth - - - {Math.max( - 0, - liveProxyUsage.recurring_limit_mb - - liveProxyUsage.used_mb, - )}{" "} - / {liveProxyUsage.recurring_limit_mb} MB remaining - -
-
- - Extra Proxy Bandwidth - - - {Math.max( - 0, - liveProxyUsage.remaining_mb - - Math.max( - 0, - liveProxyUsage.recurring_limit_mb - - liveProxyUsage.used_mb, - ), - )}{" "} - / {liveProxyUsage.extra_limit_mb} MB remaining - -
- - )} {user.teamName && ( <>