Add Chrome headless diagnostic tool
Runs automatically when --chrome-headless flag is used, checking: - ChromeDriver installation and PATH - Chrome/Chromium installation - Chrome and ChromeDriver version compatibility - config.toml chrome_binary setting - Chrome for Testing installation - ChromeDriver executable permissions (macOS quarantine) Displays a detailed report with: - Summary of detected versions and paths - Pass/warning/error status for each check - Specific fix suggestions for any issues found Users can then ask g3 to help fix any detected issues.
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1353,6 +1353,7 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"crossterm 0.29.0",
|
"crossterm 0.29.0",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
|
"g3-computer-control",
|
||||||
"g3-config",
|
"g3-config",
|
||||||
"g3-core",
|
"g3-core",
|
||||||
"g3-ensembles",
|
"g3-ensembles",
|
||||||
@@ -1386,6 +1387,7 @@ dependencies = [
|
|||||||
"cocoa 0.25.0",
|
"cocoa 0.25.0",
|
||||||
"core-foundation 0.10.1",
|
"core-foundation 0.10.1",
|
||||||
"core-graphics 0.23.2",
|
"core-graphics 0.23.2",
|
||||||
|
"dirs 5.0.1",
|
||||||
"fantoccini",
|
"fantoccini",
|
||||||
"image",
|
"image",
|
||||||
"objc",
|
"objc",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ description = "CLI interface for G3 AI coding agent"
|
|||||||
g3-core = { path = "../g3-core" }
|
g3-core = { path = "../g3-core" }
|
||||||
g3-config = { path = "../g3-config" }
|
g3-config = { path = "../g3-config" }
|
||||||
g3-planner = { path = "../g3-planner" }
|
g3-planner = { path = "../g3-planner" }
|
||||||
|
g3-computer-control = { path = "../g3-computer-control" }
|
||||||
g3-providers = { path = "../g3-providers" }
|
g3-providers = { path = "../g3-providers" }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
g3-ensembles = { path = "../g3-ensembles" }
|
g3-ensembles = { path = "../g3-ensembles" }
|
||||||
|
|||||||
@@ -548,6 +548,17 @@ pub async fn run() -> Result<()> {
|
|||||||
if cli.chrome_headless {
|
if cli.chrome_headless {
|
||||||
config.webdriver.enabled = true;
|
config.webdriver.enabled = true;
|
||||||
config.webdriver.browser = g3_config::WebDriverBrowser::ChromeHeadless;
|
config.webdriver.browser = g3_config::WebDriverBrowser::ChromeHeadless;
|
||||||
|
|
||||||
|
// Run Chrome diagnostics on first use
|
||||||
|
let report = g3_computer_control::run_chrome_diagnostics(
|
||||||
|
config.webdriver.chrome_binary.as_deref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Display the diagnostic report
|
||||||
|
println!("{}", report.format_report());
|
||||||
|
|
||||||
|
// If there are errors, the user can ask g3 to help fix them
|
||||||
|
// We continue anyway to let the user decide
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply safari flag override
|
// Apply safari flag override
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ tracing = { workspace = true }
|
|||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
|
|
||||||
shellexpand = "3.1"
|
shellexpand = "3.1"
|
||||||
|
dirs = "5.0"
|
||||||
# Async trait support
|
# Async trait support
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ pub mod webdriver;
|
|||||||
// Re-export webdriver types for convenience
|
// Re-export webdriver types for convenience
|
||||||
pub use webdriver::{
|
pub use webdriver::{
|
||||||
chrome::ChromeDriver, safari::SafariDriver, WebDriverController, WebElement,
|
chrome::ChromeDriver, safari::SafariDriver, WebDriverController, WebElement,
|
||||||
|
diagnostics::{run_diagnostics as run_chrome_diagnostics, ChromeDiagnosticReport, DiagnosticStatus},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Re-export macax types for convenience
|
// Re-export macax types for convenience
|
||||||
|
|||||||
541
crates/g3-computer-control/src/webdriver/diagnostics.rs
Normal file
541
crates/g3-computer-control/src/webdriver/diagnostics.rs
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
//! Chrome WebDriver diagnostics module
|
||||||
|
//!
|
||||||
|
//! Checks for common setup issues and provides detailed fix suggestions.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Result of a diagnostic check
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DiagnosticResult {
|
||||||
|
pub name: String,
|
||||||
|
pub status: DiagnosticStatus,
|
||||||
|
pub message: String,
|
||||||
|
pub fix_suggestion: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum DiagnosticStatus {
|
||||||
|
Ok,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full diagnostic report for Chrome headless setup
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ChromeDiagnosticReport {
|
||||||
|
pub results: Vec<DiagnosticResult>,
|
||||||
|
pub chrome_version: Option<String>,
|
||||||
|
pub chromedriver_version: Option<String>,
|
||||||
|
pub chrome_path: Option<PathBuf>,
|
||||||
|
pub chromedriver_path: Option<PathBuf>,
|
||||||
|
pub config_chrome_binary: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChromeDiagnosticReport {
|
||||||
|
/// Check if all diagnostics passed
|
||||||
|
pub fn all_ok(&self) -> bool {
|
||||||
|
self.results.iter().all(|r| r.status == DiagnosticStatus::Ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if there are any errors (not just warnings)
|
||||||
|
pub fn has_errors(&self) -> bool {
|
||||||
|
self.results.iter().any(|r| r.status == DiagnosticStatus::Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format the report as a human-readable string
|
||||||
|
pub fn format_report(&self) -> String {
|
||||||
|
let mut output = String::new();
|
||||||
|
output.push_str("\n╔══════════════════════════════════════════════════════════════╗\n");
|
||||||
|
output.push_str("║ Chrome Headless Diagnostic Report ║\n");
|
||||||
|
output.push_str("╚══════════════════════════════════════════════════════════════╝\n\n");
|
||||||
|
|
||||||
|
// Summary section
|
||||||
|
output.push_str("📋 **Summary**\n");
|
||||||
|
if let Some(ref path) = self.chrome_path {
|
||||||
|
output.push_str(&format!(" Chrome: {}\n", path.display()));
|
||||||
|
}
|
||||||
|
if let Some(ref ver) = self.chrome_version {
|
||||||
|
output.push_str(&format!(" Chrome Version: {}\n", ver));
|
||||||
|
}
|
||||||
|
if let Some(ref path) = self.chromedriver_path {
|
||||||
|
output.push_str(&format!(" ChromeDriver: {}\n", path.display()));
|
||||||
|
}
|
||||||
|
if let Some(ref ver) = self.chromedriver_version {
|
||||||
|
output.push_str(&format!(" ChromeDriver Version: {}\n", ver));
|
||||||
|
}
|
||||||
|
if let Some(ref binary) = self.config_chrome_binary {
|
||||||
|
output.push_str(&format!(" Config chrome_binary: {}\n", binary));
|
||||||
|
}
|
||||||
|
output.push_str("\n");
|
||||||
|
|
||||||
|
// Results section
|
||||||
|
output.push_str("🔍 **Diagnostic Results**\n\n");
|
||||||
|
|
||||||
|
for result in &self.results {
|
||||||
|
let icon = match result.status {
|
||||||
|
DiagnosticStatus::Ok => "✅",
|
||||||
|
DiagnosticStatus::Warning => "⚠️",
|
||||||
|
DiagnosticStatus::Error => "❌",
|
||||||
|
};
|
||||||
|
output.push_str(&format!("{} **{}**\n", icon, result.name));
|
||||||
|
output.push_str(&format!(" {}\n", result.message));
|
||||||
|
|
||||||
|
if let Some(ref fix) = result.fix_suggestion {
|
||||||
|
output.push_str(&format!(" 💡 Fix: {}\n", fix));
|
||||||
|
}
|
||||||
|
output.push_str("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overall status
|
||||||
|
if self.all_ok() {
|
||||||
|
output.push_str("🎉 **All checks passed!** Chrome headless is ready to use.\n");
|
||||||
|
} else if self.has_errors() {
|
||||||
|
output.push_str("\n🛠️ **Action Required**\n");
|
||||||
|
output.push_str(" Some issues need to be fixed before Chrome headless will work.\n");
|
||||||
|
output.push_str(" You can ask me to help fix these issues.\n");
|
||||||
|
} else {
|
||||||
|
output.push_str("\n⚠️ **Warnings Present**\n");
|
||||||
|
output.push_str(" Chrome headless may work, but there are potential issues.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run all Chrome headless diagnostics
|
||||||
|
pub fn run_diagnostics(config_chrome_binary: Option<&str>) -> ChromeDiagnosticReport {
|
||||||
|
let mut results = Vec::new();
|
||||||
|
let mut chrome_version = None;
|
||||||
|
let mut chromedriver_version = None;
|
||||||
|
let mut chrome_path = None;
|
||||||
|
let mut chromedriver_path = None;
|
||||||
|
|
||||||
|
// 1. Check for ChromeDriver in PATH
|
||||||
|
let chromedriver_check = check_chromedriver_installed();
|
||||||
|
if chromedriver_check.status == DiagnosticStatus::Ok {
|
||||||
|
chromedriver_path = find_chromedriver_path();
|
||||||
|
chromedriver_version = get_chromedriver_version();
|
||||||
|
}
|
||||||
|
results.push(chromedriver_check);
|
||||||
|
|
||||||
|
// 2. Check for Chrome installation
|
||||||
|
let chrome_check = check_chrome_installed(config_chrome_binary);
|
||||||
|
if chrome_check.status == DiagnosticStatus::Ok {
|
||||||
|
chrome_path = find_chrome_path(config_chrome_binary);
|
||||||
|
chrome_version = get_chrome_version(config_chrome_binary);
|
||||||
|
}
|
||||||
|
results.push(chrome_check);
|
||||||
|
|
||||||
|
// 3. Check version compatibility
|
||||||
|
if chrome_version.is_some() && chromedriver_version.is_some() {
|
||||||
|
results.push(check_version_compatibility(
|
||||||
|
chrome_version.as_deref(),
|
||||||
|
chromedriver_version.as_deref(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Check config.toml chrome_binary setting
|
||||||
|
results.push(check_config_chrome_binary(config_chrome_binary, chrome_path.as_ref()));
|
||||||
|
|
||||||
|
// 5. Check for Chrome for Testing installation
|
||||||
|
results.push(check_chrome_for_testing());
|
||||||
|
|
||||||
|
// 6. Check ChromeDriver is executable (macOS quarantine)
|
||||||
|
if chromedriver_path.is_some() {
|
||||||
|
results.push(check_chromedriver_executable());
|
||||||
|
}
|
||||||
|
|
||||||
|
ChromeDiagnosticReport {
|
||||||
|
results,
|
||||||
|
chrome_version,
|
||||||
|
chromedriver_version,
|
||||||
|
chrome_path,
|
||||||
|
chromedriver_path,
|
||||||
|
config_chrome_binary: config_chrome_binary.map(String::from),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if ChromeDriver is installed and in PATH
|
||||||
|
fn check_chromedriver_installed() -> DiagnosticResult {
|
||||||
|
match Command::new("which").arg("chromedriver").output() {
|
||||||
|
Ok(output) if output.status.success() => {
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "ChromeDriver Installation".to_string(),
|
||||||
|
status: DiagnosticStatus::Ok,
|
||||||
|
message: "ChromeDriver found in PATH".to_string(),
|
||||||
|
fix_suggestion: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Check common locations
|
||||||
|
let common_paths = [
|
||||||
|
dirs::home_dir().map(|h| h.join(".chrome-for-testing/chromedriver-mac-arm64/chromedriver")),
|
||||||
|
dirs::home_dir().map(|h| h.join(".chrome-for-testing/chromedriver-mac-x64/chromedriver")),
|
||||||
|
Some(PathBuf::from("/usr/local/bin/chromedriver")),
|
||||||
|
Some(PathBuf::from("/opt/homebrew/bin/chromedriver")),
|
||||||
|
];
|
||||||
|
|
||||||
|
for path in common_paths.iter().flatten() {
|
||||||
|
if path.exists() {
|
||||||
|
return DiagnosticResult {
|
||||||
|
name: "ChromeDriver Installation".to_string(),
|
||||||
|
status: DiagnosticStatus::Warning,
|
||||||
|
message: format!("ChromeDriver found at {} but not in PATH", path.display()),
|
||||||
|
fix_suggestion: Some(format!(
|
||||||
|
"Add to your shell config (~/.zshrc or ~/.bashrc):\nexport PATH=\"{}:$PATH\"",
|
||||||
|
path.parent().unwrap().display()
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "ChromeDriver Installation".to_string(),
|
||||||
|
status: DiagnosticStatus::Error,
|
||||||
|
message: "ChromeDriver not found".to_string(),
|
||||||
|
fix_suggestion: Some(
|
||||||
|
"Install ChromeDriver using one of these methods:\n\
|
||||||
|
1. Run: ./scripts/setup-chrome-for-testing.sh (recommended)\n\
|
||||||
|
2. Or: brew install chromedriver".to_string()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if Chrome is installed
|
||||||
|
fn check_chrome_installed(config_binary: Option<&str>) -> DiagnosticResult {
|
||||||
|
// First check configured binary
|
||||||
|
if let Some(binary) = config_binary {
|
||||||
|
if PathBuf::from(binary).exists() {
|
||||||
|
return DiagnosticResult {
|
||||||
|
name: "Chrome Installation".to_string(),
|
||||||
|
status: DiagnosticStatus::Ok,
|
||||||
|
message: format!("Chrome found at configured path: {}", binary),
|
||||||
|
fix_suggestion: None,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return DiagnosticResult {
|
||||||
|
name: "Chrome Installation".to_string(),
|
||||||
|
status: DiagnosticStatus::Error,
|
||||||
|
message: format!("Configured chrome_binary not found: {}", binary),
|
||||||
|
fix_suggestion: Some(
|
||||||
|
"Update chrome_binary in ~/.config/g3/config.toml to a valid Chrome path,\n\
|
||||||
|
or remove it to use system Chrome".to_string()
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check common Chrome locations
|
||||||
|
let chrome_paths = get_chrome_search_paths();
|
||||||
|
|
||||||
|
for path in &chrome_paths {
|
||||||
|
if path.exists() {
|
||||||
|
return DiagnosticResult {
|
||||||
|
name: "Chrome Installation".to_string(),
|
||||||
|
status: DiagnosticStatus::Ok,
|
||||||
|
message: format!("Chrome found at: {}", path.display()),
|
||||||
|
fix_suggestion: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "Chrome Installation".to_string(),
|
||||||
|
status: DiagnosticStatus::Error,
|
||||||
|
message: "Chrome/Chromium not found".to_string(),
|
||||||
|
fix_suggestion: Some(
|
||||||
|
"Install Chrome using one of these methods:\n\
|
||||||
|
1. Run: ./scripts/setup-chrome-for-testing.sh (recommended)\n\
|
||||||
|
2. Download from: https://www.google.com/chrome/\n\
|
||||||
|
3. Or: brew install --cask google-chrome".to_string()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check Chrome and ChromeDriver version compatibility
|
||||||
|
fn check_version_compatibility(
|
||||||
|
chrome_ver: Option<&str>,
|
||||||
|
chromedriver_ver: Option<&str>,
|
||||||
|
) -> DiagnosticResult {
|
||||||
|
let chrome_major = chrome_ver.and_then(extract_major_version);
|
||||||
|
let driver_major = chromedriver_ver.and_then(extract_major_version);
|
||||||
|
|
||||||
|
match (chrome_major, driver_major) {
|
||||||
|
(Some(cv), Some(dv)) if cv == dv => {
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "Version Compatibility".to_string(),
|
||||||
|
status: DiagnosticStatus::Ok,
|
||||||
|
message: format!("Chrome ({}) and ChromeDriver ({}) versions match", cv, dv),
|
||||||
|
fix_suggestion: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(cv), Some(dv)) => {
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "Version Compatibility".to_string(),
|
||||||
|
status: DiagnosticStatus::Error,
|
||||||
|
message: format!(
|
||||||
|
"Version mismatch! Chrome is v{} but ChromeDriver is v{}",
|
||||||
|
cv, dv
|
||||||
|
),
|
||||||
|
fix_suggestion: Some(
|
||||||
|
"Fix version mismatch:\n\
|
||||||
|
1. Run: ./scripts/setup-chrome-for-testing.sh (installs matching versions)\n\
|
||||||
|
2. Or update ChromeDriver: brew upgrade chromedriver".to_string()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "Version Compatibility".to_string(),
|
||||||
|
status: DiagnosticStatus::Warning,
|
||||||
|
message: "Could not determine version compatibility".to_string(),
|
||||||
|
fix_suggestion: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check config.toml chrome_binary setting
|
||||||
|
fn check_config_chrome_binary(
|
||||||
|
config_binary: Option<&str>,
|
||||||
|
detected_chrome: Option<&PathBuf>,
|
||||||
|
) -> DiagnosticResult {
|
||||||
|
match (config_binary, detected_chrome) {
|
||||||
|
(Some(binary), _) if PathBuf::from(binary).exists() => {
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "Config chrome_binary".to_string(),
|
||||||
|
status: DiagnosticStatus::Ok,
|
||||||
|
message: "chrome_binary is configured and valid".to_string(),
|
||||||
|
fix_suggestion: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(binary), _) => {
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "Config chrome_binary".to_string(),
|
||||||
|
status: DiagnosticStatus::Error,
|
||||||
|
message: format!("chrome_binary path does not exist: {}", binary),
|
||||||
|
fix_suggestion: Some(
|
||||||
|
"Update ~/.config/g3/config.toml with a valid chrome_binary path".to_string()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(None, Some(chrome)) => {
|
||||||
|
// Check if it's Chrome for Testing - recommend configuring it
|
||||||
|
let chrome_str = chrome.to_string_lossy();
|
||||||
|
if chrome_str.contains("chrome-for-testing") || chrome_str.contains("Chrome for Testing") {
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "Config chrome_binary".to_string(),
|
||||||
|
status: DiagnosticStatus::Warning,
|
||||||
|
message: "Chrome for Testing detected but not configured in config.toml".to_string(),
|
||||||
|
fix_suggestion: Some(format!(
|
||||||
|
"Add to ~/.config/g3/config.toml:\n\
|
||||||
|
[webdriver]\n\
|
||||||
|
chrome_binary = \"{}\"",
|
||||||
|
chrome.display()
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "Config chrome_binary".to_string(),
|
||||||
|
status: DiagnosticStatus::Ok,
|
||||||
|
message: "Using system Chrome (no chrome_binary configured)".to_string(),
|
||||||
|
fix_suggestion: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(None, None) => {
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "Config chrome_binary".to_string(),
|
||||||
|
status: DiagnosticStatus::Warning,
|
||||||
|
message: "No chrome_binary configured and no Chrome detected".to_string(),
|
||||||
|
fix_suggestion: Some(
|
||||||
|
"Install Chrome and optionally configure chrome_binary in config.toml".to_string()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check for Chrome for Testing installation
|
||||||
|
fn check_chrome_for_testing() -> DiagnosticResult {
|
||||||
|
let cft_dir = dirs::home_dir().map(|h| h.join(".chrome-for-testing"));
|
||||||
|
|
||||||
|
match cft_dir {
|
||||||
|
Some(dir) if dir.exists() => {
|
||||||
|
// Check for both Chrome and ChromeDriver
|
||||||
|
let has_chrome = dir.join("chrome-mac-arm64").exists()
|
||||||
|
|| dir.join("chrome-mac-x64").exists();
|
||||||
|
let has_driver = dir.join("chromedriver-mac-arm64").exists()
|
||||||
|
|| dir.join("chromedriver-mac-x64").exists();
|
||||||
|
|
||||||
|
if has_chrome && has_driver {
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "Chrome for Testing".to_string(),
|
||||||
|
status: DiagnosticStatus::Ok,
|
||||||
|
message: "Chrome for Testing is installed with matching ChromeDriver".to_string(),
|
||||||
|
fix_suggestion: None,
|
||||||
|
}
|
||||||
|
} else if has_chrome {
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "Chrome for Testing".to_string(),
|
||||||
|
status: DiagnosticStatus::Warning,
|
||||||
|
message: "Chrome for Testing found but ChromeDriver is missing".to_string(),
|
||||||
|
fix_suggestion: Some(
|
||||||
|
"Run: ./scripts/setup-chrome-for-testing.sh to install matching ChromeDriver".to_string()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "Chrome for Testing".to_string(),
|
||||||
|
status: DiagnosticStatus::Warning,
|
||||||
|
message: "Chrome for Testing directory exists but is incomplete".to_string(),
|
||||||
|
fix_suggestion: Some(
|
||||||
|
"Run: ./scripts/setup-chrome-for-testing.sh to reinstall".to_string()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "Chrome for Testing".to_string(),
|
||||||
|
status: DiagnosticStatus::Ok,
|
||||||
|
message: "Chrome for Testing not installed (using system Chrome)".to_string(),
|
||||||
|
fix_suggestion: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if ChromeDriver is executable (macOS quarantine issue)
|
||||||
|
fn check_chromedriver_executable() -> DiagnosticResult {
|
||||||
|
match Command::new("chromedriver").arg("--version").output() {
|
||||||
|
Ok(output) if output.status.success() => {
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "ChromeDriver Executable".to_string(),
|
||||||
|
status: DiagnosticStatus::Ok,
|
||||||
|
message: "ChromeDriver is executable".to_string(),
|
||||||
|
fix_suggestion: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "ChromeDriver Executable".to_string(),
|
||||||
|
status: DiagnosticStatus::Error,
|
||||||
|
message: "ChromeDriver found but failed to execute".to_string(),
|
||||||
|
fix_suggestion: Some(
|
||||||
|
"Remove macOS quarantine attribute:\n\
|
||||||
|
xattr -d com.apple.quarantine $(which chromedriver)".to_string()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
DiagnosticResult {
|
||||||
|
name: "ChromeDriver Executable".to_string(),
|
||||||
|
status: DiagnosticStatus::Error,
|
||||||
|
message: "ChromeDriver not executable or not in PATH".to_string(),
|
||||||
|
fix_suggestion: Some(
|
||||||
|
"Ensure ChromeDriver is in PATH and executable:\n\
|
||||||
|
chmod +x $(which chromedriver)".to_string()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
|
||||||
|
fn find_chromedriver_path() -> Option<PathBuf> {
|
||||||
|
Command::new("which")
|
||||||
|
.arg("chromedriver")
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.filter(|o| o.status.success())
|
||||||
|
.map(|o| PathBuf::from(String::from_utf8_lossy(&o.stdout).trim()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_chrome_path(config_binary: Option<&str>) -> Option<PathBuf> {
|
||||||
|
if let Some(binary) = config_binary {
|
||||||
|
let path = PathBuf::from(binary);
|
||||||
|
if path.exists() {
|
||||||
|
return Some(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for path in get_chrome_search_paths() {
|
||||||
|
if path.exists() {
|
||||||
|
return Some(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_chrome_search_paths() -> Vec<PathBuf> {
|
||||||
|
let mut paths = vec![
|
||||||
|
// macOS paths
|
||||||
|
PathBuf::from("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
|
||||||
|
PathBuf::from("/Applications/Chromium.app/Contents/MacOS/Chromium"),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Chrome for Testing paths
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
paths.push(home.join(".chrome-for-testing/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"));
|
||||||
|
paths.push(home.join(".chrome-for-testing/chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux paths
|
||||||
|
paths.extend([
|
||||||
|
PathBuf::from("/usr/bin/google-chrome"),
|
||||||
|
PathBuf::from("/usr/bin/google-chrome-stable"),
|
||||||
|
PathBuf::from("/usr/bin/chromium"),
|
||||||
|
PathBuf::from("/usr/bin/chromium-browser"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_chromedriver_version() -> Option<String> {
|
||||||
|
Command::new("chromedriver")
|
||||||
|
.arg("--version")
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.filter(|o| o.status.success())
|
||||||
|
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_chrome_version(config_binary: Option<&str>) -> Option<String> {
|
||||||
|
let chrome_path = find_chrome_path(config_binary)?;
|
||||||
|
|
||||||
|
Command::new(&chrome_path)
|
||||||
|
.arg("--version")
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.filter(|o| o.status.success())
|
||||||
|
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_major_version(version_str: &str) -> Option<u32> {
|
||||||
|
// Extract version number from strings like:
|
||||||
|
// "Google Chrome 120.0.6099.109"
|
||||||
|
// "ChromeDriver 120.0.6099.109"
|
||||||
|
version_str
|
||||||
|
.split_whitespace()
|
||||||
|
.find(|s| s.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false))
|
||||||
|
.and_then(|v| v.split('.').next())
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_major_version() {
|
||||||
|
assert_eq!(extract_major_version("Google Chrome 120.0.6099.109"), Some(120));
|
||||||
|
assert_eq!(extract_major_version("ChromeDriver 120.0.6099.109"), Some(120));
|
||||||
|
assert_eq!(extract_major_version("120.0.6099.109"), Some(120));
|
||||||
|
assert_eq!(extract_major_version("invalid"), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod safari;
|
pub mod safari;
|
||||||
pub mod chrome;
|
pub mod chrome;
|
||||||
|
pub mod diagnostics;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|||||||
Reference in New Issue
Block a user