mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-29 18:09:55 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bbc12bcc03 | |||
| 6d437f30e1 | |||
| 4b0ab6b732 | |||
| a802895491 | |||
| a57f90899b |
@@ -69,19 +69,18 @@ jobs:
|
|||||||
target: "aarch64-unknown-linux-gnu"
|
target: "aarch64-unknown-linux-gnu"
|
||||||
pkg_target: "latest-linux-arm64"
|
pkg_target: "latest-linux-arm64"
|
||||||
nodecar_script: "build:linux-arm64"
|
nodecar_script: "build:linux-arm64"
|
||||||
# Future platforms can be added here:
|
- platform: "windows-11"
|
||||||
# - platform: "windows-latest"
|
args: "--target x86_64-pc-windows-msvc"
|
||||||
# args: "--target x86_64-pc-windows-msvc"
|
arch: "x86_64"
|
||||||
# arch: "x86_64"
|
target: "x86_64-pc-windows-msvc"
|
||||||
# target: "x86_64-pc-windows-msvc"
|
pkg_target: "latest-win-x64"
|
||||||
# pkg_target: "latest-win-x64"
|
nodecar_script: "build:win-x64"
|
||||||
# nodecar_script: "build:win-x64"
|
- platform: "windows-11-arm"
|
||||||
# - platform: "windows-latest"
|
args: "--target aarch64-pc-windows-msvc"
|
||||||
# args: "--target aarch64-pc-windows-msvc"
|
arch: "aarch64"
|
||||||
# arch: "aarch64"
|
target: "aarch64-pc-windows-msvc"
|
||||||
# target: "aarch64-pc-windows-msvc"
|
pkg_target: "latest-win-arm64"
|
||||||
# pkg_target: "latest-win-arm64"
|
nodecar_script: "build:win-arm64"
|
||||||
# nodecar_script: "build:win-arm64"
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -68,6 +68,18 @@ jobs:
|
|||||||
target: "aarch64-unknown-linux-gnu"
|
target: "aarch64-unknown-linux-gnu"
|
||||||
pkg_target: "latest-linux-arm64"
|
pkg_target: "latest-linux-arm64"
|
||||||
nodecar_script: "build:linux-arm64"
|
nodecar_script: "build:linux-arm64"
|
||||||
|
- platform: "windows-11"
|
||||||
|
args: "--target x86_64-pc-windows-msvc"
|
||||||
|
arch: "x86_64"
|
||||||
|
target: "x86_64-pc-windows-msvc"
|
||||||
|
pkg_target: "latest-win-x64"
|
||||||
|
nodecar_script: "build:win-x64"
|
||||||
|
- platform: "windows-11-arm"
|
||||||
|
args: "--target aarch64-pc-windows-msvc"
|
||||||
|
arch: "aarch64"
|
||||||
|
target: "aarch64-pc-windows-msvc"
|
||||||
|
pkg_target: "latest-win-arm64"
|
||||||
|
nodecar_script: "build:win-arm64"
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
Vendored
+1
@@ -67,6 +67,7 @@
|
|||||||
"unlisten",
|
"unlisten",
|
||||||
"unrs",
|
"unrs",
|
||||||
"vercel",
|
"vercel",
|
||||||
|
"winreg",
|
||||||
"wiremock",
|
"wiremock",
|
||||||
"xattr",
|
"xattr",
|
||||||
"zhom"
|
"zhom"
|
||||||
|
|||||||
Generated
+2
@@ -1018,6 +1018,8 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-test",
|
"tokio-test",
|
||||||
|
"windows",
|
||||||
|
"winreg",
|
||||||
"wiremock",
|
"wiremock",
|
||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -43,6 +43,20 @@ core-foundation="0.10"
|
|||||||
objc2 = "0.6.1"
|
objc2 = "0.6.1"
|
||||||
objc2-app-kit = { version = "0.3.1", features = ["NSWindow"] }
|
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]
|
[dev-dependencies]
|
||||||
tempfile = "3.13.0"
|
tempfile = "3.13.0"
|
||||||
tokio-test = "0.4.4"
|
tokio-test = "0.4.4"
|
||||||
|
|||||||
@@ -398,6 +398,30 @@ impl AppAutoUpdater {
|
|||||||
Err("DMG extraction is only supported on macOS".into())
|
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,
|
"zip" => extractor.extract_zip(archive_path, dest_dir).await,
|
||||||
_ => Err(format!("Unsupported archive format: {extension}").into()),
|
_ => Err(format!("Unsupported archive format: {extension}").into()),
|
||||||
}
|
}
|
||||||
@@ -406,71 +430,282 @@ impl AppAutoUpdater {
|
|||||||
/// Install the update by replacing the current app
|
/// Install the update by replacing the current app
|
||||||
async fn install_update(
|
async fn install_update(
|
||||||
&self,
|
&self,
|
||||||
new_app_path: &Path,
|
installer_path: &Path,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
// Get the current application bundle path
|
#[cfg(target_os = "macos")]
|
||||||
let current_app_path = self.get_current_app_path()?;
|
{
|
||||||
|
// Get the current application bundle path
|
||||||
|
let current_app_path = self.get_current_app_path()?;
|
||||||
|
|
||||||
// Create a backup of the current app
|
// Create a backup of the current app
|
||||||
let backup_path = current_app_path.with_extension("app.backup");
|
let backup_path = current_app_path.with_extension("app.backup");
|
||||||
if backup_path.exists() {
|
if backup_path.exists() {
|
||||||
fs::remove_dir_all(&backup_path)?;
|
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
|
#[cfg(target_os = "windows")]
|
||||||
fs::rename(¤t_app_path, &backup_path)?;
|
{
|
||||||
|
let extension = installer_path
|
||||||
|
.extension()
|
||||||
|
.and_then(|ext| ext.to_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
// Move new app to current location
|
println!("Installing Windows update with extension: {extension}");
|
||||||
fs::rename(new_app_path, ¤t_app_path)?;
|
|
||||||
|
|
||||||
// Remove quarantine attributes from the new app
|
match extension {
|
||||||
let _ = Command::new("xattr")
|
"msi" => {
|
||||||
.args([
|
// Install MSI silently with enhanced error handling
|
||||||
"-dr",
|
println!("Running MSI installer: {}", installer_path.display());
|
||||||
"com.apple.quarantine",
|
|
||||||
current_app_path.to_str().unwrap(),
|
|
||||||
])
|
|
||||||
.output();
|
|
||||||
|
|
||||||
let _ = Command::new("xattr")
|
let mut cmd = Command::new("msiexec");
|
||||||
.args(["-cr", current_app_path.to_str().unwrap()])
|
cmd.args([
|
||||||
.output();
|
"/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 output = cmd.output()?;
|
||||||
let _ = fs::remove_dir_all(&backup_path);
|
|
||||||
|
|
||||||
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
|
/// Get the current application bundle path
|
||||||
fn get_current_app_path(&self) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
fn get_current_app_path(&self) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
// Get the current executable path
|
#[cfg(target_os = "macos")]
|
||||||
let exe_path = std::env::current_exe()?;
|
{
|
||||||
|
// Get the current executable path
|
||||||
|
let exe_path = std::env::current_exe()?;
|
||||||
|
|
||||||
// Navigate up to find the .app bundle
|
// Navigate up to find the .app bundle
|
||||||
let mut current = exe_path.as_path();
|
let mut current = exe_path.as_path();
|
||||||
while let Some(parent) = current.parent() {
|
while let Some(parent) = current.parent() {
|
||||||
if parent.extension().is_some_and(|ext| ext == "app") {
|
if parent.extension().is_some_and(|ext| ext == "app") {
|
||||||
return Ok(parent.to_path_buf());
|
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
|
/// Restart the application
|
||||||
async fn restart_application(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
async fn restart_application(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let app_path = self.get_current_app_path()?;
|
#[cfg(target_os = "macos")]
|
||||||
let current_pid = std::process::id();
|
{
|
||||||
|
let app_path = self.get_current_app_path()?;
|
||||||
|
let current_pid = std::process::id();
|
||||||
|
|
||||||
// Create a temporary restart script
|
// Create a temporary restart script
|
||||||
let temp_dir = std::env::temp_dir();
|
let temp_dir = std::env::temp_dir();
|
||||||
let script_path = temp_dir.join("donut_restart.sh");
|
let script_path = temp_dir.join("donut_restart.sh");
|
||||||
|
|
||||||
// Create the restart script content
|
// Create the restart script content
|
||||||
let script_content = format!(
|
let script_content = format!(
|
||||||
r#"#!/bin/bash
|
r#"#!/bin/bash
|
||||||
# Wait for the current process to exit
|
# Wait for the current process to exit
|
||||||
while kill -0 {} 2>/dev/null; do
|
while kill -0 {} 2>/dev/null; do
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
@@ -485,37 +720,146 @@ open "{}"
|
|||||||
# Clean up this script
|
# Clean up this script
|
||||||
rm "{}"
|
rm "{}"
|
||||||
"#,
|
"#,
|
||||||
current_pid,
|
current_pid,
|
||||||
app_path.to_str().unwrap(),
|
app_path.to_str().unwrap(),
|
||||||
script_path.to_str().unwrap()
|
script_path.to_str().unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Write the script to file
|
// Write the script to file
|
||||||
fs::write(&script_path, script_content)?;
|
fs::write(&script_path, script_content)?;
|
||||||
|
|
||||||
// Make the script executable
|
// Make the script executable
|
||||||
let _ = Command::new("chmod")
|
let _ = Command::new("chmod")
|
||||||
.args(["+x", script_path.to_str().unwrap()])
|
.args(["+x", script_path.to_str().unwrap()])
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
// Execute the restart script in the background
|
// Execute the restart script in the background
|
||||||
let mut cmd = Command::new("bash");
|
let mut cmd = Command::new("bash");
|
||||||
cmd.arg(script_path.to_str().unwrap());
|
cmd.arg(script_path.to_str().unwrap());
|
||||||
|
|
||||||
// Detach the process completely
|
// Detach the process completely
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::process::CommandExt;
|
use std::os::unix::process::CommandExt;
|
||||||
cmd.process_group(0);
|
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
|
// Create a temporary restart batch script
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let script_path = temp_dir.join("donut_restart.bat");
|
||||||
|
|
||||||
// Exit the current process
|
// Create the restart script content
|
||||||
std::process::exit(0);
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-24
@@ -216,17 +216,13 @@ mod linux {
|
|||||||
install_dir: &Path,
|
install_dir: &Path,
|
||||||
browser_type: &BrowserType,
|
browser_type: &BrowserType,
|
||||||
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||||
// Expected structure: install_dir/<browser>/<binary>
|
|
||||||
let browser_subdir = install_dir.join(browser_type.as_str());
|
|
||||||
|
|
||||||
let possible_executables = match browser_type {
|
let possible_executables = match browser_type {
|
||||||
BrowserType::Chromium => vec![
|
BrowserType::Chromium => vec![install_dir.join("chromium"), install_dir.join("chrome")],
|
||||||
browser_subdir.join("chromium"),
|
|
||||||
browser_subdir.join("chrome"),
|
|
||||||
],
|
|
||||||
BrowserType::Brave => vec![
|
BrowserType::Brave => vec![
|
||||||
browser_subdir.join("brave"),
|
install_dir.join("brave"),
|
||||||
browser_subdir.join("brave-browser"),
|
install_dir.join("brave-browser"),
|
||||||
|
install_dir.join("brave-browser-nightly"),
|
||||||
|
install_dir.join("brave-browser-beta"),
|
||||||
],
|
],
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
@@ -292,23 +288,13 @@ mod linux {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_chromium_version_downloaded(install_dir: &Path, browser_type: &BrowserType) -> bool {
|
pub fn is_chromium_version_downloaded(install_dir: &Path, browser_type: &BrowserType) -> bool {
|
||||||
// Expected structure: install_dir/<browser>/<binary>
|
|
||||||
let browser_subdir = install_dir.join(browser_type.as_str());
|
|
||||||
|
|
||||||
if !browser_subdir.exists() || !browser_subdir.is_dir() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let possible_executables = match browser_type {
|
let possible_executables = match browser_type {
|
||||||
BrowserType::Chromium => vec![
|
BrowserType::Chromium => vec![install_dir.join("chromium"), install_dir.join("chrome")],
|
||||||
browser_subdir.join("chromium"),
|
|
||||||
browser_subdir.join("chrome"),
|
|
||||||
],
|
|
||||||
BrowserType::Brave => vec![
|
BrowserType::Brave => vec![
|
||||||
browser_subdir.join("brave"),
|
install_dir.join("brave"),
|
||||||
browser_subdir.join("brave-browser"),
|
install_dir.join("brave-browser"),
|
||||||
browser_subdir.join("brave-browser-nightly"),
|
install_dir.join("brave-browser-nightly"),
|
||||||
browser_subdir.join("brave-browser-beta"),
|
install_dir.join("brave-browser-beta"),
|
||||||
],
|
],
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
|
|||||||
+226
-39
@@ -3,7 +3,6 @@ use directories::BaseDirs;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs::{self, create_dir_all};
|
use std::fs::{self, create_dir_all};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use sysinfo::{Pid, System};
|
use sysinfo::{Pid, System};
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
@@ -35,6 +34,7 @@ pub struct BrowserProfile {
|
|||||||
mod macos {
|
mod macos {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
pub fn is_tor_or_mullvad_browser(exe_name: &str, cmd: &[OsString], browser_type: &str) -> bool {
|
pub fn is_tor_or_mullvad_browser(exe_name: &str, cmd: &[OsString], browser_type: &str) -> bool {
|
||||||
match browser_type {
|
match browser_type {
|
||||||
@@ -482,14 +482,42 @@ end try
|
|||||||
mod windows {
|
mod windows {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
pub fn is_tor_or_mullvad_browser(
|
pub fn is_tor_or_mullvad_browser(exe_name: &str, cmd: &[OsString], browser_type: &str) -> bool {
|
||||||
_exe_name: &str,
|
let exe_lower = exe_name.to_lowercase();
|
||||||
_cmd: &[OsString],
|
|
||||||
_browser_type: &str,
|
// Check for Firefox-based browsers first by executable name
|
||||||
) -> bool {
|
let is_firefox_family = exe_lower.contains("firefox") || exe_lower.contains(".exe");
|
||||||
// Windows implementation would go here
|
|
||||||
false
|
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(
|
pub async fn launch_browser_process(
|
||||||
@@ -500,7 +528,48 @@ mod windows {
|
|||||||
"Launching browser on Windows: {:?} with args: {:?}",
|
"Launching browser on Windows: {:?} with args: {:?}",
|
||||||
executable_path, 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(
|
pub async fn open_url_in_existing_browser_firefox_like(
|
||||||
@@ -514,14 +583,88 @@ mod windows {
|
|||||||
.get_executable_path(browser_dir)
|
.get_executable_path(browser_dir)
|
||||||
.map_err(|e| format!("Failed to get executable path: {}", e))?;
|
.map_err(|e| format!("Failed to get executable path: {}", e))?;
|
||||||
|
|
||||||
let output = Command::new(executable_path)
|
// For Windows, try using the -requestPending approach for Firefox
|
||||||
.args(["-profile", &profile.profile_path, "-new-tab", url])
|
let mut cmd = Command::new(executable_path);
|
||||||
.output()?;
|
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 executable_path = browser
|
||||||
|
.get_executable_path(browser_dir)
|
||||||
|
.map_err(|e| format!("Failed to get executable path: {}", e))?;
|
||||||
|
let mut fallback_cmd = Command::new(executable_path);
|
||||||
|
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() {
|
if !output.status.success() {
|
||||||
return Err(
|
return Err(
|
||||||
format!(
|
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)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
@@ -531,38 +674,57 @@ mod windows {
|
|||||||
Ok(())
|
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(
|
pub async fn open_url_in_existing_browser_chromium(
|
||||||
profile: &BrowserProfile,
|
profile: &BrowserProfile,
|
||||||
url: &str,
|
url: &str,
|
||||||
browser_type: BrowserType,
|
browser_type: BrowserType,
|
||||||
browser_dir: &Path,
|
browser_dir: &Path,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let browser = create_browser(browser_type);
|
let browser = create_browser(browser_type.clone());
|
||||||
let executable_path = browser
|
let executable_path = browser
|
||||||
.get_executable_path(browser_dir)
|
.get_executable_path(browser_dir)
|
||||||
.map_err(|e| format!("Failed to get executable path: {}", e))?;
|
.map_err(|e| format!("Failed to get executable path: {}", e))?;
|
||||||
|
|
||||||
let output = Command::new(executable_path)
|
let mut cmd = Command::new(executable_path);
|
||||||
.args([&format!("--user-data-dir={}", profile.profile_path), url])
|
cmd.args([
|
||||||
.output()?;
|
&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() {
|
if !output.status.success() {
|
||||||
return Err(
|
// Try fallback without --new-window
|
||||||
format!(
|
let mut fallback_cmd = Command::new(executable_path);
|
||||||
"Failed to open URL in existing Chromium-based browser: {}",
|
fallback_cmd.args([&format!("--user-data-dir={}", profile.profile_path), url]);
|
||||||
String::from_utf8_lossy(&output.stderr)
|
|
||||||
)
|
if let Some(parent_dir) = browser_dir
|
||||||
.into(),
|
.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(())
|
Ok(())
|
||||||
@@ -571,17 +733,41 @@ mod windows {
|
|||||||
pub async fn kill_browser_process_impl(
|
pub async fn kill_browser_process_impl(
|
||||||
pid: u32,
|
pid: u32,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
// First try using sysinfo (cross-platform approach)
|
||||||
let system = System::new_all();
|
let system = System::new_all();
|
||||||
if let Some(process) = system.process(Pid::from(pid as usize)) {
|
if let Some(process) = system.process(Pid::from(pid as usize)) {
|
||||||
if !process.kill() {
|
if process.kill() {
|
||||||
return Err(format!("Failed to kill process {}", pid).into());
|
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}");
|
// Fallback to Windows-specific process termination
|
||||||
Ok(())
|
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()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -589,6 +775,7 @@ mod windows {
|
|||||||
mod linux {
|
mod linux {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
pub fn is_tor_or_mullvad_browser(
|
pub fn is_tor_or_mullvad_browser(
|
||||||
_exe_name: &str,
|
_exe_name: &str,
|
||||||
|
|||||||
@@ -65,13 +65,283 @@ mod macos {
|
|||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
mod 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> {
|
pub fn is_default_browser() -> Result<bool, String> {
|
||||||
// Windows implementation would go here
|
let schemes = ["http", "https"];
|
||||||
Err("Windows support not implemented yet".to_string())
|
|
||||||
|
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> {
|
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;
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+270
-41
@@ -453,8 +453,13 @@ impl Extractor {
|
|||||||
zip_path: &Path,
|
zip_path: &Path,
|
||||||
dest_dir: &Path,
|
dest_dir: &Path,
|
||||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
// Use PowerShell's Expand-Archive on Windows
|
println!("Extracting ZIP archive on Windows: {}", zip_path.display());
|
||||||
let output = Command::new("powershell")
|
|
||||||
|
// 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([
|
.args([
|
||||||
"-Command",
|
"-Command",
|
||||||
&format!(
|
&format!(
|
||||||
@@ -463,21 +468,81 @@ impl Extractor {
|
|||||||
dest_dir.display()
|
dest_dir.display()
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
.output()?;
|
.output();
|
||||||
|
|
||||||
if !output.status.success() {
|
match powershell_result {
|
||||||
return Err(
|
Ok(output) if output.status.success() => {
|
||||||
format!(
|
println!("Successfully extracted using PowerShell");
|
||||||
"Failed to extract zip with PowerShell: {}",
|
}
|
||||||
|
Ok(output) => {
|
||||||
|
println!(
|
||||||
|
"PowerShell extraction failed: {}, trying Rust zip crate fallback",
|
||||||
String::from_utf8_lossy(&output.stderr)
|
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
|
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>> {
|
||||||
|
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"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
async fn extract_zip_unix(
|
async fn extract_zip_unix(
|
||||||
&self,
|
&self,
|
||||||
@@ -514,24 +579,60 @@ impl Extractor {
|
|||||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
create_dir_all(dest_dir)?;
|
create_dir_all(dest_dir)?;
|
||||||
|
|
||||||
// Use tar command for more reliable extraction
|
#[cfg(target_os = "windows")]
|
||||||
let output = Command::new("tar")
|
{
|
||||||
.args([
|
// On Windows, try multiple extraction methods for better compatibility
|
||||||
"-xf",
|
// First try using tar if available (Windows 10+)
|
||||||
tar_path.to_str().unwrap(),
|
let tar_result = Command::new("tar")
|
||||||
"-C",
|
.args([
|
||||||
dest_dir.to_str().unwrap(),
|
"-xf",
|
||||||
])
|
tar_path.to_str().unwrap(),
|
||||||
.output()?;
|
"-C",
|
||||||
|
dest_dir.to_str().unwrap(),
|
||||||
|
])
|
||||||
|
.output();
|
||||||
|
|
||||||
if !output.status.success() {
|
match tar_result {
|
||||||
return Err(
|
Ok(output) if output.status.success() => {
|
||||||
format!(
|
println!("Successfully extracted tar.xz using tar command");
|
||||||
"Failed to extract tar.xz: {}",
|
}
|
||||||
String::from_utf8_lossy(&output.stderr)
|
Ok(output) => {
|
||||||
)
|
println!(
|
||||||
.into(),
|
"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
|
// Find the extracted executable and set proper permissions
|
||||||
@@ -545,6 +646,44 @@ impl Extractor {
|
|||||||
Ok(executable_path)
|
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(
|
pub async fn extract_tar_bz2(
|
||||||
&self,
|
&self,
|
||||||
tar_path: &Path,
|
tar_path: &Path,
|
||||||
@@ -827,40 +966,130 @@ impl Extractor {
|
|||||||
&self,
|
&self,
|
||||||
dest_dir: &Path,
|
dest_dir: &Path,
|
||||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
) -> 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
|
// Look for .exe files, preferring main browser executables
|
||||||
let exe_names = [
|
let priority_exe_names = [
|
||||||
"chrome.exe",
|
|
||||||
"firefox.exe",
|
"firefox.exe",
|
||||||
|
"chrome.exe",
|
||||||
|
"chromium.exe",
|
||||||
"zen.exe",
|
"zen.exe",
|
||||||
"brave.exe",
|
"brave.exe",
|
||||||
|
"tor-browser.exe",
|
||||||
"tor.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);
|
let exe_path = dest_dir.join(exe_name);
|
||||||
if exe_path.exists() {
|
if exe_path.exists() {
|
||||||
|
println!("Found priority executable: {}", exe_path.display());
|
||||||
return Ok(exe_path);
|
return Ok(exe_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no specific executable found, look for any .exe file
|
// Recursively search for executables with depth limit
|
||||||
if let Ok(entries) = fs::read_dir(dest_dir) {
|
match self.find_windows_executable_recursive(dest_dir, 0, 3).await {
|
||||||
for entry in entries.flatten() {
|
Ok(exe_path) => {
|
||||||
let path = entry.path();
|
println!(
|
||||||
if path.extension().is_some_and(|ext| ext == "exe") {
|
"Found executable via recursive search: {}",
|
||||||
return Ok(path);
|
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
|
Err("No executable found after extraction".into())
|
||||||
if path.is_dir() {
|
}
|
||||||
if let Ok(sub_result) = self.find_windows_executable(&path).await {
|
}
|
||||||
return Ok(sub_result);
|
}
|
||||||
|
|
||||||
|
#[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")]
|
#[cfg(target_os = "linux")]
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ impl ProfileImporter {
|
|||||||
// Detect Zen Browser profiles
|
// Detect Zen Browser profiles
|
||||||
detected_profiles.extend(self.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
|
// Remove duplicates based on path
|
||||||
let mut seen_paths = HashSet::new();
|
let mut seen_paths = HashSet::new();
|
||||||
let unique_profiles: Vec<DetectedProfile> = detected_profiles
|
let unique_profiles: Vec<DetectedProfile> = detected_profiles
|
||||||
@@ -80,10 +83,19 @@ impl ProfileImporter {
|
|||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
|
// Primary location in AppData\Roaming
|
||||||
if let Some(app_data) = self.base_dirs.data_dir() {
|
if let Some(app_data) = self.base_dirs.data_dir() {
|
||||||
let firefox_dir = app_data.join("Mozilla/Firefox/Profiles");
|
let firefox_dir = app_data.join("Mozilla/Firefox/Profiles");
|
||||||
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dir, "firefox")?);
|
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")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -251,10 +263,19 @@ impl ProfileImporter {
|
|||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
|
// Primary location in AppData\Roaming
|
||||||
if let Some(app_data) = self.base_dirs.data_dir() {
|
if let Some(app_data) = self.base_dirs.data_dir() {
|
||||||
let mullvad_dir = app_data.join("MullvadBrowser/Profiles");
|
let mullvad_dir = app_data.join("MullvadBrowser/Profiles");
|
||||||
profiles.extend(self.scan_firefox_profiles_dir(&mullvad_dir, "mullvad-browser")?);
|
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")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -298,6 +319,108 @@ impl ProfileImporter {
|
|||||||
Ok(profiles)
|
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
|
/// Scan Firefox-style profiles directory
|
||||||
fn scan_firefox_profiles_dir(
|
fn scan_firefox_profiles_dir(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
Reference in New Issue
Block a user