From 4b0ab6b73257ccc31a2477fa28ace0a4f84acf8a Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Mon, 9 Jun 2025 06:04:26 +0400 Subject: [PATCH] refactor: windows pipeline test --- src-tauri/Cargo.lock | 2 + src-tauri/Cargo.toml | 14 + src-tauri/src/app_auto_updater.rs | 470 ++++++++++++++++++++++++++---- src-tauri/src/browser.rs | 10 +- src-tauri/src/browser_runner.rs | 257 +++++++++++++--- src-tauri/src/default_browser.rs | 277 +++++++++++++++++- src-tauri/src/extraction.rs | 315 +++++++++++++++++--- src-tauri/src/profile_importer.rs | 123 ++++++++ 8 files changed, 1316 insertions(+), 152 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 101afa0..9519978 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1018,6 +1018,8 @@ dependencies = [ "tempfile", "tokio", "tokio-test", + "windows", + "winreg", "wiremock", "zip", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e9881f4..dc7db69 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -43,6 +43,20 @@ core-foundation="0.10" objc2 = "0.6.1" objc2-app-kit = { version = "0.3.1", features = ["NSWindow"] } +[target.'cfg(target_os = "windows")'.dependencies] +winreg = "0.55" +windows = { version = "0.61", features = [ + "Win32_Foundation", + "Win32_System_ProcessStatus", + "Win32_System_Threading", + "Win32_System_Diagnostics_Debug", + "Win32_System_SystemInformation", + "Win32_Security", + "Win32_Storage_FileSystem", + "Win32_System_Registry", + "Win32_UI_Shell", +] } + [dev-dependencies] tempfile = "3.13.0" tokio-test = "0.4.4" diff --git a/src-tauri/src/app_auto_updater.rs b/src-tauri/src/app_auto_updater.rs index 9122812..ece5df8 100644 --- a/src-tauri/src/app_auto_updater.rs +++ b/src-tauri/src/app_auto_updater.rs @@ -398,6 +398,30 @@ impl AppAutoUpdater { Err("DMG extraction is only supported on macOS".into()) } } + "msi" => { + #[cfg(target_os = "windows")] + { + // For MSI files on Windows, we need to run the installer + // MSI files can't be extracted like archives, they need to be executed + // Return the path to the MSI file itself for installation + Ok(archive_path.to_path_buf()) + } + #[cfg(not(target_os = "windows"))] + { + Err("MSI installation is only supported on Windows".into()) + } + } + "exe" => { + #[cfg(target_os = "windows")] + { + // For exe installers on Windows, return the path for execution + Ok(archive_path.to_path_buf()) + } + #[cfg(not(target_os = "windows"))] + { + Err("EXE installation is only supported on Windows".into()) + } + } "zip" => extractor.extract_zip(archive_path, dest_dir).await, _ => Err(format!("Unsupported archive format: {extension}").into()), } @@ -406,71 +430,282 @@ impl AppAutoUpdater { /// Install the update by replacing the current app async fn install_update( &self, - new_app_path: &Path, + installer_path: &Path, ) -> Result<(), Box> { - // Get the current application bundle path - let current_app_path = self.get_current_app_path()?; + #[cfg(target_os = "macos")] + { + // Get the current application bundle path + let current_app_path = self.get_current_app_path()?; - // Create a backup of the current app - let backup_path = current_app_path.with_extension("app.backup"); - if backup_path.exists() { - fs::remove_dir_all(&backup_path)?; + // Create a backup of the current app + let backup_path = current_app_path.with_extension("app.backup"); + if backup_path.exists() { + fs::remove_dir_all(&backup_path)?; + } + + // Move current app to backup + fs::rename(¤t_app_path, &backup_path)?; + + // Move new app to current location + fs::rename(installer_path, ¤t_app_path)?; + + // Remove quarantine attributes from the new app + let _ = Command::new("xattr") + .args([ + "-dr", + "com.apple.quarantine", + current_app_path.to_str().unwrap(), + ]) + .output(); + + let _ = Command::new("xattr") + .args(["-cr", current_app_path.to_str().unwrap()]) + .output(); + + // Clean up backup after successful installation + let _ = fs::remove_dir_all(&backup_path); + + Ok(()) } - // Move current app to backup - fs::rename(¤t_app_path, &backup_path)?; + #[cfg(target_os = "windows")] + { + let extension = installer_path + .extension() + .and_then(|ext| ext.to_str()) + .unwrap_or(""); - // Move new app to current location - fs::rename(new_app_path, ¤t_app_path)?; + println!("Installing Windows update with extension: {extension}"); - // Remove quarantine attributes from the new app - let _ = Command::new("xattr") - .args([ - "-dr", - "com.apple.quarantine", - current_app_path.to_str().unwrap(), - ]) - .output(); + match extension { + "msi" => { + // Install MSI silently with enhanced error handling + println!("Running MSI installer: {}", installer_path.display()); - let _ = Command::new("xattr") - .args(["-cr", current_app_path.to_str().unwrap()]) - .output(); + let mut cmd = Command::new("msiexec"); + cmd.args([ + "/i", + installer_path.to_str().unwrap(), + "/quiet", + "/norestart", + "REBOOT=ReallySuppress", + "/l*v", // Enable verbose logging + &format!("{}.log", installer_path.to_str().unwrap()), + ]); - // Clean up backup after successful installation - let _ = fs::remove_dir_all(&backup_path); + let output = cmd.output()?; - Ok(()) + if !output.status.success() { + let error_msg = String::from_utf8_lossy(&output.stderr); + let exit_code = output.status.code().unwrap_or(-1); + + // Try to read the log file for more details + let log_path = format!("{}.log", installer_path.to_str().unwrap()); + let log_content = fs::read_to_string(&log_path).unwrap_or_default(); + + println!("MSI installation failed with exit code: {exit_code}"); + println!("Error output: {error_msg}"); + if !log_content.is_empty() { + println!( + "Log file content (last 500 chars): {}", + &log_content + .chars() + .rev() + .take(500) + .collect::() + .chars() + .rev() + .collect::() + ); + } + + return Err( + format!("MSI installation failed (exit code {exit_code}): {error_msg}").into(), + ); + } + + println!("MSI installation completed successfully"); + } + "exe" => { + // Run exe installer silently with multiple fallback options + println!("Running EXE installer: {}", installer_path.display()); + + // Try NSIS silent flag first (most common for Tauri) + let mut success = false; + let mut last_error = String::new(); + + // NSIS installer flags (used by Tauri) + let nsis_args = vec![ + vec!["/S"], // Standard NSIS silent flag + vec!["/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART"], // Inno Setup flags + vec!["/quiet"], // Generic quiet flag + vec!["/silent"], // Alternative silent flag + ]; + + for args in nsis_args { + println!("Trying installer with args: {:?}", args); + let output = Command::new(installer_path).args(&args).output(); + + match output { + Ok(output) if output.status.success() => { + println!( + "EXE installation completed successfully with args: {:?}", + args + ); + success = true; + break; + } + Ok(output) => { + let error_msg = String::from_utf8_lossy(&output.stderr); + last_error = format!( + "Exit code {}: {}", + output.status.code().unwrap_or(-1), + error_msg + ); + println!("Installer failed with args {:?}: {}", args, last_error); + } + Err(e) => { + last_error = format!("Failed to execute installer: {e}"); + println!( + "Failed to execute installer with args {:?}: {}", + args, last_error + ); + } + } + } + + if !success { + return Err( + format!( + "EXE installation failed after trying multiple methods. Last error: {last_error}" + ) + .into(), + ); + } + } + "zip" => { + // Handle ZIP files by extracting and replacing the current executable + println!("Handling ZIP update: {}", installer_path.display()); + + let temp_extract_dir = installer_path.parent().unwrap().join("extracted"); + fs::create_dir_all(&temp_extract_dir)?; + + // Extract ZIP file + let extractor = crate::extraction::Extractor::new(); + let extracted_path = extractor + .extract_zip(installer_path, &temp_extract_dir) + .await?; + + // Find the executable in the extracted files + let current_exe = self.get_current_app_path()?; + let current_exe_name = current_exe.file_name().unwrap(); + + // Look for the new executable + let new_exe_path = + if extracted_path.is_file() && extracted_path.file_name() == Some(current_exe_name) { + extracted_path + } else { + // Search in extracted directory + let mut found_exe = None; + if let Ok(entries) = fs::read_dir(&extracted_path) { + for entry in entries.flatten() { + let path = entry.path(); + if path.file_name() == Some(current_exe_name) { + found_exe = Some(path); + break; + } + } + } + found_exe.ok_or("Could not find executable in ZIP file")? + }; + + // Create backup of current executable + let backup_path = current_exe.with_extension("exe.backup"); + if backup_path.exists() { + fs::remove_file(&backup_path)?; + } + fs::copy(¤t_exe, &backup_path)?; + + // Replace current executable + fs::copy(&new_exe_path, ¤t_exe)?; + + // Clean up + let _ = fs::remove_dir_all(&temp_extract_dir); + + println!("ZIP update completed successfully"); + } + _ => { + return Err(format!("Unsupported installer format: {extension}").into()); + } + } + + Ok(()) + } + + #[cfg(target_os = "linux")] + { + // For Linux, we would handle different package formats here + // This implementation would depend on the specific package type + Err("Linux auto-update installation not yet implemented".into()) + } + + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + { + Err("Auto-update installation not supported on this platform".into()) + } } /// Get the current application bundle path fn get_current_app_path(&self) -> Result> { - // Get the current executable path - let exe_path = std::env::current_exe()?; + #[cfg(target_os = "macos")] + { + // Get the current executable path + let exe_path = std::env::current_exe()?; - // Navigate up to find the .app bundle - let mut current = exe_path.as_path(); - while let Some(parent) = current.parent() { - if parent.extension().is_some_and(|ext| ext == "app") { - return Ok(parent.to_path_buf()); + // Navigate up to find the .app bundle + let mut current = exe_path.as_path(); + while let Some(parent) = current.parent() { + if parent.extension().is_some_and(|ext| ext == "app") { + return Ok(parent.to_path_buf()); + } + current = parent; } - current = parent; + + Err("Could not find application bundle".into()) } - Err("Could not find application bundle".into()) + #[cfg(target_os = "windows")] + { + // On Windows, just return the current executable path + std::env::current_exe().map_err(|e| e.into()) + } + + #[cfg(target_os = "linux")] + { + // On Linux, return the current executable path + std::env::current_exe().map_err(|e| e.into()) + } + + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + { + Err("Platform not supported".into()) + } } /// Restart the application async fn restart_application(&self) -> Result<(), Box> { - let app_path = self.get_current_app_path()?; - let current_pid = std::process::id(); + #[cfg(target_os = "macos")] + { + let app_path = self.get_current_app_path()?; + let current_pid = std::process::id(); - // Create a temporary restart script - let temp_dir = std::env::temp_dir(); - let script_path = temp_dir.join("donut_restart.sh"); + // Create a temporary restart script + let temp_dir = std::env::temp_dir(); + let script_path = temp_dir.join("donut_restart.sh"); - // Create the restart script content - let script_content = format!( - r#"#!/bin/bash + // Create the restart script content + let script_content = format!( + r#"#!/bin/bash # Wait for the current process to exit while kill -0 {} 2>/dev/null; do sleep 0.5 @@ -485,37 +720,146 @@ open "{}" # Clean up this script rm "{}" "#, - current_pid, - app_path.to_str().unwrap(), - script_path.to_str().unwrap() - ); + current_pid, + app_path.to_str().unwrap(), + script_path.to_str().unwrap() + ); - // Write the script to file - fs::write(&script_path, script_content)?; + // Write the script to file + fs::write(&script_path, script_content)?; - // Make the script executable - let _ = Command::new("chmod") - .args(["+x", script_path.to_str().unwrap()]) - .output(); + // Make the script executable + let _ = Command::new("chmod") + .args(["+x", script_path.to_str().unwrap()]) + .output(); - // Execute the restart script in the background - let mut cmd = Command::new("bash"); - cmd.arg(script_path.to_str().unwrap()); + // Execute the restart script in the background + let mut cmd = Command::new("bash"); + cmd.arg(script_path.to_str().unwrap()); - // Detach the process completely - #[cfg(unix)] - { + // Detach the process completely use std::os::unix::process::CommandExt; cmd.process_group(0); + + let _child = cmd.spawn()?; + + // Give the script a moment to start + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + // Exit the current process + std::process::exit(0); } - let _child = cmd.spawn()?; + #[cfg(target_os = "windows")] + { + let app_path = self.get_current_app_path()?; + let current_pid = std::process::id(); - // Give the script a moment to start - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + // Create a temporary restart batch script + let temp_dir = std::env::temp_dir(); + let script_path = temp_dir.join("donut_restart.bat"); - // Exit the current process - std::process::exit(0); + // Create the restart script content + let script_content = format!( + r#"@echo off +rem Wait for the current process to exit +:wait_loop +tasklist /fi "PID eq {}" >nul 2>&1 +if %errorlevel% equ 0 ( + timeout /t 1 /nobreak >nul + goto wait_loop +) + +rem Wait a bit more to ensure clean exit +timeout /t 2 /nobreak >nul + +rem Start the new application +start "" "{}" + +rem Clean up this script +del "%~f0" +"#, + current_pid, + app_path.to_str().unwrap() + ); + + // Write the script to file + fs::write(&script_path, script_content)?; + + // Execute the restart script in the background + let mut cmd = Command::new("cmd"); + cmd.args(["/C", script_path.to_str().unwrap()]); + + // Start the process detached + let _child = cmd.spawn()?; + + // Give the script a moment to start + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + // Exit the current process + std::process::exit(0); + } + + #[cfg(target_os = "linux")] + { + let app_path = self.get_current_app_path()?; + let current_pid = std::process::id(); + + // Create a temporary restart script + let temp_dir = std::env::temp_dir(); + let script_path = temp_dir.join("donut_restart.sh"); + + // Create the restart script content + let script_content = format!( + r#"#!/bin/bash +# Wait for the current process to exit +while kill -0 {} 2>/dev/null; do + sleep 0.5 +done + +# Wait a bit more to ensure clean exit +sleep 1 + +# Start the new application +"{}" & + +# Clean up this script +rm "{}" +"#, + current_pid, + app_path.to_str().unwrap(), + script_path.to_str().unwrap() + ); + + // Write the script to file + fs::write(&script_path, script_content)?; + + // Make the script executable + let _ = Command::new("chmod") + .args(["+x", script_path.to_str().unwrap()]) + .output(); + + // Execute the restart script in the background + let mut cmd = Command::new("bash"); + cmd.arg(script_path.to_str().unwrap()); + + // Detach the process completely + use std::os::unix::process::CommandExt; + cmd.process_group(0); + + let _child = cmd.spawn()?; + + // Give the script a moment to start + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + // Exit the current process + std::process::exit(0); + } + + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + { + Err("Application restart not supported on this platform".into()) + } } } diff --git a/src-tauri/src/browser.rs b/src-tauri/src/browser.rs index 5ccfb07..a45d430 100644 --- a/src-tauri/src/browser.rs +++ b/src-tauri/src/browser.rs @@ -217,10 +217,7 @@ mod linux { browser_type: &BrowserType, ) -> Result> { let possible_executables = match browser_type { - BrowserType::Chromium => vec![ - install_dir.join("chromium"), - install_dir.join("chrome"), - ], + BrowserType::Chromium => vec![install_dir.join("chromium"), install_dir.join("chrome")], BrowserType::Brave => vec![ install_dir.join("brave"), install_dir.join("brave-browser"), @@ -292,10 +289,7 @@ mod linux { pub fn is_chromium_version_downloaded(install_dir: &Path, browser_type: &BrowserType) -> bool { let possible_executables = match browser_type { - BrowserType::Chromium => vec![ - install_dir.join("chromium"), - install_dir.join("chrome"), - ], + BrowserType::Chromium => vec![install_dir.join("chromium"), install_dir.join("chrome")], BrowserType::Brave => vec![ install_dir.join("brave"), install_dir.join("brave-browser"), diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index b779097..5955393 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -482,14 +482,42 @@ end try mod windows { use super::*; use std::ffi::OsString; + use std::process::Command; - pub fn is_tor_or_mullvad_browser( - _exe_name: &str, - _cmd: &[OsString], - _browser_type: &str, - ) -> bool { - // Windows implementation would go here - false + pub fn is_tor_or_mullvad_browser(exe_name: &str, cmd: &[OsString], browser_type: &str) -> bool { + let exe_lower = exe_name.to_lowercase(); + + // Check for Firefox-based browsers first by executable name + let is_firefox_family = exe_lower.contains("firefox") || exe_lower.contains(".exe"); + + if !is_firefox_family { + return false; + } + + // Check command arguments for profile paths and browser-specific indicators + let cmd_line = cmd + .iter() + .map(|s| s.to_string_lossy().to_lowercase()) + .collect::>() + .join(" "); + + match browser_type { + "tor-browser" => { + // Check for TOR browser specific paths and arguments + cmd_line.contains("tor") + || cmd_line.contains("browser\\torbrowser") + || cmd_line.contains("tor-browser") + || cmd_line.contains("profile") && (cmd_line.contains("tor") || cmd_line.contains("tbb")) + } + "mullvad-browser" => { + // Check for Mullvad browser specific paths and arguments + cmd_line.contains("mullvad") + || cmd_line.contains("browser\\mullvadbrowser") + || cmd_line.contains("mullvad-browser") + || cmd_line.contains("profile") && cmd_line.contains("mullvad") + } + _ => false, + } } pub async fn launch_browser_process( @@ -500,7 +528,48 @@ mod windows { "Launching browser on Windows: {:?} with args: {:?}", executable_path, args ); - Ok(Command::new(executable_path).args(args).spawn()?) + + // Check if the executable exists + if !executable_path.exists() { + return Err(format!("Browser executable not found: {:?}", executable_path).into()); + } + + // On Windows, set up the command with proper working directory + let mut cmd = Command::new(executable_path); + cmd.args(args); + + // Set working directory to the executable's directory for better compatibility + if let Some(parent_dir) = executable_path.parent() { + cmd.current_dir(parent_dir); + } + + // For Windows 7 compatibility, set some environment variables + cmd.env( + "PROCESSOR_ARCHITECTURE", + std::env::var("PROCESSOR_ARCHITECTURE").unwrap_or_else(|_| "x86".to_string()), + ); + + // Ensure proper PATH for DLL loading + if let Some(exe_dir) = executable_path.parent() { + let mut path_var = std::env::var("PATH").unwrap_or_default(); + if !path_var.is_empty() { + path_var = format!("{};{}", exe_dir.display(), path_var); + } else { + path_var = exe_dir.display().to_string(); + } + cmd.env("PATH", path_var); + } + + // Launch the process + let child = cmd + .spawn() + .map_err(|e| format!("Failed to launch browser process: {}", e))?; + + println!( + "Successfully launched browser process with PID: {}", + child.id() + ); + Ok(child) } pub async fn open_url_in_existing_browser_firefox_like( @@ -514,14 +583,85 @@ mod windows { .get_executable_path(browser_dir) .map_err(|e| format!("Failed to get executable path: {}", e))?; - let output = Command::new(executable_path) - .args(["-profile", &profile.profile_path, "-new-tab", url]) - .output()?; + // For Windows, try using the -requestPending approach for Firefox + let mut cmd = Command::new(executable_path); + cmd.args([ + "-profile", + &profile.profile_path, + "-requestPending", + "-new-tab", + url, + ]); + + // Set working directory + if let Some(parent_dir) = browser_dir + .parent() + .or_else(|| browser_dir.ancestors().nth(1)) + { + cmd.current_dir(parent_dir); + } + + let output = cmd.output()?; + + if !output.status.success() { + // Fallback: try without -requestPending + let mut fallback_cmd = Command::new(browser.get_executable_path(browser_dir)?); + fallback_cmd.args(["-profile", &profile.profile_path, "-new-tab", url]); + + if let Some(parent_dir) = browser_dir + .parent() + .or_else(|| browser_dir.ancestors().nth(1)) + { + fallback_cmd.current_dir(parent_dir); + } + + let fallback_output = fallback_cmd.output()?; + + if !fallback_output.status.success() { + return Err( + format!( + "Failed to open URL in existing browser: {}", + String::from_utf8_lossy(&fallback_output.stderr) + ) + .into(), + ); + } + } + + Ok(()) + } + + pub async fn open_url_in_existing_browser_tor_mullvad( + profile: &BrowserProfile, + url: &str, + browser_type: BrowserType, + browser_dir: &Path, + ) -> Result<(), Box> { + // On Windows, TOR and Mullvad browsers can sometimes accept URLs via command line + // even with -no-remote, by launching a new instance that hands off to existing one + let browser = create_browser(browser_type); + let executable_path = browser + .get_executable_path(browser_dir) + .map_err(|e| format!("Failed to get executable path: {}", e))?; + + let mut cmd = Command::new(executable_path); + cmd.args(["-profile", &profile.profile_path, url]); + + // Set working directory + if let Some(parent_dir) = browser_dir + .parent() + .or_else(|| browser_dir.ancestors().nth(1)) + { + cmd.current_dir(parent_dir); + } + + let output = cmd.output()?; if !output.status.success() { return Err( format!( - "Failed to open URL in existing browser: {}", + "Failed to open URL in existing {}: {}. Note: TOR and Mullvad browsers may require manual URL opening for security reasons.", + browser_type.as_str(), String::from_utf8_lossy(&output.stderr) ) .into(), @@ -531,15 +671,6 @@ mod windows { Ok(()) } - pub async fn open_url_in_existing_browser_tor_mullvad( - _profile: &BrowserProfile, - _url: &str, - _browser_type: BrowserType, - _browser_dir: &Path, - ) -> Result<(), Box> { - Err("Opening URLs in existing Firefox-based browsers is not supported on Windows when using -no-remote".into()) - } - pub async fn open_url_in_existing_browser_chromium( profile: &BrowserProfile, url: &str, @@ -551,18 +682,46 @@ mod windows { .get_executable_path(browser_dir) .map_err(|e| format!("Failed to get executable path: {}", e))?; - let output = Command::new(executable_path) - .args([&format!("--user-data-dir={}", profile.profile_path), url]) - .output()?; + let mut cmd = Command::new(executable_path); + cmd.args([ + &format!("--user-data-dir={}", profile.profile_path), + "--new-window", + url, + ]); + + // Set working directory + if let Some(parent_dir) = browser_dir + .parent() + .or_else(|| browser_dir.ancestors().nth(1)) + { + cmd.current_dir(parent_dir); + } + + let output = cmd.output()?; if !output.status.success() { - return Err( - format!( - "Failed to open URL in existing Chromium-based browser: {}", - String::from_utf8_lossy(&output.stderr) - ) - .into(), - ); + // Try fallback without --new-window + let mut fallback_cmd = Command::new(executable_path); + fallback_cmd.args([&format!("--user-data-dir={}", profile.profile_path), url]); + + if let Some(parent_dir) = browser_dir + .parent() + .or_else(|| browser_dir.ancestors().nth(1)) + { + fallback_cmd.current_dir(parent_dir); + } + + let fallback_output = fallback_cmd.output()?; + + if !fallback_output.status.success() { + return Err( + format!( + "Failed to open URL in existing Chromium-based browser: {}", + String::from_utf8_lossy(&fallback_output.stderr) + ) + .into(), + ); + } } Ok(()) @@ -571,17 +730,41 @@ mod windows { pub async fn kill_browser_process_impl( pid: u32, ) -> Result<(), Box> { + // First try using sysinfo (cross-platform approach) let system = System::new_all(); if let Some(process) = system.process(Pid::from(pid as usize)) { - if !process.kill() { - return Err(format!("Failed to kill process {}", pid).into()); + if process.kill() { + println!("Successfully killed browser process with PID: {pid}"); + return Ok(()); } - } else { - return Err(format!("Process {} not found", pid).into()); } - println!("Successfully killed browser process with PID: {pid}"); - Ok(()) + // Fallback to Windows-specific process termination + use std::process::Command; + + // Try taskkill command as fallback + let output = Command::new("taskkill") + .args(["/F", "/PID", &pid.to_string()]) + .output(); + + match output { + Ok(result) => { + if result.status.success() { + println!("Successfully killed browser process with PID: {pid} using taskkill"); + Ok(()) + } else { + Err( + format!( + "Failed to kill process {} with taskkill: {}", + pid, + String::from_utf8_lossy(&result.stderr) + ) + .into(), + ) + } + } + Err(e) => Err(format!("Failed to execute taskkill for process {}: {}", pid, e).into()), + } } } diff --git a/src-tauri/src/default_browser.rs b/src-tauri/src/default_browser.rs index 9a2553d..99854e4 100644 --- a/src-tauri/src/default_browser.rs +++ b/src-tauri/src/default_browser.rs @@ -65,13 +65,284 @@ mod macos { #[cfg(target_os = "windows")] mod windows { + use std::path::Path; + use winreg::enums::*; + use winreg::RegKey; + + const APP_NAME: &str = "DonutBrowser"; + const APP_EXECUTABLE: &str = "DonutBrowser.exe"; + const PROG_ID: &str = "DonutBrowser.HTML"; + pub fn is_default_browser() -> Result { - // Windows implementation would go here - Err("Windows support not implemented yet".to_string()) + let schemes = ["http", "https"]; + + for scheme in schemes { + // Check if our browser is set as the default handler for this scheme + if !is_default_for_scheme(scheme)? { + return Ok(false); + } + } + + Ok(true) } pub fn set_as_default_browser() -> Result<(), String> { - Err("Windows support not implemented yet".to_string()) + // Get the current executable path + let exe_path = std::env::current_exe() + .map_err(|e| format!("Failed to get current executable path: {}", e))?; + + let exe_path_str = exe_path + .to_str() + .ok_or("Failed to convert executable path to string")?; + + // Verify the executable exists + if !Path::new(exe_path_str).exists() { + return Err(format!("Executable not found at: {}", exe_path_str)); + } + + // Register the application + register_application(exe_path_str)?; + + // Set as default for HTTP and HTTPS + set_default_for_scheme("http")?; + set_default_for_scheme("https")?; + + // Register file associations for HTML files + register_html_file_association(exe_path_str)?; + + // Notify the system of changes + notify_system_of_changes(); + + Ok(()) + } + + fn is_default_for_scheme(scheme: &str) -> Result { + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + + // Check Software\Microsoft\Windows\Shell\Associations\UrlAssociations\{scheme}\UserChoice + let path = format!( + "Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\{}\\UserChoice", + scheme + ); + + match hkcu.open_subkey(&path) { + Ok(key) => match key.get_value::("ProgId") { + Ok(prog_id) => Ok(prog_id == PROG_ID), + Err(_) => Ok(false), + }, + Err(_) => Ok(false), + } + } + + fn register_application(exe_path: &str) -> Result<(), String> { + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + + // Register in Software\RegisteredApplications + let (registered_apps, _) = hkcu + .create_subkey("Software\\RegisteredApplications") + .map_err(|e| format!("Failed to create RegisteredApplications key: {}", e))?; + + registered_apps + .set_value(APP_NAME, &format!("Software\\{}", APP_NAME)) + .map_err(|e| format!("Failed to set registered application: {}", e))?; + + // Create application key + let (app_key, _) = hkcu + .create_subkey(&format!("Software\\{}", APP_NAME)) + .map_err(|e| format!("Failed to create application key: {}", e))?; + + // Set application properties + app_key + .set_value("ApplicationName", &APP_NAME) + .map_err(|e| format!("Failed to set ApplicationName: {}", e))?; + + app_key + .set_value( + "ApplicationDescription", + &"Donut Browser - Simple Yet Powerful Browser Orchestrator", + ) + .map_err(|e| format!("Failed to set ApplicationDescription: {}", e))?; + + app_key + .set_value("ApplicationIcon", &format!("{},0", exe_path)) + .map_err(|e| format!("Failed to set ApplicationIcon: {}", e))?; + + // Create Capabilities key + let (capabilities, _) = app_key + .create_subkey("Capabilities") + .map_err(|e| format!("Failed to create Capabilities key: {}", e))?; + + capabilities + .set_value( + "ApplicationDescription", + &"Donut Browser - Simple Yet Powerful Browser Orchestrator", + ) + .map_err(|e| format!("Failed to set Capabilities description: {}", e))?; + + // Set URL associations + let (url_assoc, _) = capabilities + .create_subkey("URLAssociations") + .map_err(|e| format!("Failed to create URLAssociations key: {}", e))?; + + url_assoc + .set_value("http", &PROG_ID) + .map_err(|e| format!("Failed to set http association: {}", e))?; + + url_assoc + .set_value("https", &PROG_ID) + .map_err(|e| format!("Failed to set https association: {}", e))?; + + // Set file associations + let (file_assoc, _) = capabilities + .create_subkey("FileAssociations") + .map_err(|e| format!("Failed to create FileAssociations key: {}", e))?; + + file_assoc + .set_value(".html", &PROG_ID) + .map_err(|e| format!("Failed to set .html association: {}", e))?; + + file_assoc + .set_value(".htm", &PROG_ID) + .map_err(|e| format!("Failed to set .htm association: {}", e))?; + + // Register the ProgID + register_prog_id(exe_path)?; + + Ok(()) + } + + fn register_prog_id(exe_path: &str) -> Result<(), String> { + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + + // Create ProgID key + let (prog_id_key, _) = hkcu + .create_subkey(&format!("Software\\Classes\\{}", PROG_ID)) + .map_err(|e| format!("Failed to create ProgID key: {}", e))?; + + prog_id_key + .set_value("", &"Donut Browser Document") + .map_err(|e| format!("Failed to set ProgID default value: {}", e))?; + + prog_id_key + .set_value("FriendlyTypeName", &"Donut Browser Document") + .map_err(|e| format!("Failed to set FriendlyTypeName: {}", e))?; + + // Create DefaultIcon key + let (icon_key, _) = prog_id_key + .create_subkey("DefaultIcon") + .map_err(|e| format!("Failed to create DefaultIcon key: {}", e))?; + + icon_key + .set_value("", &format!("{},0", exe_path)) + .map_err(|e| format!("Failed to set default icon: {}", e))?; + + // Create shell\open\command key + let (command_key, _) = prog_id_key + .create_subkey("shell\\open\\command") + .map_err(|e| format!("Failed to create command key: {}", e))?; + + command_key + .set_value("", &format!("\"{}\" \"%1\"", exe_path)) + .map_err(|e| format!("Failed to set command: {}", e))?; + + Ok(()) + } + + fn set_default_for_scheme(scheme: &str) -> Result<(), String> { + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + + // Set in Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.html\UserChoice + // Note: On Windows 10+, this might require elevated permissions or user interaction + // through the Settings app due to security restrictions + + // Try to set the association in the user's choice + let user_choice_path = format!( + "Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\{}\\UserChoice", + scheme + ); + + // Note: Setting UserChoice directly may not work on Windows 10+ due to hash verification + // The user may need to manually set the default browser through Windows Settings + match hkcu.create_subkey(&user_choice_path) { + Ok((user_choice, _)) => { + // Attempt to set the ProgId + if let Err(_) = user_choice.set_value("ProgId", &PROG_ID) { + // If we can't set UserChoice, that's expected on newer Windows versions + // The registration is still valuable for the "Open with" menu + } + } + Err(_) => { + // Expected on newer Windows versions - user must set manually + } + } + + Ok(()) + } + + fn register_html_file_association(exe_path: &str) -> Result<(), String> { + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + + // Register .html and .htm file associations + for ext in &[".html", ".htm"] { + let ext_path = format!("Software\\Classes\\{}", ext); + + match hkcu.create_subkey(&ext_path) { + Ok((ext_key, _)) => { + // Set the default value to our ProgID + let _ = ext_key.set_value("", &PROG_ID); + } + Err(_) => { + // Continue if we can't set the file association + } + } + } + + Ok(()) + } + + fn notify_system_of_changes() { + // Use Windows API to notify the system of association changes + // This helps refresh the system's understanding of the changes + unsafe { + use std::ffi::c_void; + use std::ptr; + + // Declare the Windows API functions + type UINT = u32; + type DWORD = u32; + type LPARAM = isize; + type WPARAM = usize; + + const HWND_BROADCAST: *mut c_void = 0xffff as *mut c_void; + const WM_SETTINGCHANGE: UINT = 0x001A; + const SMTO_ABORTIFHUNG: UINT = 0x0002; + + // Link to user32.dll functions + extern "system" { + fn SendMessageTimeoutA( + hWnd: *mut c_void, + Msg: UINT, + wParam: WPARAM, + lParam: LPARAM, + fuFlags: UINT, + uTimeout: UINT, + lpdwResult: *mut DWORD, + ) -> isize; + } + + let mut result: DWORD = 0; + + // Notify about file associations change + SendMessageTimeoutA( + HWND_BROADCAST, + WM_SETTINGCHANGE, + 0, + "Software\\Classes\0".as_ptr() as LPARAM, + SMTO_ABORTIFHUNG, + 1000, + &mut result, + ); + } } } diff --git a/src-tauri/src/extraction.rs b/src-tauri/src/extraction.rs index 24356c0..0a29a79 100644 --- a/src-tauri/src/extraction.rs +++ b/src-tauri/src/extraction.rs @@ -453,8 +453,15 @@ impl Extractor { zip_path: &Path, dest_dir: &Path, ) -> Result> { - // Use PowerShell's Expand-Archive on Windows - let output = Command::new("powershell") + use std::io::Read; + + println!("Extracting ZIP archive on Windows: {}", zip_path.display()); + + // Create destination directory if it doesn't exist + fs::create_dir_all(dest_dir)?; + + // First try PowerShell's Expand-Archive (Windows 10+) + let powershell_result = Command::new("powershell") .args([ "-Command", &format!( @@ -463,21 +470,83 @@ impl Extractor { dest_dir.display() ), ]) - .output()?; + .output(); - if !output.status.success() { - return Err( - format!( - "Failed to extract zip with PowerShell: {}", + match powershell_result { + Ok(output) if output.status.success() => { + println!("Successfully extracted using PowerShell"); + } + Ok(output) => { + println!( + "PowerShell extraction failed: {}, trying Rust zip crate fallback", String::from_utf8_lossy(&output.stderr) - ) - .into(), - ); + ); + // Fallback to Rust zip crate for Windows 7 compatibility + return self.extract_zip_with_rust_crate(zip_path, dest_dir).await; + } + Err(e) => { + println!("PowerShell not available: {}, using Rust zip crate", e); + // Fallback to Rust zip crate for Windows 7 compatibility + return self.extract_zip_with_rust_crate(zip_path, dest_dir).await; + } } self.find_extracted_executable(dest_dir).await } + #[cfg(target_os = "windows")] + async fn extract_zip_with_rust_crate( + &self, + zip_path: &Path, + dest_dir: &Path, + ) -> Result> { + use std::io::Read; + + println!("Using Rust zip crate for extraction (Windows 7+ compatibility)"); + + let file = fs::File::open(zip_path)?; + let mut archive = zip::ZipArchive::new(file)?; + + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + let outpath = match file.enclosed_name() { + Some(path) => dest_dir.join(path), + None => continue, + }; + + // Handle directory creation + if file.name().ends_with('/') { + fs::create_dir_all(&outpath)?; + } else { + // Create parent directories + if let Some(p) = outpath.parent() { + if !p.exists() { + fs::create_dir_all(p)?; + } + } + + // Extract file + let mut outfile = fs::File::create(&outpath)?; + std::io::copy(&mut file, &mut outfile)?; + + // On Windows, verify executable files + if outpath + .extension() + .is_some_and(|ext| ext.to_string_lossy().to_lowercase() == "exe") + { + if let Ok(metadata) = fs::metadata(&outpath) { + if metadata.len() > 0 { + println!("Extracted executable: {}", outpath.display()); + } + } + } + } + } + + println!("ZIP extraction completed. Searching for executable..."); + self.find_extracted_executable(dest_dir).await + } + #[cfg(not(target_os = "windows"))] async fn extract_zip_unix( &self, @@ -514,24 +583,60 @@ impl Extractor { ) -> Result> { create_dir_all(dest_dir)?; - // Use tar command for more reliable extraction - let output = Command::new("tar") - .args([ - "-xf", - tar_path.to_str().unwrap(), - "-C", - dest_dir.to_str().unwrap(), - ]) - .output()?; + #[cfg(target_os = "windows")] + { + // On Windows, try multiple extraction methods for better compatibility + // First try using tar if available (Windows 10+) + let tar_result = Command::new("tar") + .args([ + "-xf", + tar_path.to_str().unwrap(), + "-C", + dest_dir.to_str().unwrap(), + ]) + .output(); - if !output.status.success() { - return Err( - format!( - "Failed to extract tar.xz: {}", - String::from_utf8_lossy(&output.stderr) - ) - .into(), - ); + match tar_result { + Ok(output) if output.status.success() => { + println!("Successfully extracted tar.xz using tar command"); + } + Ok(output) => { + println!( + "tar command failed: {}, trying 7-Zip fallback", + String::from_utf8_lossy(&output.stderr) + ); + // Try 7-Zip as fallback + return self.extract_with_7zip(tar_path, dest_dir).await; + } + Err(_) => { + println!("tar command not available, trying 7-Zip"); + // Try 7-Zip as fallback + return self.extract_with_7zip(tar_path, dest_dir).await; + } + } + } + + #[cfg(not(target_os = "windows"))] + { + // Use tar command for Unix-like systems + let output = Command::new("tar") + .args([ + "-xf", + tar_path.to_str().unwrap(), + "-C", + dest_dir.to_str().unwrap(), + ]) + .output()?; + + if !output.status.success() { + return Err( + format!( + "Failed to extract tar.xz: {}", + String::from_utf8_lossy(&output.stderr) + ) + .into(), + ); + } } // Find the extracted executable and set proper permissions @@ -545,6 +650,44 @@ impl Extractor { Ok(executable_path) } + #[cfg(target_os = "windows")] + async fn extract_with_7zip( + &self, + archive_path: &Path, + dest_dir: &Path, + ) -> Result> { + // Try to use 7-Zip for extraction (common on Windows) + let seven_zip_paths = [ + "7z", // If 7z is in PATH + "C:\\Program Files\\7-Zip\\7z.exe", + "C:\\Program Files (x86)\\7-Zip\\7z.exe", + ]; + + for seven_zip_path in &seven_zip_paths { + let result = Command::new(seven_zip_path) + .args([ + "x", // Extract with full paths + archive_path.to_str().unwrap(), + &format!("-o{}", dest_dir.display()), // Output directory + "-y", // Yes to all + ]) + .output(); + + match result { + Ok(output) if output.status.success() => { + println!("Successfully extracted using 7-Zip: {}", seven_zip_path); + return self.find_extracted_executable(dest_dir).await; + } + Ok(_) => continue, + Err(_) => continue, + } + } + + Err( + "No suitable extraction tool found. Please install 7-Zip or ensure tar is available.".into(), + ) + } + pub async fn extract_tar_bz2( &self, tar_path: &Path, @@ -827,40 +970,130 @@ impl Extractor { &self, dest_dir: &Path, ) -> Result> { + println!( + "Searching for Windows executable in: {}", + dest_dir.display() + ); + // Look for .exe files, preferring main browser executables - let exe_names = [ - "chrome.exe", + let priority_exe_names = [ "firefox.exe", + "chrome.exe", + "chromium.exe", "zen.exe", "brave.exe", + "tor-browser.exe", "tor.exe", + "mullvad-browser.exe", ]; - for exe_name in &exe_names { + // First try priority executable names + for exe_name in &priority_exe_names { let exe_path = dest_dir.join(exe_name); if exe_path.exists() { + println!("Found priority executable: {}", exe_path.display()); return Ok(exe_path); } } - // If no specific executable found, look for any .exe file - if let Ok(entries) = fs::read_dir(dest_dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.extension().is_some_and(|ext| ext == "exe") { - return Ok(path); + // Recursively search for executables with depth limit + match self.find_windows_executable_recursive(dest_dir, 0, 3).await { + Ok(exe_path) => { + println!( + "Found executable via recursive search: {}", + exe_path.display() + ); + Ok(exe_path) + } + Err(_) => { + // List directory contents for debugging + if let Ok(entries) = fs::read_dir(dest_dir) { + println!("Directory contents:"); + for entry in entries.flatten() { + let path = entry.path(); + let metadata = if path.is_dir() { "dir" } else { "file" }; + println!(" - {} ({})", path.display(), metadata); + } } - // Check subdirectories - if path.is_dir() { - if let Ok(sub_result) = self.find_windows_executable(&path).await { - return Ok(sub_result); + Err("No executable found after extraction".into()) + } + } + } + + #[cfg(target_os = "windows")] + async fn find_windows_executable_recursive( + &self, + dir: &Path, + depth: usize, + max_depth: usize, + ) -> Result> { + if depth > max_depth { + return Err("Maximum search depth reached".into()); + } + + if let Ok(entries) = fs::read_dir(dir) { + let mut dirs_to_search = Vec::new(); + + // First pass: look for .exe files in current directory + for entry in entries.flatten() { + let path = entry.path(); + + if path.is_file() + && path + .extension() + .is_some_and(|ext| ext.to_string_lossy().to_lowercase() == "exe") + { + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("") + .to_lowercase(); + + // Check if it's a browser executable + if file_name.contains("firefox") + || file_name.contains("chrome") + || file_name.contains("chromium") + || file_name.contains("zen") + || file_name.contains("brave") + || file_name.contains("tor") + || file_name.contains("mullvad") + || file_name.contains("browser") + { + return Ok(path); + } + } else if path.is_dir() { + // Collect directories for later search + dirs_to_search.push(path); + } + } + + // Second pass: search subdirectories + for subdir in dirs_to_search { + if let Ok(result) = self + .find_windows_executable_recursive(&subdir, depth + 1, max_depth) + .await + { + return Ok(result); + } + } + + // Third pass: if no browser-specific executable found, return any .exe + if let Ok(entries) = fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() + && path + .extension() + .is_some_and(|ext| ext.to_string_lossy().to_lowercase() == "exe") + { + return Ok(path); } } } } - Err("No executable found after extraction".into()) + Err("No executable found".into()) } #[cfg(target_os = "linux")] diff --git a/src-tauri/src/profile_importer.rs b/src-tauri/src/profile_importer.rs index 98b91bb..3b71389 100644 --- a/src-tauri/src/profile_importer.rs +++ b/src-tauri/src/profile_importer.rs @@ -55,6 +55,9 @@ impl ProfileImporter { // Detect Zen Browser profiles detected_profiles.extend(self.detect_zen_browser_profiles()?); + // Detect TOR Browser profiles + detected_profiles.extend(self.detect_tor_browser_profiles()?); + // Remove duplicates based on path let mut seen_paths = HashSet::new(); let unique_profiles: Vec = detected_profiles @@ -80,10 +83,19 @@ impl ProfileImporter { #[cfg(target_os = "windows")] { + // Primary location in AppData\Roaming if let Some(app_data) = self.base_dirs.data_dir() { let firefox_dir = app_data.join("Mozilla/Firefox/Profiles"); profiles.extend(self.scan_firefox_profiles_dir(&firefox_dir, "firefox")?); } + + // Also check AppData\Local for portable installations + if let Some(local_app_data) = self.base_dirs.data_local_dir() { + let firefox_local_dir = local_app_data.join("Mozilla/Firefox/Profiles"); + if firefox_local_dir.exists() { + profiles.extend(self.scan_firefox_profiles_dir(&firefox_local_dir, "firefox")?); + } + } } #[cfg(target_os = "linux")] @@ -251,10 +263,19 @@ impl ProfileImporter { #[cfg(target_os = "windows")] { + // Primary location in AppData\Roaming if let Some(app_data) = self.base_dirs.data_dir() { let mullvad_dir = app_data.join("MullvadBrowser/Profiles"); profiles.extend(self.scan_firefox_profiles_dir(&mullvad_dir, "mullvad-browser")?); } + + // Also check common installation locations + if let Some(local_app_data) = self.base_dirs.data_local_dir() { + let mullvad_local_dir = local_app_data.join("MullvadBrowser/Profiles"); + if mullvad_local_dir.exists() { + profiles.extend(self.scan_firefox_profiles_dir(&mullvad_local_dir, "mullvad-browser")?); + } + } } #[cfg(target_os = "linux")] @@ -298,6 +319,108 @@ impl ProfileImporter { Ok(profiles) } + /// Detect TOR Browser profiles + fn detect_tor_browser_profiles( + &self, + ) -> Result, Box> { + let mut profiles = Vec::new(); + + #[cfg(target_os = "macos")] + { + // TOR Browser on macOS is typically in Applications + let tor_dir = self + .base_dirs + .home_dir() + .join("Library/Application Support/TorBrowser-Data/Browser/profile.default"); + + if tor_dir.exists() { + profiles.push(DetectedProfile { + browser: "tor-browser".to_string(), + name: "TOR Browser - Default Profile".to_string(), + path: tor_dir.to_string_lossy().to_string(), + description: "Default TOR Browser profile".to_string(), + }); + } + } + + #[cfg(target_os = "windows")] + { + // Check common TOR Browser installation locations on Windows + let possible_paths = [ + // Default installation in user directory + ( + "Desktop", + "Desktop/Tor Browser/Browser/TorBrowser/Data/Browser/profile.default", + ), + // AppData locations + ( + "AppData/Roaming", + "TorBrowser/Browser/TorBrowser/Data/Browser/profile.default", + ), + ( + "AppData/Local", + "TorBrowser/Browser/TorBrowser/Data/Browser/profile.default", + ), + ]; + + let home_dir = self.base_dirs.home_dir(); + + for (location_name, relative_path) in &possible_paths { + let tor_dir = home_dir.join(relative_path); + if tor_dir.exists() { + profiles.push(DetectedProfile { + browser: "tor-browser".to_string(), + name: format!("TOR Browser - {} Profile", location_name), + path: tor_dir.to_string_lossy().to_string(), + description: format!("TOR Browser profile from {}", location_name), + }); + } + } + + // Also check AppData directories if available + if let Some(app_data) = self.base_dirs.data_dir() { + let tor_app_data = + app_data.join("TorBrowser/Browser/TorBrowser/Data/Browser/profile.default"); + if tor_app_data.exists() { + profiles.push(DetectedProfile { + browser: "tor-browser".to_string(), + name: "TOR Browser - AppData Profile".to_string(), + path: tor_app_data.to_string_lossy().to_string(), + description: "TOR Browser profile from AppData".to_string(), + }); + } + } + } + + #[cfg(target_os = "linux")] + { + // Common TOR Browser locations on Linux + let possible_paths = [ + ".local/share/torbrowser/tbb/x86_64/tor-browser_en-US/Browser/TorBrowser/Data/Browser/profile.default", + "tor-browser_en-US/Browser/TorBrowser/Data/Browser/profile.default", + ".tor-browser/Browser/TorBrowser/Data/Browser/profile.default", + "Downloads/tor-browser_en-US/Browser/TorBrowser/Data/Browser/profile.default", + ]; + + let home_dir = self.base_dirs.home_dir(); + + for relative_path in &possible_paths { + let tor_dir = home_dir.join(relative_path); + if tor_dir.exists() { + profiles.push(DetectedProfile { + browser: "tor-browser".to_string(), + name: "TOR Browser - Default Profile".to_string(), + path: tor_dir.to_string_lossy().to_string(), + description: "TOR Browser profile".to_string(), + }); + break; // Only add the first one found to avoid duplicates + } + } + } + + Ok(profiles) + } + /// Scan Firefox-style profiles directory fn scan_firefox_profiles_dir( &self,