mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-05-08 03:16:42 +02:00
refactor: windows pipeline test
This commit is contained in:
Generated
+2
@@ -1018,6 +1018,8 @@ dependencies = [
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-test",
|
||||
"windows",
|
||||
"winreg",
|
||||
"wiremock",
|
||||
"zip",
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<dyn std::error::Error + Send + Sync>> {
|
||||
// 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::<String>()
|
||||
.chars()
|
||||
.rev()
|
||||
.collect::<String>()
|
||||
);
|
||||
}
|
||||
|
||||
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<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// 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<dyn std::error::Error + Send + Sync>> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -217,10 +217,7 @@ mod linux {
|
||||
browser_type: &BrowserType,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
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"),
|
||||
|
||||
+220
-37
@@ -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::<Vec<_>>()
|
||||
.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<dyn std::error::Error + Send + Sync>> {
|
||||
// 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<dyn std::error::Error + Send + Sync>> {
|
||||
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<dyn std::error::Error + Send + Sync>> {
|
||||
// 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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<bool, String> {
|
||||
// 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<bool, String> {
|
||||
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::<String, _>("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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+274
-41
@@ -453,8 +453,15 @@ impl Extractor {
|
||||
zip_path: &Path,
|
||||
dest_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// 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<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
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<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
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<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// 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<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
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<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
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")]
|
||||
|
||||
@@ -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<DetectedProfile> = 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<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user