From 9bef7753bf72b025ff027d7b82d6ee43efa4a64e Mon Sep 17 00:00:00 2001 From: "Dhanji R. Prasanna" Date: Sat, 10 Jan 2026 20:44:23 +1100 Subject: [PATCH] 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. --- Cargo.lock | 2 + crates/g3-cli/Cargo.toml | 1 + crates/g3-cli/src/lib.rs | 11 + crates/g3-computer-control/Cargo.toml | 1 + crates/g3-computer-control/src/lib.rs | 1 + .../src/webdriver/diagnostics.rs | 541 ++++++++++++++++++ .../g3-computer-control/src/webdriver/mod.rs | 1 + 7 files changed, 558 insertions(+) create mode 100644 crates/g3-computer-control/src/webdriver/diagnostics.rs diff --git a/Cargo.lock b/Cargo.lock index 0f1da9e..afdc6a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1353,6 +1353,7 @@ dependencies = [ "clap", "crossterm 0.29.0", "dirs 5.0.1", + "g3-computer-control", "g3-config", "g3-core", "g3-ensembles", @@ -1386,6 +1387,7 @@ dependencies = [ "cocoa 0.25.0", "core-foundation 0.10.1", "core-graphics 0.23.2", + "dirs 5.0.1", "fantoccini", "image", "objc", diff --git a/crates/g3-cli/Cargo.toml b/crates/g3-cli/Cargo.toml index 061d966..f6d6c3b 100644 --- a/crates/g3-cli/Cargo.toml +++ b/crates/g3-cli/Cargo.toml @@ -8,6 +8,7 @@ description = "CLI interface for G3 AI coding agent" g3-core = { path = "../g3-core" } g3-config = { path = "../g3-config" } g3-planner = { path = "../g3-planner" } +g3-computer-control = { path = "../g3-computer-control" } g3-providers = { path = "../g3-providers" } clap = { workspace = true } g3-ensembles = { path = "../g3-ensembles" } diff --git a/crates/g3-cli/src/lib.rs b/crates/g3-cli/src/lib.rs index 726edd1..ccbf808 100644 --- a/crates/g3-cli/src/lib.rs +++ b/crates/g3-cli/src/lib.rs @@ -548,6 +548,17 @@ pub async fn run() -> Result<()> { if cli.chrome_headless { config.webdriver.enabled = true; 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 diff --git a/crates/g3-computer-control/Cargo.toml b/crates/g3-computer-control/Cargo.toml index b9ed189..f2167d1 100644 --- a/crates/g3-computer-control/Cargo.toml +++ b/crates/g3-computer-control/Cargo.toml @@ -17,6 +17,7 @@ tracing = { workspace = true } uuid = { workspace = true } shellexpand = "3.1" +dirs = "5.0" # Async trait support async-trait = "0.1" diff --git a/crates/g3-computer-control/src/lib.rs b/crates/g3-computer-control/src/lib.rs index 7faae63..d37e295 100644 --- a/crates/g3-computer-control/src/lib.rs +++ b/crates/g3-computer-control/src/lib.rs @@ -10,6 +10,7 @@ pub mod webdriver; // Re-export webdriver types for convenience pub use webdriver::{ chrome::ChromeDriver, safari::SafariDriver, WebDriverController, WebElement, + diagnostics::{run_diagnostics as run_chrome_diagnostics, ChromeDiagnosticReport, DiagnosticStatus}, }; // Re-export macax types for convenience diff --git a/crates/g3-computer-control/src/webdriver/diagnostics.rs b/crates/g3-computer-control/src/webdriver/diagnostics.rs new file mode 100644 index 0000000..970c773 --- /dev/null +++ b/crates/g3-computer-control/src/webdriver/diagnostics.rs @@ -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, +} + +#[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, + pub chrome_version: Option, + pub chromedriver_version: Option, + pub chrome_path: Option, + pub chromedriver_path: Option, + pub config_chrome_binary: Option, +} + +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 { + 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 { + 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 { + 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 { + 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 { + 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 { + // 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); + } +} diff --git a/crates/g3-computer-control/src/webdriver/mod.rs b/crates/g3-computer-control/src/webdriver/mod.rs index 13c9afe..88e7282 100644 --- a/crates/g3-computer-control/src/webdriver/mod.rs +++ b/crates/g3-computer-control/src/webdriver/mod.rs @@ -1,5 +1,6 @@ pub mod safari; pub mod chrome; +pub mod diagnostics; use anyhow::Result; use async_trait::async_trait;