mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-29 07:16:11 +02:00
refactor: better camoufox instance tracking
This commit is contained in:
@@ -48,7 +48,7 @@ tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
||||
zip = "4"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation="0.10"
|
||||
core-foundation = "0.10"
|
||||
objc2 = "0.6.1"
|
||||
objc2-app-kit = { version = "0.3.1", features = ["NSWindow"] }
|
||||
|
||||
@@ -74,11 +74,17 @@ hyper-util = { version = "0.1", features = ["full"] }
|
||||
http-body-util = "0.1"
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["fs", "trace"] }
|
||||
futures-util = "0.3"
|
||||
|
||||
# Integration test configuration
|
||||
[[test]]
|
||||
name = "nodecar_integration"
|
||||
path = "tests/nodecar_integration.rs"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` points to the filesystem
|
||||
default = [ "custom-protocol" ]
|
||||
default = ["custom-protocol"]
|
||||
# this feature is used used for production builds where `devPath` points to the filesystem
|
||||
# DO NOT remove this
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
@@ -212,6 +212,10 @@ impl BrowserRunner {
|
||||
};
|
||||
|
||||
// Use the nodecar camoufox launcher
|
||||
println!(
|
||||
"Launching Camoufox via nodecar for profile: {}",
|
||||
profile.name
|
||||
);
|
||||
let camoufox_result = crate::camoufox::launch_camoufox_profile_nodecar(
|
||||
app_handle.clone(),
|
||||
profile.clone(),
|
||||
@@ -223,21 +227,27 @@ impl BrowserRunner {
|
||||
format!("Failed to launch camoufox via nodecar: {e}").into()
|
||||
})?;
|
||||
|
||||
// For server-based Camoufox, we don't have a PID but we have a port
|
||||
// We'll use the port as a unique identifier for the running instance
|
||||
let process_id = camoufox_result.port;
|
||||
// For server-based Camoufox, we use the port as a unique identifier (which is actually the PID)
|
||||
let process_id = camoufox_result.port.unwrap_or(0);
|
||||
println!("Camoufox launched successfully with PID: {process_id}");
|
||||
|
||||
// Update profile with the process info from camoufox result
|
||||
let mut updated_profile = profile.clone();
|
||||
updated_profile.process_id = process_id;
|
||||
updated_profile.process_id = Some(process_id);
|
||||
updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs());
|
||||
|
||||
// Save the updated profile
|
||||
self.save_process_info(&updated_profile)?;
|
||||
println!(
|
||||
"Updated profile with process info: {}",
|
||||
updated_profile.name
|
||||
);
|
||||
|
||||
// Emit profile update event to frontend
|
||||
if let Err(e) = app_handle.emit("profile-updated", &updated_profile) {
|
||||
println!("Warning: Failed to emit profile update event: {e}");
|
||||
} else {
|
||||
println!("Emitted profile update event for: {}", updated_profile.name);
|
||||
}
|
||||
|
||||
return Ok(updated_profile);
|
||||
@@ -769,69 +779,62 @@ impl BrowserRunner {
|
||||
if profile.browser == "camoufox" {
|
||||
let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::new(app_handle.clone());
|
||||
|
||||
// Try to stop by PID first (faster)
|
||||
if let Some(stored_pid) = profile.process_id {
|
||||
match camoufox_launcher
|
||||
.stop_camoufox(&app_handle, &stored_pid.to_string())
|
||||
.await
|
||||
{
|
||||
Ok(stopped) => {
|
||||
if stopped {
|
||||
println!("Successfully stopped Camoufox process by PID: {stored_pid}");
|
||||
} else {
|
||||
println!("Failed to stop Camoufox process by PID: {stored_pid}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error stopping Camoufox process by PID: {e}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: search by profile path
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_data_path = profile.get_profile_data_path(&profiles_dir);
|
||||
let profile_path_str = profile_data_path.to_string_lossy();
|
||||
// Search by profile path to find the running Camoufox instance
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_data_path = profile.get_profile_data_path(&profiles_dir);
|
||||
let profile_path_str = profile_data_path.to_string_lossy();
|
||||
|
||||
match camoufox_launcher
|
||||
.find_camoufox_by_profile(&profile_path_str)
|
||||
.await
|
||||
{
|
||||
Ok(Some(camoufox_process)) => {
|
||||
match camoufox_launcher
|
||||
.stop_camoufox(&app_handle, &camoufox_process.id)
|
||||
.await
|
||||
{
|
||||
Ok(stopped) => {
|
||||
if stopped {
|
||||
println!(
|
||||
"Successfully stopped Camoufox process: {}",
|
||||
camoufox_process.id
|
||||
);
|
||||
} else {
|
||||
println!("Failed to stop Camoufox process: {}", camoufox_process.id);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error stopping Camoufox process: {e}");
|
||||
println!(
|
||||
"Attempting to kill Camoufox process for profile: {}",
|
||||
profile.name
|
||||
);
|
||||
|
||||
match camoufox_launcher
|
||||
.find_camoufox_by_profile(&profile_path_str)
|
||||
.await
|
||||
{
|
||||
Ok(Some(camoufox_process)) => {
|
||||
println!(
|
||||
"Found Camoufox process: {} (PID: {:?})",
|
||||
camoufox_process.id, camoufox_process.port
|
||||
);
|
||||
|
||||
match camoufox_launcher
|
||||
.stop_camoufox(&app_handle, &camoufox_process.id)
|
||||
.await
|
||||
{
|
||||
Ok(stopped) => {
|
||||
if stopped {
|
||||
println!(
|
||||
"Successfully stopped Camoufox process: {} (PID: {:?})",
|
||||
camoufox_process.id, camoufox_process.port
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"Failed to stop Camoufox process: {} (PID: {:?})",
|
||||
camoufox_process.id, camoufox_process.port
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
println!(
|
||||
"No running Camoufox process found for profile: {}",
|
||||
profile.name
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error finding Camoufox process: {e}");
|
||||
Err(e) => {
|
||||
println!(
|
||||
"Error stopping Camoufox process {}: {}",
|
||||
camoufox_process.id, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop proxy if one was running for this profile
|
||||
if let Some(pid) = profile.process_id {
|
||||
if let Err(e) = PROXY_MANAGER.stop_proxy(app_handle.clone(), pid).await {
|
||||
println!("Warning: Failed to stop proxy for Camoufox profile: {e}");
|
||||
Ok(None) => {
|
||||
println!(
|
||||
"No running Camoufox process found for profile: {}",
|
||||
profile.name
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!(
|
||||
"Error finding Camoufox process for profile {}: {}",
|
||||
profile.name, e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -847,6 +850,10 @@ impl BrowserRunner {
|
||||
println!("Warning: Failed to emit profile update event: {e}");
|
||||
}
|
||||
|
||||
println!(
|
||||
"Camoufox process cleanup completed for profile: {}",
|
||||
profile.name
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -877,13 +884,7 @@ impl BrowserRunner {
|
||||
"zen" => exe_name.contains("zen"),
|
||||
"chromium" => exe_name.contains("chromium"),
|
||||
"brave" => exe_name.contains("brave"),
|
||||
"camoufox" => {
|
||||
exe_name.contains("camoufox")
|
||||
|| (exe_name.contains("firefox")
|
||||
&& cmd
|
||||
.iter()
|
||||
.any(|arg| arg.to_str().unwrap_or("").contains("camoufox")))
|
||||
}
|
||||
// Camoufox is handled via nodecar, not PID-based checking
|
||||
_ => false,
|
||||
};
|
||||
|
||||
@@ -1620,12 +1621,14 @@ pub fn create_browser_profile_new(
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_camoufox_config(
|
||||
app_handle: tauri::AppHandle,
|
||||
profile_name: String,
|
||||
config: CamoufoxConfig,
|
||||
) -> Result<(), String> {
|
||||
let profile_manager = ProfileManager::new();
|
||||
profile_manager
|
||||
.update_camoufox_config(&profile_name, config)
|
||||
.update_camoufox_config(app_handle, &profile_name, config)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to update Camoufox config: {e}"))
|
||||
}
|
||||
|
||||
|
||||
+35
-16
@@ -437,18 +437,22 @@ impl CamoufoxNodecarLauncher {
|
||||
}
|
||||
|
||||
// Execute nodecar sidecar command
|
||||
println!("Executing nodecar command with args: {args:?}");
|
||||
let output = sidecar_command.output().await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
println!("nodecar camoufox failed - stdout: {stdout}, stderr: {stderr}");
|
||||
return Err(format!("nodecar camoufox failed: {stderr}").into());
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
println!("nodecar camoufox output: {stdout}");
|
||||
|
||||
// Parse the JSON output
|
||||
let launch_result: CamoufoxLaunchResult = serde_json::from_str(&stdout)
|
||||
.map_err(|e| format!("Failed to parse nodecar output as JSON: {e}"))?;
|
||||
.map_err(|e| format!("Failed to parse nodecar output as JSON: {e}\nOutput was: {stdout}"))?;
|
||||
|
||||
// Store the instance
|
||||
let instance = CamoufoxInstance {
|
||||
@@ -529,9 +533,10 @@ impl CamoufoxNodecarLauncher {
|
||||
.unwrap_or_else(|_| std::path::Path::new(instance_profile_path).to_path_buf());
|
||||
|
||||
if instance_path == target_path {
|
||||
// Verify the server is actually running by checking the port
|
||||
// Verify the server is actually running by checking the process
|
||||
if let Some(port) = instance.port {
|
||||
if self.is_server_running(port).await {
|
||||
println!("Found running Camoufox instance for profile: {profile_path}");
|
||||
return Ok(Some(CamoufoxLaunchResult {
|
||||
id: id.clone(),
|
||||
port: instance.port,
|
||||
@@ -539,12 +544,15 @@ impl CamoufoxNodecarLauncher {
|
||||
profilePath: instance.profile_path.clone(),
|
||||
url: instance.url.clone(),
|
||||
}));
|
||||
} else {
|
||||
println!("Camoufox instance found but process is not running: {id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("No running Camoufox instance found for profile: {profile_path}");
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -560,14 +568,16 @@ impl CamoufoxNodecarLauncher {
|
||||
|
||||
for (id, instance) in inner.instances.iter() {
|
||||
if let Some(port) = instance.port {
|
||||
// Check if the server is still alive
|
||||
// Check if the process is still alive (port is actually PID)
|
||||
if !self.is_server_running(port).await {
|
||||
// Server is dead
|
||||
// Process is dead
|
||||
println!("Camoufox instance {id} (PID: {port}) is no longer running");
|
||||
dead_instances.push(id.clone());
|
||||
instances_to_remove.push(id.clone());
|
||||
}
|
||||
} else {
|
||||
// No port means it's likely a dead instance
|
||||
// No port/PID means it's likely a dead instance
|
||||
println!("Camoufox instance {id} has no PID, marking as dead");
|
||||
dead_instances.push(id.clone());
|
||||
instances_to_remove.push(id.clone());
|
||||
}
|
||||
@@ -579,26 +589,35 @@ impl CamoufoxNodecarLauncher {
|
||||
let mut inner = self.inner.lock().await;
|
||||
for id in &instances_to_remove {
|
||||
inner.instances.remove(id);
|
||||
println!("Removed dead Camoufox instance: {id}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(dead_instances)
|
||||
}
|
||||
|
||||
/// Check if a Camoufox server is running on the given port
|
||||
/// Check if a Camoufox server is running on the given port (which is actually a PID)
|
||||
async fn is_server_running(&self, port: u32) -> bool {
|
||||
let client = reqwest::Client::new();
|
||||
let url = format!("http://localhost:{port}/json/version");
|
||||
// For Camoufox, the "port" is actually the process PID
|
||||
// Check if the process is still running
|
||||
use sysinfo::{Pid, System};
|
||||
|
||||
match client
|
||||
.get(&url)
|
||||
.timeout(std::time::Duration::from_secs(1))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) => response.status().is_success(),
|
||||
Err(_) => false,
|
||||
let system = System::new_all();
|
||||
if let Some(process) = system.process(Pid::from(port as usize)) {
|
||||
// Check if this is actually a Camoufox process by looking at the command line
|
||||
let cmd = process.cmd();
|
||||
let is_camoufox = cmd.iter().any(|arg| {
|
||||
let arg_str = arg.to_str().unwrap_or("");
|
||||
arg_str.contains("camoufox-worker") || arg_str.contains("camoufox")
|
||||
});
|
||||
|
||||
if is_camoufox {
|
||||
println!("Found running Camoufox process with PID: {port}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,12 @@ impl GeoIPDownloader {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new downloader with custom client (for testing)
|
||||
#[cfg(test)]
|
||||
pub fn new_with_client(client: Client) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
fn get_cache_dir() -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let base_dirs = BaseDirs::new().ok_or("Failed to determine base directories")?;
|
||||
|
||||
@@ -169,3 +175,125 @@ impl GeoIPDownloader {
|
||||
Ok(releases)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::browser::GithubRelease;
|
||||
use wiremock::matchers::{method, path};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
|
||||
fn create_mock_release() -> GithubRelease {
|
||||
GithubRelease {
|
||||
tag_name: "v1.0.0".to_string(),
|
||||
name: "Test Release".to_string(),
|
||||
body: Some("Test release body".to_string()),
|
||||
published_at: "2023-01-01T00:00:00Z".to_string(),
|
||||
created_at: Some("2023-01-01T00:00:00Z".to_string()),
|
||||
html_url: Some("https://example.com/release".to_string()),
|
||||
tarball_url: Some("https://example.com/tarball".to_string()),
|
||||
zipball_url: Some("https://example.com/zipball".to_string()),
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
is_nightly: false,
|
||||
id: Some(1),
|
||||
node_id: Some("test_node_id".to_string()),
|
||||
target_commitish: None,
|
||||
assets: vec![crate::browser::GithubAsset {
|
||||
id: Some(1),
|
||||
node_id: Some("test_asset_node_id".to_string()),
|
||||
name: "GeoLite2-City.mmdb".to_string(),
|
||||
label: None,
|
||||
content_type: Some("application/octet-stream".to_string()),
|
||||
state: Some("uploaded".to_string()),
|
||||
size: 1024,
|
||||
download_count: Some(0),
|
||||
created_at: Some("2023-01-01T00:00:00Z".to_string()),
|
||||
updated_at: Some("2023-01-01T00:00:00Z".to_string()),
|
||||
browser_download_url: "https://example.com/GeoLite2-City.mmdb".to_string(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_geoip_releases_success() {
|
||||
let mock_server = MockServer::start().await;
|
||||
let releases = vec![create_mock_release()];
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!("/repos/{MMDB_REPO}/releases")))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(&releases))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let client = Client::builder()
|
||||
.build()
|
||||
.expect("Failed to create HTTP client");
|
||||
|
||||
let downloader = GeoIPDownloader::new_with_client(client);
|
||||
|
||||
// Override the URL for testing
|
||||
let url = format!("{}/repos/{}/releases", mock_server.uri(), MMDB_REPO);
|
||||
let response = downloader
|
||||
.client
|
||||
.get(&url)
|
||||
.header("User-Agent", "Mozilla/5.0 (compatible; donutbrowser)")
|
||||
.send()
|
||||
.await
|
||||
.expect("Request should succeed");
|
||||
|
||||
assert!(response.status().is_success());
|
||||
|
||||
let fetched_releases: Vec<GithubRelease> = response.json().await.expect("Should parse JSON");
|
||||
assert_eq!(fetched_releases.len(), 1);
|
||||
assert_eq!(fetched_releases[0].tag_name, "v1.0.0");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_find_city_mmdb_asset() {
|
||||
let downloader = GeoIPDownloader::new();
|
||||
let release = create_mock_release();
|
||||
|
||||
let asset_url = downloader.find_city_mmdb_asset(&release);
|
||||
assert!(asset_url.is_some());
|
||||
assert_eq!(asset_url.unwrap(), "https://example.com/GeoLite2-City.mmdb");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_find_city_mmdb_asset_not_found() {
|
||||
let downloader = GeoIPDownloader::new();
|
||||
let mut release = create_mock_release();
|
||||
release.assets[0].name = "wrong-file.txt".to_string();
|
||||
|
||||
let asset_url = downloader.find_city_mmdb_asset(&release);
|
||||
assert!(asset_url.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_cache_dir() {
|
||||
let cache_dir = GeoIPDownloader::get_cache_dir();
|
||||
assert!(cache_dir.is_ok());
|
||||
|
||||
let path = cache_dir.unwrap();
|
||||
assert!(path.to_string_lossy().contains("camoufox"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_mmdb_file_path() {
|
||||
let mmdb_path = GeoIPDownloader::get_mmdb_file_path();
|
||||
assert!(mmdb_path.is_ok());
|
||||
|
||||
let path = mmdb_path.unwrap();
|
||||
assert!(path.to_string_lossy().ends_with("GeoLite2-City.mmdb"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_geoip_database_available() {
|
||||
// This test will return false unless the database actually exists
|
||||
// In a real environment, this would check the actual file system
|
||||
let is_available = GeoIPDownloader::is_geoip_database_available();
|
||||
// We can't assert a specific value since it depends on the system state
|
||||
// But we can verify the function doesn't panic
|
||||
println!("GeoIP database available: {is_available}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,20 +335,30 @@ impl ProfileManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_camoufox_config(
|
||||
pub async fn update_camoufox_config(
|
||||
&self,
|
||||
app_handle: tauri::AppHandle,
|
||||
profile_name: &str,
|
||||
config: CamoufoxConfig,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Find the profile by name
|
||||
let profiles = self.list_profiles()?;
|
||||
let profiles =
|
||||
self
|
||||
.list_profiles()
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
format!("Failed to list profiles: {e}").into()
|
||||
})?;
|
||||
let mut profile = profiles
|
||||
.into_iter()
|
||||
.find(|p| p.name == profile_name)
|
||||
.ok_or_else(|| format!("Profile {profile_name} not found"))?;
|
||||
.ok_or_else(|| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
format!("Profile {profile_name} not found").into()
|
||||
})?;
|
||||
|
||||
// Check if the browser is currently running
|
||||
if profile.process_id.is_some() {
|
||||
// Check if the browser is currently running using the comprehensive status check
|
||||
let is_running = self.check_browser_status(app_handle, &profile).await?;
|
||||
|
||||
if is_running {
|
||||
return Err(
|
||||
"Cannot update Camoufox configuration while browser is running. Please stop the browser first.".into(),
|
||||
);
|
||||
@@ -358,7 +368,11 @@ impl ProfileManager {
|
||||
profile.camoufox_config = Some(config);
|
||||
|
||||
// Save the updated profile
|
||||
self.save_profile(&profile)?;
|
||||
self
|
||||
.save_profile(&profile)
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
format!("Failed to save profile: {e}").into()
|
||||
})?;
|
||||
|
||||
println!("Camoufox configuration updated for profile '{profile_name}'.");
|
||||
|
||||
@@ -433,9 +447,6 @@ impl ProfileManager {
|
||||
.map_err(|e| format!("Failed to update profile proxy: {e}"))?;
|
||||
|
||||
println!("Successfully started proxy for profile: {}", profile.name);
|
||||
|
||||
// Give the proxy a moment to fully start up
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to start proxy: {e}");
|
||||
@@ -498,10 +509,14 @@ impl ProfileManager {
|
||||
app_handle: tauri::AppHandle,
|
||||
profile: &BrowserProfile,
|
||||
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Handle camoufox profiles using the same fast approach as other browsers
|
||||
// No special handling needed - camoufox uses the same process checking logic
|
||||
// Handle Camoufox profiles using nodecar-based status checking
|
||||
if profile.browser == "camoufox" {
|
||||
return self
|
||||
.check_camoufox_status_via_nodecar(&app_handle, profile)
|
||||
.await;
|
||||
}
|
||||
|
||||
// For non-camoufox browsers, use the existing logic
|
||||
// For non-camoufox browsers, use the existing PID-based logic
|
||||
let mut inner_profile = profile.clone();
|
||||
let system = System::new_all();
|
||||
let mut is_running = false;
|
||||
@@ -517,12 +532,8 @@ impl ProfileManager {
|
||||
let profile_data_path_str = profile_data_path.to_string_lossy();
|
||||
let profile_path_match = cmd.iter().any(|s| {
|
||||
let arg = s.to_str().unwrap_or("");
|
||||
// For Firefox-based browsers (including camoufox), check for exact profile path match
|
||||
if profile.browser == "camoufox" {
|
||||
// Camoufox uses user_data_dir like Chromium browsers
|
||||
arg.contains(&format!("--user-data-dir={profile_data_path_str}"))
|
||||
|| arg == profile_data_path_str
|
||||
} else if profile.browser == "tor-browser"
|
||||
// For Firefox-based browsers, check for exact profile path match
|
||||
if profile.browser == "tor-browser"
|
||||
|| profile.browser == "firefox"
|
||||
|| profile.browser == "firefox-developer"
|
||||
|| profile.browser == "mullvad-browser"
|
||||
@@ -577,13 +588,7 @@ impl ProfileManager {
|
||||
"zen" => exe_name.contains("zen"),
|
||||
"chromium" => exe_name.contains("chromium"),
|
||||
"brave" => exe_name.contains("brave"),
|
||||
"camoufox" => {
|
||||
exe_name.contains("camoufox")
|
||||
|| (exe_name.contains("firefox")
|
||||
&& cmd
|
||||
.iter()
|
||||
.any(|arg| arg.to_str().unwrap_or("").contains("camoufox")))
|
||||
}
|
||||
// Camoufox is handled via nodecar, not PID-based checking
|
||||
_ => false,
|
||||
};
|
||||
|
||||
@@ -660,6 +665,77 @@ impl ProfileManager {
|
||||
Ok(is_running)
|
||||
}
|
||||
|
||||
// Check Camoufox status using nodecar-based approach
|
||||
async fn check_camoufox_status_via_nodecar(
|
||||
&self,
|
||||
app_handle: &tauri::AppHandle,
|
||||
profile: &BrowserProfile,
|
||||
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
|
||||
use crate::camoufox::CamoufoxNodecarLauncher;
|
||||
|
||||
let launcher = CamoufoxNodecarLauncher::new(app_handle.clone());
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_data_path = profile.get_profile_data_path(&profiles_dir);
|
||||
let profile_path_str = profile_data_path.to_string_lossy();
|
||||
|
||||
// Check if there's a running Camoufox instance for this profile
|
||||
match launcher.find_camoufox_by_profile(&profile_path_str).await {
|
||||
Ok(Some(camoufox_process)) => {
|
||||
// Found a running instance, update profile with process info
|
||||
let mut updated_profile = profile.clone();
|
||||
updated_profile.process_id = camoufox_process.port;
|
||||
if let Err(e) = self.save_profile(&updated_profile) {
|
||||
println!("Warning: Failed to update Camoufox profile with process info: {e}");
|
||||
}
|
||||
|
||||
// Emit profile update event to frontend
|
||||
if let Err(e) = app_handle.emit("profile-updated", &updated_profile) {
|
||||
println!("Warning: Failed to emit profile update event: {e}");
|
||||
}
|
||||
|
||||
println!(
|
||||
"Camoufox profile '{}' is running with PID: {:?}",
|
||||
profile.name, camoufox_process.port
|
||||
);
|
||||
Ok(true)
|
||||
}
|
||||
Ok(None) => {
|
||||
// No running instance found, clear process ID if set
|
||||
if profile.process_id.is_some() {
|
||||
let mut updated_profile = profile.clone();
|
||||
updated_profile.process_id = None;
|
||||
if let Err(e) = self.save_profile(&updated_profile) {
|
||||
println!("Warning: Failed to clear Camoufox profile process info: {e}");
|
||||
}
|
||||
|
||||
// Emit profile update event to frontend
|
||||
if let Err(e) = app_handle.emit("profile-updated", &updated_profile) {
|
||||
println!("Warning: Failed to emit profile update event: {e}");
|
||||
}
|
||||
}
|
||||
println!("Camoufox profile '{}' is not running", profile.name);
|
||||
Ok(false)
|
||||
}
|
||||
Err(e) => {
|
||||
// Error checking status, assume not running and clear process ID
|
||||
println!("Warning: Failed to check Camoufox status via nodecar: {e}");
|
||||
if profile.process_id.is_some() {
|
||||
let mut updated_profile = profile.clone();
|
||||
updated_profile.process_id = None;
|
||||
if let Err(e) = self.save_profile(&updated_profile) {
|
||||
println!("Warning: Failed to clear Camoufox profile process info after error: {e}");
|
||||
}
|
||||
|
||||
// Emit profile update event to frontend
|
||||
if let Err(e) = app_handle.emit("profile-updated", &updated_profile) {
|
||||
println!("Warning: Failed to emit profile update event: {e}");
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if a process matches TOR/Mullvad browser
|
||||
fn is_tor_or_mullvad_browser(
|
||||
&self,
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
/// Utility functions for integration tests
|
||||
pub struct TestUtils;
|
||||
|
||||
impl TestUtils {
|
||||
/// Build the nodecar binary if it doesn't exist
|
||||
pub async fn ensure_nodecar_binary() -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>>
|
||||
{
|
||||
let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR")?;
|
||||
let project_root = PathBuf::from(cargo_manifest_dir)
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
let nodecar_dir = project_root.join("nodecar");
|
||||
let nodecar_binary = nodecar_dir.join("nodecar-bin");
|
||||
|
||||
// Check if binary already exists
|
||||
if nodecar_binary.exists() {
|
||||
return Ok(nodecar_binary);
|
||||
}
|
||||
|
||||
println!("Building nodecar binary for integration tests...");
|
||||
|
||||
// Install dependencies
|
||||
let install_status = Command::new("pnpm")
|
||||
.args(["install", "--frozen-lockfile"])
|
||||
.current_dir(&nodecar_dir)
|
||||
.status()?;
|
||||
|
||||
if !install_status.success() {
|
||||
return Err("Failed to install nodecar dependencies".into());
|
||||
}
|
||||
|
||||
// Build the binary
|
||||
let build_status = Command::new("pnpm")
|
||||
.args(["run", "build"])
|
||||
.current_dir(&nodecar_dir)
|
||||
.status()?;
|
||||
|
||||
if !build_status.success() {
|
||||
return Err("Failed to build nodecar binary".into());
|
||||
}
|
||||
|
||||
if !nodecar_binary.exists() {
|
||||
return Err("Nodecar binary was not created successfully".into());
|
||||
}
|
||||
|
||||
Ok(nodecar_binary)
|
||||
}
|
||||
|
||||
/// Get the appropriate build target for the current platform
|
||||
#[allow(dead_code)]
|
||||
fn get_build_target() -> &'static str {
|
||||
if cfg!(target_arch = "aarch64") && cfg!(target_os = "macos") {
|
||||
"build:mac-aarch64"
|
||||
} else if cfg!(target_arch = "x86_64") && cfg!(target_os = "macos") {
|
||||
"build:mac-x86_64"
|
||||
} else if cfg!(target_arch = "x86_64") && cfg!(target_os = "linux") {
|
||||
"build:linux-x64"
|
||||
} else if cfg!(target_arch = "aarch64") && cfg!(target_os = "linux") {
|
||||
"build:linux-arm64"
|
||||
} else if cfg!(target_arch = "x86_64") && cfg!(target_os = "windows") {
|
||||
"build:win-x64"
|
||||
} else if cfg!(target_arch = "aarch64") && cfg!(target_os = "windows") {
|
||||
"build:win-arm64"
|
||||
} else {
|
||||
panic!("Unsupported target architecture for nodecar build")
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a nodecar command with timeout
|
||||
pub async fn execute_nodecar_command(
|
||||
binary_path: &PathBuf,
|
||||
args: &[&str],
|
||||
timeout_secs: u64,
|
||||
) -> Result<std::process::Output, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut cmd = Command::new(binary_path);
|
||||
cmd.args(args);
|
||||
|
||||
// Add environment variable to ensure nodecar doesn't hang
|
||||
cmd.env("NODE_ENV", "test");
|
||||
|
||||
let output = timeout(Duration::from_secs(timeout_secs), async {
|
||||
tokio::process::Command::from(cmd).output().await
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Check if a port is available
|
||||
pub async fn is_port_available(port: u16) -> bool {
|
||||
tokio::net::TcpListener::bind(format!("127.0.0.1:{port}"))
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Wait for a port to become available or occupied
|
||||
pub async fn wait_for_port_state(port: u16, should_be_occupied: bool, timeout_secs: u64) -> bool {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
while start.elapsed().as_secs() < timeout_secs {
|
||||
let is_available = Self::is_port_available(port).await;
|
||||
|
||||
if should_be_occupied && !is_available {
|
||||
return true; // Port is occupied as expected
|
||||
} else if !should_be_occupied && is_available {
|
||||
return true; // Port is available as expected
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Create a temporary directory for test files
|
||||
pub fn create_temp_dir() -> Result<tempfile::TempDir, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Ok(tempfile::tempdir()?)
|
||||
}
|
||||
|
||||
/// Clean up all running nodecar processes (proxies and camoufox instances)
|
||||
pub async fn cleanup_all_nodecar_processes(
|
||||
nodecar_path: &PathBuf,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
println!("Cleaning up all nodecar processes...");
|
||||
|
||||
// Get list of all proxies and stop them individually
|
||||
let proxy_list_args = ["proxy", "list"];
|
||||
if let Ok(list_output) = Self::execute_nodecar_command(nodecar_path, &proxy_list_args, 10).await
|
||||
{
|
||||
if list_output.status.success() {
|
||||
let list_stdout = String::from_utf8(list_output.stdout)?;
|
||||
if let Ok(proxies) = serde_json::from_str::<serde_json::Value>(&list_stdout) {
|
||||
if let Some(proxy_array) = proxies.as_array() {
|
||||
for proxy in proxy_array {
|
||||
if let Some(proxy_id) = proxy["id"].as_str() {
|
||||
let stop_args = ["proxy", "stop", "--id", proxy_id];
|
||||
let _ = Self::execute_nodecar_command(nodecar_path, &stop_args, 10).await;
|
||||
println!("Stopped proxy: {proxy_id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get list of all camoufox instances and stop them individually
|
||||
let camoufox_list_args = ["camoufox", "list"];
|
||||
if let Ok(list_output) =
|
||||
Self::execute_nodecar_command(nodecar_path, &camoufox_list_args, 10).await
|
||||
{
|
||||
if list_output.status.success() {
|
||||
let list_stdout = String::from_utf8(list_output.stdout)?;
|
||||
if let Ok(instances) = serde_json::from_str::<serde_json::Value>(&list_stdout) {
|
||||
if let Some(instance_array) = instances.as_array() {
|
||||
for instance in instance_array {
|
||||
if let Some(instance_id) = instance["id"].as_str() {
|
||||
let stop_args = ["camoufox", "stop", "--id", instance_id];
|
||||
let _ = Self::execute_nodecar_command(nodecar_path, &stop_args, 30).await;
|
||||
println!("Stopped camoufox instance: {instance_id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Give processes time to clean up
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
|
||||
println!("Nodecar process cleanup completed");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,767 @@
|
||||
mod common;
|
||||
use common::TestUtils;
|
||||
use serde_json::Value;
|
||||
|
||||
/// Setup function to ensure clean state before tests
|
||||
async fn setup_test() -> Result<std::path::PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let nodecar_path = TestUtils::ensure_nodecar_binary().await?;
|
||||
|
||||
// Clean up any existing processes from previous test runs
|
||||
let _ = TestUtils::cleanup_all_nodecar_processes(&nodecar_path).await;
|
||||
|
||||
Ok(nodecar_path)
|
||||
}
|
||||
|
||||
/// Cleanup function to ensure clean state after tests
|
||||
async fn cleanup_test(nodecar_path: &std::path::PathBuf) {
|
||||
let _ = TestUtils::cleanup_all_nodecar_processes(nodecar_path).await;
|
||||
}
|
||||
|
||||
/// Helper function to stop a specific camoufox by ID
|
||||
async fn stop_camoufox_by_id(
|
||||
nodecar_path: &std::path::PathBuf,
|
||||
camoufox_id: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let stop_args = ["camoufox", "stop", "--id", camoufox_id];
|
||||
let _ = TestUtils::execute_nodecar_command(nodecar_path, &stop_args, 10).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Integration tests for nodecar proxy functionality
|
||||
#[tokio::test]
|
||||
async fn test_nodecar_proxy_lifecycle() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let nodecar_path = setup_test().await?;
|
||||
|
||||
// Test proxy start with a known working upstream
|
||||
let args = [
|
||||
"proxy",
|
||||
"start",
|
||||
"--host",
|
||||
"httpbin.org",
|
||||
"--proxy-port",
|
||||
"80",
|
||||
"--type",
|
||||
"http",
|
||||
];
|
||||
|
||||
println!("Starting proxy with nodecar...");
|
||||
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args, 30).await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
return Err(format!("Proxy start failed - stdout: {stdout}, stderr: {stderr}").into());
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8(output.stdout)?;
|
||||
let config: Value = serde_json::from_str(&stdout)?;
|
||||
|
||||
// Verify proxy configuration structure
|
||||
assert!(config["id"].is_string(), "Proxy ID should be a string");
|
||||
assert!(
|
||||
config["localPort"].is_number(),
|
||||
"Local port should be a number"
|
||||
);
|
||||
assert!(
|
||||
config["localUrl"].is_string(),
|
||||
"Local URL should be a string"
|
||||
);
|
||||
|
||||
let proxy_id = config["id"].as_str().unwrap();
|
||||
let local_port = config["localPort"].as_u64().unwrap() as u16;
|
||||
|
||||
println!("Proxy started with ID: {proxy_id} on port: {local_port}");
|
||||
|
||||
// Wait for the proxy to start listening
|
||||
let is_listening = TestUtils::wait_for_port_state(local_port, true, 10).await;
|
||||
assert!(
|
||||
is_listening,
|
||||
"Proxy should be listening on the assigned port"
|
||||
);
|
||||
|
||||
// Test stopping the proxy
|
||||
let stop_args = ["proxy", "stop", "--id", proxy_id];
|
||||
let stop_output = TestUtils::execute_nodecar_command(&nodecar_path, &stop_args, 10).await?;
|
||||
|
||||
assert!(stop_output.status.success(), "Proxy stop should succeed");
|
||||
|
||||
let port_available = TestUtils::wait_for_port_state(local_port, false, 5).await;
|
||||
assert!(
|
||||
port_available,
|
||||
"Port should be available after stopping proxy"
|
||||
);
|
||||
|
||||
cleanup_test(&nodecar_path).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test proxy with authentication
|
||||
#[tokio::test]
|
||||
async fn test_nodecar_proxy_with_auth() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let nodecar_path = setup_test().await?;
|
||||
|
||||
let args = [
|
||||
"proxy",
|
||||
"start",
|
||||
"--host",
|
||||
"httpbin.org",
|
||||
"--proxy-port",
|
||||
"80",
|
||||
"--type",
|
||||
"http",
|
||||
"--username",
|
||||
"testuser",
|
||||
"--password",
|
||||
"testpass",
|
||||
];
|
||||
|
||||
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args, 30).await?;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8(output.stdout)?;
|
||||
let config: Value = serde_json::from_str(&stdout)?;
|
||||
|
||||
// Clean up
|
||||
let proxy_id = config["id"].as_str().unwrap();
|
||||
let stop_args = ["proxy", "stop", "--id", proxy_id];
|
||||
let _ = TestUtils::execute_nodecar_command(&nodecar_path, &stop_args, 10).await;
|
||||
|
||||
// Verify upstream URL contains encoded credentials
|
||||
if let Some(upstream_url) = config["upstreamUrl"].as_str() {
|
||||
assert!(
|
||||
upstream_url.contains("testuser"),
|
||||
"Upstream URL should contain username"
|
||||
);
|
||||
// Password might be encoded, so we check for the presence of auth info
|
||||
assert!(
|
||||
upstream_url.contains("@"),
|
||||
"Upstream URL should contain auth separator"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup_test(&nodecar_path).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test proxy list functionality
|
||||
#[tokio::test]
|
||||
async fn test_nodecar_proxy_list() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let nodecar_path = setup_test().await?;
|
||||
|
||||
// Start a proxy first
|
||||
let start_args = [
|
||||
"proxy",
|
||||
"start",
|
||||
"--host",
|
||||
"httpbin.org",
|
||||
"--proxy-port",
|
||||
"80",
|
||||
"--type",
|
||||
"http",
|
||||
];
|
||||
|
||||
let start_output = TestUtils::execute_nodecar_command(&nodecar_path, &start_args, 30).await?;
|
||||
|
||||
if start_output.status.success() {
|
||||
let stdout = String::from_utf8(start_output.stdout)?;
|
||||
let config: Value = serde_json::from_str(&stdout)?;
|
||||
let proxy_id = config["id"].as_str().unwrap();
|
||||
|
||||
// Test list command
|
||||
let list_args = ["proxy", "list"];
|
||||
let list_output = TestUtils::execute_nodecar_command(&nodecar_path, &list_args, 10).await?;
|
||||
|
||||
assert!(list_output.status.success(), "Proxy list should succeed");
|
||||
|
||||
let list_stdout = String::from_utf8(list_output.stdout)?;
|
||||
let proxy_list: Value = serde_json::from_str(&list_stdout)?;
|
||||
|
||||
assert!(proxy_list.is_array(), "Proxy list should be an array");
|
||||
|
||||
let proxies = proxy_list.as_array().unwrap();
|
||||
assert!(
|
||||
!proxies.is_empty(),
|
||||
"Should have at least one proxy in the list"
|
||||
);
|
||||
|
||||
// Find our proxy in the list
|
||||
let found_proxy = proxies.iter().find(|p| p["id"].as_str() == Some(proxy_id));
|
||||
assert!(found_proxy.is_some(), "Started proxy should be in the list");
|
||||
|
||||
// Clean up
|
||||
let stop_args = ["proxy", "stop", "--id", proxy_id];
|
||||
let _ = TestUtils::execute_nodecar_command(&nodecar_path, &stop_args, 10).await;
|
||||
}
|
||||
|
||||
cleanup_test(&nodecar_path).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test Camoufox functionality
|
||||
#[tokio::test]
|
||||
async fn test_nodecar_camoufox_lifecycle() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let nodecar_path = setup_test().await?;
|
||||
|
||||
let temp_dir = TestUtils::create_temp_dir()?;
|
||||
let profile_path = temp_dir.path().join("test_profile");
|
||||
|
||||
let args = [
|
||||
"camoufox",
|
||||
"start",
|
||||
"--profile-path",
|
||||
profile_path.to_str().unwrap(),
|
||||
"--headless",
|
||||
"--debug",
|
||||
];
|
||||
|
||||
println!("Starting Camoufox with nodecar...");
|
||||
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args, 35).await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// If Camoufox is not installed or times out, skip the test
|
||||
if stderr.contains("not installed")
|
||||
|| stderr.contains("not found")
|
||||
|| stderr.contains("timeout")
|
||||
|| stdout.contains("timeout")
|
||||
{
|
||||
println!("Skipping Camoufox test - Camoufox not available or timed out");
|
||||
cleanup_test(&nodecar_path).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
cleanup_test(&nodecar_path).await;
|
||||
return Err(format!("Camoufox start failed - stdout: {stdout}, stderr: {stderr}").into());
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8(output.stdout)?;
|
||||
let config: Value = serde_json::from_str(&stdout)?;
|
||||
|
||||
// Verify Camoufox configuration structure
|
||||
assert!(config["id"].is_string(), "Camoufox ID should be a string");
|
||||
|
||||
let camoufox_id = config["id"].as_str().unwrap();
|
||||
println!("Camoufox started with ID: {camoufox_id}");
|
||||
|
||||
// Test stopping Camoufox
|
||||
let stop_args = ["camoufox", "stop", "--id", camoufox_id];
|
||||
let stop_output = TestUtils::execute_nodecar_command(&nodecar_path, &stop_args, 30).await?;
|
||||
|
||||
assert!(stop_output.status.success(), "Camoufox stop should succeed");
|
||||
|
||||
cleanup_test(&nodecar_path).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test Camoufox with URL opening
|
||||
#[tokio::test]
|
||||
async fn test_nodecar_camoufox_with_url() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let nodecar_path = setup_test().await?;
|
||||
|
||||
let temp_dir = TestUtils::create_temp_dir()?;
|
||||
let profile_path = temp_dir.path().join("test_profile_url");
|
||||
|
||||
let args = [
|
||||
"camoufox",
|
||||
"start",
|
||||
"--profile-path",
|
||||
profile_path.to_str().unwrap(),
|
||||
"--url",
|
||||
"https://httpbin.org/get",
|
||||
"--headless",
|
||||
"--debug",
|
||||
];
|
||||
|
||||
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args, 15).await?;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8(output.stdout)?;
|
||||
let config: Value = serde_json::from_str(&stdout)?;
|
||||
|
||||
let camoufox_id = config["id"].as_str().unwrap();
|
||||
|
||||
// Verify URL is set
|
||||
if let Some(url) = config["url"].as_str() {
|
||||
assert_eq!(
|
||||
url, "https://httpbin.org/get",
|
||||
"URL should match what was provided"
|
||||
);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
let _ = stop_camoufox_by_id(&nodecar_path, camoufox_id).await;
|
||||
} else {
|
||||
println!("Skipping Camoufox URL test - likely not installed");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
cleanup_test(&nodecar_path).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test Camoufox list functionality
|
||||
#[tokio::test]
|
||||
async fn test_nodecar_camoufox_list() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let nodecar_path = setup_test().await?;
|
||||
|
||||
// Test list command (should work even without Camoufox installed)
|
||||
let list_args = ["camoufox", "list"];
|
||||
let list_output = TestUtils::execute_nodecar_command(&nodecar_path, &list_args, 10).await?;
|
||||
|
||||
assert!(list_output.status.success(), "Camoufox list should succeed");
|
||||
|
||||
let list_stdout = String::from_utf8(list_output.stdout)?;
|
||||
let camoufox_list: Value = serde_json::from_str(&list_stdout)?;
|
||||
|
||||
assert!(camoufox_list.is_array(), "Camoufox list should be an array");
|
||||
|
||||
cleanup_test(&nodecar_path).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test Camoufox process tracking and management
|
||||
#[tokio::test]
|
||||
async fn test_nodecar_camoufox_process_tracking(
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let nodecar_path = setup_test().await?;
|
||||
|
||||
let temp_dir = TestUtils::create_temp_dir()?;
|
||||
let profile_path = temp_dir.path().join("test_profile_tracking");
|
||||
|
||||
// Start multiple Camoufox instances
|
||||
let mut instance_ids: Vec<String> = Vec::new();
|
||||
|
||||
for i in 0..2 {
|
||||
let instance_profile_path = format!("{}_instance_{}", profile_path.to_str().unwrap(), i);
|
||||
let args = [
|
||||
"camoufox",
|
||||
"start",
|
||||
"--profile-path",
|
||||
&instance_profile_path,
|
||||
"--headless",
|
||||
"--debug",
|
||||
];
|
||||
|
||||
println!("Starting Camoufox instance {i}...");
|
||||
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args, 10).await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// If Camoufox is not installed, skip the test
|
||||
if stderr.contains("not installed") || stderr.contains("not found") {
|
||||
println!("Skipping Camoufox process tracking test - Camoufox not installed");
|
||||
|
||||
// Clean up any instances that were started
|
||||
for instance_id in &instance_ids {
|
||||
let stop_args = ["camoufox", "stop", "--id", instance_id];
|
||||
let _ = TestUtils::execute_nodecar_command(&nodecar_path, &stop_args, 30).await;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
return Err(
|
||||
format!("Camoufox instance {i} start failed - stdout: {stdout}, stderr: {stderr}").into(),
|
||||
);
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8(output.stdout)?;
|
||||
let config: Value = serde_json::from_str(&stdout)?;
|
||||
|
||||
let camoufox_id = config["id"].as_str().unwrap().to_string();
|
||||
instance_ids.push(camoufox_id.clone());
|
||||
println!("Camoufox instance {i} started with ID: {camoufox_id}");
|
||||
}
|
||||
|
||||
// Verify all instances are tracked
|
||||
let list_args = ["camoufox", "list"];
|
||||
let list_output = TestUtils::execute_nodecar_command(&nodecar_path, &list_args, 10).await?;
|
||||
|
||||
assert!(list_output.status.success(), "Camoufox list should succeed");
|
||||
|
||||
let list_stdout = String::from_utf8(list_output.stdout)?;
|
||||
println!("Camoufox list output: {}", list_stdout);
|
||||
let instances: Value = serde_json::from_str(&list_stdout)?;
|
||||
|
||||
let instances_array = instances.as_array().unwrap();
|
||||
println!("Found {} instances in list", instances_array.len());
|
||||
|
||||
// Verify our instances are in the list
|
||||
for instance_id in &instance_ids {
|
||||
let instance_found = instances_array
|
||||
.iter()
|
||||
.any(|i| i["id"].as_str() == Some(instance_id));
|
||||
if !instance_found {
|
||||
println!(
|
||||
"Instance {} not found in list. Available instances:",
|
||||
instance_id
|
||||
);
|
||||
for instance in instances_array {
|
||||
if let Some(id) = instance["id"].as_str() {
|
||||
println!(" - {}", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
instance_found,
|
||||
"Camoufox instance {instance_id} should be found in list"
|
||||
);
|
||||
}
|
||||
|
||||
// Stop all instances individually
|
||||
for instance_id in &instance_ids {
|
||||
println!("Stopping Camoufox instance: {instance_id}");
|
||||
let stop_args = ["camoufox", "stop", "--id", instance_id];
|
||||
let stop_output = TestUtils::execute_nodecar_command(&nodecar_path, &stop_args, 30).await?;
|
||||
|
||||
assert!(
|
||||
stop_output.status.success(),
|
||||
"Camoufox stop should succeed for instance {instance_id}"
|
||||
);
|
||||
|
||||
let stop_stdout = String::from_utf8(stop_output.stdout)?;
|
||||
let stop_result: Value = serde_json::from_str(&stop_stdout)?;
|
||||
assert!(
|
||||
stop_result["success"].as_bool().unwrap_or(false),
|
||||
"Stop result should indicate success for instance {instance_id}"
|
||||
);
|
||||
}
|
||||
|
||||
// Verify all instances are removed
|
||||
let list_output_after = TestUtils::execute_nodecar_command(&nodecar_path, &list_args, 10).await?;
|
||||
|
||||
let instances_after: Value = serde_json::from_str(&String::from_utf8(list_output_after.stdout)?)?;
|
||||
let instances_after_array = instances_after.as_array().unwrap();
|
||||
|
||||
for instance_id in &instance_ids {
|
||||
let instance_still_exists = instances_after_array
|
||||
.iter()
|
||||
.any(|i| i["id"].as_str() == Some(instance_id));
|
||||
assert!(
|
||||
!instance_still_exists,
|
||||
"Stopped Camoufox instance {instance_id} should not be found in list"
|
||||
);
|
||||
}
|
||||
|
||||
println!("Camoufox process tracking test completed successfully");
|
||||
cleanup_test(&nodecar_path).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test Camoufox with various configuration options
|
||||
#[tokio::test]
|
||||
async fn test_nodecar_camoufox_configuration_options(
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let nodecar_path = setup_test().await?;
|
||||
|
||||
let temp_dir = TestUtils::create_temp_dir()?;
|
||||
let profile_path = temp_dir.path().join("test_profile_config");
|
||||
|
||||
let args = [
|
||||
"camoufox",
|
||||
"start",
|
||||
"--profile-path",
|
||||
profile_path.to_str().unwrap(),
|
||||
"--headless",
|
||||
"--debug",
|
||||
"--os",
|
||||
"linux",
|
||||
"--block-images",
|
||||
"--humanize",
|
||||
"--locale",
|
||||
"en-US,en-GB",
|
||||
"--timezone",
|
||||
"America/New_York",
|
||||
"--disable-cache",
|
||||
];
|
||||
|
||||
println!("Starting Camoufox with configuration options...");
|
||||
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args, 15).await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// If Camoufox is not installed, skip the test
|
||||
if stderr.contains("not installed") || stderr.contains("not found") {
|
||||
println!("Skipping Camoufox configuration test - Camoufox not installed");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
return Err(
|
||||
format!("Camoufox with config start failed - stdout: {stdout}, stderr: {stderr}").into(),
|
||||
);
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8(output.stdout)?;
|
||||
let config: Value = serde_json::from_str(&stdout)?;
|
||||
|
||||
let camoufox_id = config["id"].as_str().unwrap();
|
||||
println!("Camoufox with configuration started with ID: {camoufox_id}");
|
||||
|
||||
// Verify configuration was applied by checking the profile path
|
||||
if let Some(returned_profile_path) = config["profilePath"].as_str() {
|
||||
assert!(
|
||||
returned_profile_path.contains("test_profile_config"),
|
||||
"Profile path should match what was provided"
|
||||
);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
let _ = stop_camoufox_by_id(&nodecar_path, camoufox_id).await;
|
||||
|
||||
println!("Camoufox configuration test completed successfully");
|
||||
cleanup_test(&nodecar_path).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test nodecar command validation
|
||||
#[tokio::test]
|
||||
async fn test_nodecar_command_validation() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let nodecar_path = setup_test().await?;
|
||||
|
||||
// Test invalid command
|
||||
let invalid_args = ["invalid", "command"];
|
||||
let output = TestUtils::execute_nodecar_command(&nodecar_path, &invalid_args, 10).await?;
|
||||
|
||||
assert!(!output.status.success(), "Invalid command should fail");
|
||||
|
||||
// Test proxy without required arguments
|
||||
let incomplete_args = ["proxy", "start"];
|
||||
let output = TestUtils::execute_nodecar_command(&nodecar_path, &incomplete_args, 10).await?;
|
||||
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"Incomplete proxy command should fail"
|
||||
);
|
||||
|
||||
cleanup_test(&nodecar_path).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test concurrent proxy operations
|
||||
#[tokio::test]
|
||||
async fn test_nodecar_concurrent_proxies() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let nodecar_path = setup_test().await?;
|
||||
|
||||
// Start multiple proxies concurrently
|
||||
let mut handles = vec![];
|
||||
let mut proxy_ids: Vec<String> = vec![];
|
||||
|
||||
for i in 0..3 {
|
||||
let nodecar_path_clone = nodecar_path.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
let args = [
|
||||
"proxy",
|
||||
"start",
|
||||
"--host",
|
||||
"httpbin.org",
|
||||
"--proxy-port",
|
||||
"80",
|
||||
"--type",
|
||||
"http",
|
||||
];
|
||||
|
||||
TestUtils::execute_nodecar_command(&nodecar_path_clone, &args, 30).await
|
||||
});
|
||||
handles.push((i, handle));
|
||||
}
|
||||
|
||||
// Wait for all proxies to start
|
||||
for (i, handle) in handles {
|
||||
match handle.await.map_err(|e| format!("Join error: {e}"))? {
|
||||
Ok(output) if output.status.success() => {
|
||||
let stdout = String::from_utf8(output.stdout)?;
|
||||
let config: Value = serde_json::from_str(&stdout)?;
|
||||
let proxy_id = config["id"].as_str().unwrap().to_string();
|
||||
proxy_ids.push(proxy_id);
|
||||
println!("Proxy {i} started successfully");
|
||||
}
|
||||
Ok(output) => {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
println!("Proxy {i} failed to start: {stderr}");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Proxy {i} error: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up all started proxies
|
||||
for proxy_id in proxy_ids {
|
||||
let stop_args = ["proxy", "stop", "--id", &proxy_id];
|
||||
let _ = TestUtils::execute_nodecar_command(&nodecar_path, &stop_args, 10).await;
|
||||
}
|
||||
|
||||
cleanup_test(&nodecar_path).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test proxy with different upstream types
|
||||
#[tokio::test]
|
||||
async fn test_nodecar_proxy_types() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let nodecar_path = setup_test().await?;
|
||||
|
||||
let test_cases = vec![
|
||||
("http", "httpbin.org", "80"),
|
||||
("https", "httpbin.org", "443"),
|
||||
];
|
||||
|
||||
for (proxy_type, host, port) in test_cases {
|
||||
println!("Testing {proxy_type} proxy to {host}:{port}");
|
||||
|
||||
let args = [
|
||||
"proxy",
|
||||
"start",
|
||||
"--host",
|
||||
host,
|
||||
"--proxy-port",
|
||||
port,
|
||||
"--type",
|
||||
proxy_type,
|
||||
];
|
||||
|
||||
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args, 30).await?;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8(output.stdout)?;
|
||||
let config: Value = serde_json::from_str(&stdout)?;
|
||||
let proxy_id = config["id"].as_str().unwrap();
|
||||
|
||||
// Clean up
|
||||
let stop_args = ["proxy", "stop", "--id", proxy_id];
|
||||
let _ = TestUtils::execute_nodecar_command(&nodecar_path, &stop_args, 10).await;
|
||||
|
||||
println!("{proxy_type} proxy test passed");
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
println!("{proxy_type} proxy test failed: {stderr}");
|
||||
}
|
||||
}
|
||||
|
||||
cleanup_test(&nodecar_path).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test SOCKS5 proxy chaining - create two proxies where the second uses the first as upstream
|
||||
#[tokio::test]
|
||||
async fn test_nodecar_socks5_proxy_chaining() -> Result<(), Box<dyn std::error::Error + Send + Sync>>
|
||||
{
|
||||
let nodecar_path = setup_test().await?;
|
||||
|
||||
// Step 1: Start a SOCKS5 proxy with a known working upstream (httpbin.org)
|
||||
let socks5_args = [
|
||||
"proxy",
|
||||
"start",
|
||||
"--host",
|
||||
"httpbin.org",
|
||||
"--proxy-port",
|
||||
"80",
|
||||
"--type",
|
||||
"http", // Use HTTP upstream for the first proxy
|
||||
];
|
||||
|
||||
println!("Starting first proxy with HTTP upstream...");
|
||||
let socks5_output = TestUtils::execute_nodecar_command(&nodecar_path, &socks5_args, 30).await?;
|
||||
|
||||
if !socks5_output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&socks5_output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&socks5_output.stdout);
|
||||
return Err(format!("First proxy start failed - stdout: {stdout}, stderr: {stderr}").into());
|
||||
}
|
||||
|
||||
let socks5_stdout = String::from_utf8(socks5_output.stdout)?;
|
||||
let socks5_config: Value = serde_json::from_str(&socks5_stdout)?;
|
||||
|
||||
let socks5_proxy_id = socks5_config["id"].as_str().unwrap();
|
||||
let socks5_local_port = socks5_config["localPort"].as_u64().unwrap() as u16;
|
||||
|
||||
println!("First proxy started with ID: {socks5_proxy_id} on port: {socks5_local_port}");
|
||||
|
||||
// Step 2: Start a second proxy that uses the first proxy as upstream
|
||||
let http_proxy_args = [
|
||||
"proxy",
|
||||
"start",
|
||||
"--upstream",
|
||||
&format!("http://127.0.0.1:{socks5_local_port}"),
|
||||
];
|
||||
|
||||
println!("Starting second proxy with first proxy as upstream...");
|
||||
let http_output = TestUtils::execute_nodecar_command(&nodecar_path, &http_proxy_args, 30).await?;
|
||||
|
||||
if !http_output.status.success() {
|
||||
// Clean up first proxy before failing
|
||||
let stop_socks5_args = ["proxy", "stop", "--id", socks5_proxy_id, "--type", "socks5"];
|
||||
let _ = TestUtils::execute_nodecar_command(&nodecar_path, &stop_socks5_args, 10).await;
|
||||
|
||||
let stderr = String::from_utf8_lossy(&http_output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&http_output.stdout);
|
||||
return Err(
|
||||
format!("Second proxy with chained upstream failed - stdout: {stdout}, stderr: {stderr}")
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
let http_stdout = String::from_utf8(http_output.stdout)?;
|
||||
let http_config: Value = serde_json::from_str(&http_stdout)?;
|
||||
|
||||
let http_proxy_id = http_config["id"].as_str().unwrap();
|
||||
let http_local_port = http_config["localPort"].as_u64().unwrap() as u16;
|
||||
|
||||
println!(
|
||||
"Second proxy started with ID: {http_proxy_id} on port: {http_local_port} (chained through first proxy)"
|
||||
);
|
||||
|
||||
// Verify both proxies are listening by waiting for them to be occupied
|
||||
let socks5_listening = TestUtils::wait_for_port_state(socks5_local_port, true, 5).await;
|
||||
let http_listening = TestUtils::wait_for_port_state(http_local_port, true, 5).await;
|
||||
|
||||
assert!(
|
||||
socks5_listening,
|
||||
"First proxy should be listening on port {socks5_local_port}"
|
||||
);
|
||||
assert!(
|
||||
http_listening,
|
||||
"Second proxy should be listening on port {http_local_port}"
|
||||
);
|
||||
|
||||
// Clean up both proxies
|
||||
let stop_http_args = ["proxy", "stop", "--id", http_proxy_id];
|
||||
let stop_socks5_args = ["proxy", "stop", "--id", socks5_proxy_id];
|
||||
|
||||
let http_stop_result =
|
||||
TestUtils::execute_nodecar_command(&nodecar_path, &stop_http_args, 10).await;
|
||||
let socks5_stop_result =
|
||||
TestUtils::execute_nodecar_command(&nodecar_path, &stop_socks5_args, 10).await;
|
||||
|
||||
// Verify cleanup
|
||||
assert!(
|
||||
http_stop_result.is_ok() && http_stop_result.unwrap().status.success(),
|
||||
"Second proxy stop should succeed"
|
||||
);
|
||||
assert!(
|
||||
socks5_stop_result.is_ok() && socks5_stop_result.unwrap().status.success(),
|
||||
"First proxy stop should succeed"
|
||||
);
|
||||
|
||||
let http_port_available = TestUtils::wait_for_port_state(http_local_port, false, 5).await;
|
||||
let socks5_port_available = TestUtils::wait_for_port_state(socks5_local_port, false, 5).await;
|
||||
|
||||
assert!(
|
||||
http_port_available,
|
||||
"Second proxy port should be available after stopping"
|
||||
);
|
||||
assert!(
|
||||
socks5_port_available,
|
||||
"First proxy port should be available after stopping"
|
||||
);
|
||||
|
||||
println!("Proxy chaining test completed successfully");
|
||||
cleanup_test(&nodecar_path).await;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user