# Browser-Architektur: Technische Ausarbeitung
> Statushinweis:
> Dieses Dokument enthält neben dem Ist-Zustand auch Architekturideen und frühere Vorschläge.
> Nicht jede hier beschriebene Option ist aktuell in der CLI implementiert.
> Für den tatsächlichen Benutzerstand sind `auditmysite --help`, die [README](../README.md) und [ARCHITECTURE.md](ARCHITECTURE.md) maßgeblich.
## Status quo (Ist-Zustand)
- Browser-Auflösung über `resolver` mit `--browser-path`, `AUDITMYSITE_BROWSER`, System-Browsern und Managed Install
- Managed Browser unter `~/.auditmysite/browsers/`
- Kein stiller Auto-Download; Installation erfolgt explizit über `auditmysite browser install`
- `browser`-Subcommands und `doctor` sind implementiert
- `--chrome-path` ist nur noch Alias für `--browser-path`
- Einige weiter unten beschriebene Flags wie `--browser`, `--fast` und `--strict` sind derzeit Vorschläge, nicht produktive CLI-Optionen
---
## 1. Zielarchitektur
```
┌─────────────────────────────────────────────────────┐
│ CLI Layer │
│ audit <url> │ browser detect/install │ doctor │
└────────┬──────┴────────────┬─────────────┴──────────┘
│ │
┌────────▼───────────────────▼────────────────────────┐
│ Browser Resolution │
│ 1. --browser-path / AUDITMYSITE_BROWSER │
│ 2. Registry: Chrome → Edge → Ungoogled → Chromium │
│ 3. Managed install (~/.auditmysite/browsers/) │
│ 4. Error mit Installationshinweisen │
└────────┬────────────────────────────────────────────┘
│
┌────────▼────────────────────────────────────────────┐
│ Browser Launcher │
│ Modes: Standard │ Strict │ Fast (headless-shell) │
│ Flags, Timeouts, Port-Handling, Cleanup │
└────────┬────────────────────────────────────────────┘
│
┌────────▼────────────────────────────────────────────┐
│ chromiumoxide / CDP │
│ AXTree, Computed Styles, Web Vitals, Navigation │
└─────────────────────────────────────────────────────┘
```
**Kernprinzipien:**
1. **Kein stiller Download.** Wenn kein Browser gefunden wird → Fehler mit klarer Anleitung.
2. **System-Browser bevorzugen.** Der User hat Chrome oder Edge installiert? Nutzen.
3. **Expliziter Installer.** `auditmysite browser install` als bewusste Aktion.
4. **Homebrew bleibt leichtgewichtig.** Keine Browser-Dependency in der Formula.
**Verantwortlichkeiten:**
| `browser::registry` | Kennt alle unterstützten Browser-Typen und ihre Pfade pro Plattform |
| `browser::detection` | Findet installierte Browser, prüft Executable + Version |
| `browser::resolver` | Führt die priorisierte Auswahl durch (CLI → Env → Registry) |
| `browser::installer` | Lädt Chrome for Testing / headless-shell herunter |
| `browser::launcher` | Startet den Browser mit den richtigen Flags für den Mode |
| `cli::browser` | Subcommands: `detect`, `install`, `remove` |
| `cli::doctor` | Systemdiagnose: Browser, Netzwerk, Berechtigungen |
---
## 2. Browser-Strategie
### Unterstützte Browser
| Google Chrome | `Chrome` | Ja | 1 (Standard) |
| Microsoft Edge | `Edge` | Ja (gleiche Engine) | 2 |
| Ungoogled Chromium | `UngoogledChromium` | Ja | 3 |
| Chromium (System) | `Chromium` | Ja | 4 |
| Chrome for Testing (managed) | `ChromeForTesting` | Ja | 5 (explizit installiert) |
| chrome-headless-shell (managed) | `HeadlessShell` | Ja (eingeschränkt) | — (nur mit `--fast`) |
**Annahme:** Brave, Opera, Vivaldi sind technisch möglich (Chromium-basiert), aber der Support-Aufwand lohnt sich nicht. Wer Brave nutzt, kann `--browser-path` setzen.
### Erkennungsreihenfolge (Default-Mode)
```
1. --browser-path /explicit/path → Genau diesen Browser nutzen
2. AUDITMYSITE_BROWSER=/env/path → Env-Variable
3. CHROME_PATH=/legacy/path → Rückwärtskompatibilität
4. Google Chrome (System) → Bevorzugt, weil verbreitet und signiert
5. Microsoft Edge (System) → Auf macOS/Windows oft vorinstalliert
6. Ungoogled Chromium (System/Brew) → Privacy-bewusste Alternative
7. Chromium (System/Brew/Snap) → Generisches Chromium
8. Chrome for Testing (~/.auditmysite/) → Nur wenn per `browser install` installiert
9. → Fehler: ChromeNotFound → Mit konkreten Installationshinweisen
```
### Modi
| Modus | Flag | Verhalten |
|---|---|---|
| **Default** | (keiner) | System-Browser suchen → managed Browser → Fehler |
| **Strict** | `--strict` | Nur System-Browser, kein Fallback auf managed |
| **Fast** | `--fast` | Bevorzugt `chrome-headless-shell`, dann Standard-Kette |
**Strict-Mode:** Für CI/CD-Pipelines, wo der Browser explizit kontrolliert werden soll. Kein Auto-Fallback auf Downloads.
**Fast-Mode:** `chrome-headless-shell` ist kleiner (~50 MB vs ~300 MB) und startet schneller. Einschränkung: Kein vollständiger AXTree — prüfen ob das für den Use-Case reicht. **Annahme:** Falls der AXTree mit headless-shell nicht verfügbar ist, muss ein Fallback auf Standard-Chrome passieren, mit Warnung.
### CLI-Flags und Env-Variablen
| Flag/Env | Typ | Default | Beschreibung |
|---|---|---|---|
| `--browser-path <PATH>` | CLI | — | Expliziter Pfad zur Browser-Binary |
| `--browser <KIND>` | CLI | `auto` | Browser-Typ erzwingen: `chrome`, `edge`, `chromium`, `auto` |
| `--fast` | CLI | false | Bevorzugt headless-shell für Geschwindigkeit |
| `--strict` | CLI | false | Nur System-Browser, kein managed Fallback |
| `AUDITMYSITE_BROWSER` | Env | — | Pfad zur Browser-Binary |
| `CHROME_PATH` | Env | — | Legacy-Kompatibilität (wird zu Gunsten von `AUDITMYSITE_BROWSER` deprecated) |
---
## 3. CLI-Design
### Subcommand-Struktur
```
auditmysite <URL> # Single audit (Hauptbefehl bleibt)
auditmysite --sitemap <URL> # Batch via Sitemap
auditmysite --url-file <FILE> # Batch via Datei
auditmysite browser detect # Zeigt alle gefundenen Browser
auditmysite browser install # Installiert Chrome for Testing
auditmysite browser install --headless-shell # Installiert chrome-headless-shell
auditmysite browser remove # Entfernt managed Browser
auditmysite browser path # Gibt Pfad des aktiven Browsers aus
auditmysite doctor # Systemdiagnose
```
### Beispielaufrufe
```bash
# Standard: System-Chrome nutzen
auditmysite https://example.com --full --format pdf --output reports/audit.pdf
# Expliziter Browser
auditmysite https://example.com --browser-path /usr/bin/chromium-browser
# Edge erzwingen
auditmysite https://example.com --browser edge
# Fast-Mode (headless-shell, falls installiert)
auditmysite https://example.com --fast
# Strict-Mode (nur System-Browser, kein Fallback)
auditmysite https://example.com --strict
# Browser erkennen (Diagnostik)
auditmysite browser detect
# Ausgabe:
# ✓ Google Chrome 131.0.6778.108 /Applications/Google Chrome.app/.../Google Chrome
# ✓ Microsoft Edge 131.0.2903.86 /Applications/Microsoft Edge.app/.../Microsoft Edge
# ✗ Ungoogled Chromium not found
# ✗ Chromium not found
# ✗ Chrome for Testing not installed (~/.auditmysite/browsers/)
# Active: Google Chrome (auto-detected)
# Browser installieren
auditmysite browser install
# Ausgabe:
# Downloading Chrome for Testing v131.0.6778.108...
# Platform: macOS ARM64
# Destination: ~/.auditmysite/browsers/chrome-for-testing/
# Progress: [████████████████████] 100% (142 MB)
# ✓ Installed successfully
# Systemdiagnose
auditmysite doctor
# Ausgabe:
# Browser: ✓ Google Chrome 131.0.6778.108
# Permissions: ✓ Executable
# Network: ✓ Can reach example.com
# Firewall: ✓ Loopback connections allowed
# Disk: ✓ 2.1 GB free in ~/.auditmysite/
# Config: ✓ auditmysite.toml found
```
### Fehlerfall: Kein Browser gefunden
```
Error: No compatible browser found.
auditmysite requires a Chromium-based browser (Chrome, Edge, or Chromium).
Options:
1. Install Google Chrome:
macOS: brew install --cask google-chrome
Linux: sudo apt install google-chrome-stable
Windows: https://www.google.com/chrome/
2. Install a managed browser for auditmysite:
auditmysite browser install
3. Specify an existing browser:
auditmysite --browser-path /path/to/chrome <url>
AUDITMYSITE_BROWSER=/path/to/chrome auditmysite <url>
Run 'auditmysite doctor' for full system diagnostics.
```
---
## 4. Rust-Modulstruktur
```
src/browser/
├── mod.rs # Public exports
├── detection.rs # Findet Browser auf dem System (Pfade, which, Validierung)
├── registry.rs # Definiert BrowserKind, Pfade pro Plattform, Suchreihenfolge
├── resolver.rs # Orchestriert: CLI → Env → Registry → Managed → Error
├── installer.rs # Downloads: Chrome for Testing, headless-shell
├── launcher.rs # Startet Browser mit Mode-spezifischen Flags
├── manager.rs # Browser-Lifecycle (bestehend, nutzt resolver + launcher)
├── pool.rs # Concurrent page management (bestehend)
└── types.rs # Shared types (BrowserKind, DetectedBrowser, LaunchConfig, etc.)
src/cli/
├── mod.rs
├── args.rs # Hauptargs + neue Flags (--browser, --browser-path, --fast, --strict)
├── browser.rs # NEU: Subcommands detect/install/remove/path
├── doctor.rs # NEU: Systemdiagnose
└── config.rs # Config file support
src/
├── doctor.rs # NEU: Doctor-Logik (Browser, Netzwerk, Berechtigungen)
└── ...
```
**Verantwortung pro Modul:**
| `types.rs` | Alle Enums und Structs: `BrowserKind`, `DetectedBrowser`, `LaunchConfig`, `BrowserMode` |
| `registry.rs` | Statische Daten: Welche Browser existieren, welche Pfade pro OS, Suchreihenfolge |
| `detection.rs` | Laufzeit-Erkennung: Pfade prüfen, `which` aufrufen, Version auslesen, Capabilities checken |
| `resolver.rs` | Entscheidungslogik: Nimmt CLI-Optionen, Env-Vars und Detection-Ergebnisse, gibt einen `ResolvedBrowser` zurück |
| `installer.rs` | Download + Extraktion von Chrome for Testing / headless-shell nach `~/.auditmysite/browsers/` |
| `launcher.rs` | Baut `BrowserConfig` auf, wählt Flags je nach Mode, startet via chromiumoxide |
| `manager.rs` | High-Level-API: `BrowserManager::new()` nutzt resolver → launcher, verwaltet Lifecycle |
| `pool.rs` | Concurrent page management (unverändert) |
---
## 5. Zustandsmodell / Datenstrukturen
```rust
// ── types.rs ──────────────────────────────────────────────────
/// Welcher Browser-Typ
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BrowserKind {
Chrome,
Edge,
UngoogledChromium,
Chromium,
ChromeForTesting, // Managed install
HeadlessShell, // Managed install, fast-mode only
Custom, // --browser-path
}
impl BrowserKind {
/// Menschenlesbarer Name
pub fn display_name(&self) -> &'static str {
match self {
Self::Chrome => "Google Chrome",
Self::Edge => "Microsoft Edge",
Self::UngoogledChromium => "Ungoogled Chromium",
Self::Chromium => "Chromium",
Self::ChromeForTesting => "Chrome for Testing",
Self::HeadlessShell => "Chrome Headless Shell",
Self::Custom => "Custom Browser",
}
}
/// Ob das ein managed (selbst installierter) Browser ist
pub fn is_managed(&self) -> bool {
matches!(self, Self::ChromeForTesting | Self::HeadlessShell)
}
}
/// Wie der Browser gefunden wurde
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrowserSource {
/// Explizit per --browser-path
CliFlag,
/// Per AUDITMYSITE_BROWSER oder CHROME_PATH
EnvVar,
/// In bekannten System-Pfaden gefunden
SystemPath,
/// Per `which`/`where` im PATH gefunden
PathSearch,
/// Selbst installiert unter ~/.auditmysite/browsers/
ManagedInstall,
}
/// Laufmodus für den Browser
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrowserMode {
/// Normaler Headless-Chrome mit allen Features
Standard,
/// Nur System-Browser, kein Fallback
Strict,
/// Bevorzugt headless-shell für Geschwindigkeit
Fast,
}
/// Ein gefundener Browser
#[derive(Debug, Clone)]
pub struct DetectedBrowser {
pub kind: BrowserKind,
pub path: PathBuf,
pub version: Option<String>,
pub source: BrowserSource,
}
/// Ergebnis der Browser-Resolution
#[derive(Debug, Clone)]
pub struct ResolvedBrowser {
/// Der ausgewählte Browser
pub browser: DetectedBrowser,
/// Welcher Mode aktiv ist
pub mode: BrowserMode,
/// Alle gefundenen Kandidaten (für `browser detect`)
pub all_candidates: Vec<DetectedBrowser>,
}
/// Konfiguration für den Browser-Start
#[derive(Debug, Clone)]
pub struct LaunchConfig {
pub browser: DetectedBrowser,
pub mode: BrowserMode,
pub headless: bool,
pub no_sandbox: bool,
pub disable_gpu: bool,
pub disable_images: bool,
pub window_size: (u32, u32),
pub timeout_secs: u64,
pub extra_args: Vec<String>,
}
/// Was installiert werden soll
#[derive(Debug, Clone, Copy)]
pub enum InstallTarget {
/// Chrome for Testing (vollständig)
ChromeForTesting,
/// chrome-headless-shell (minimal, schnell)
HeadlessShell,
}
```
---
## 6. Erkennungslogik
### Schritt für Schritt
```
resolve_browser(cli_args) → Result<ResolvedBrowser>
│
├─ 1. CLI-Flag: --browser-path gesetzt?
│ → Pfad validieren, Version prüfen, BrowserKind::Custom
│ → Fehler wenn Pfad nicht existiert oder nicht executable
│
├─ 2. Env-Var: AUDITMYSITE_BROWSER gesetzt?
│ → Pfad validieren, Version prüfen
│ → Fehler nur loggen wenn Pfad ungültig, weiter mit nächstem Schritt
│
├─ 3. Env-Var: CHROME_PATH gesetzt? (Legacy)
│ → Wie 2., aber mit Deprecation-Warnung
│
├─ 4. --browser Flag gesetzt? (z.B. --browser edge)
│ → Nur den spezifischen BrowserKind suchen
│ → Fehler wenn dieser Typ nicht gefunden
│
├─ 5. System-Scan (Registry-Reihenfolge):
│ │
│ ├─ Chrome: Bekannte Pfade → which google-chrome
│ ├─ Edge: Bekannte Pfade → which microsoft-edge
│ ├─ Ungoogled Chromium: Brew-Pfade → which
│ └─ Chromium: Bekannte Pfade → Brew → Snap → which
│ │
│ → Ersten Treffer nehmen (wenn nicht --strict)
│
├─ 6. Managed Install prüfen (wenn nicht --strict):
│ → ~/.auditmysite/browsers/chrome-for-testing/ existiert?
│ → ~/.auditmysite/browsers/headless-shell/ existiert? (nur bei --fast)
│
└─ 7. Nichts gefunden → ChromeNotFound Error
```
### Plattform-spezifische Pfade
#### macOS
```rust
fn macos_paths() -> Vec<(BrowserKind, &'static str)> {
vec![
// Chrome
(BrowserKind::Chrome,
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
// Edge
(BrowserKind::Edge,
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"),
// Ungoogled Chromium (Homebrew Cask: eloston-chromium)
(BrowserKind::UngoogledChromium,
"/Applications/Chromium.app/Contents/MacOS/Chromium"), // Achtung: kollidiert mit normalem Chromium
// Chromium (Homebrew)
(BrowserKind::Chromium,
"/opt/homebrew/bin/chromium"),
(BrowserKind::Chromium,
"/usr/local/bin/chromium"),
// Chrome Canary (niedrige Prio, instabil)
(BrowserKind::Chrome,
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"),
]
}
```
**Problem: Ungoogled Chromium vs. Chromium auf macOS**
Beide installieren sich als `/Applications/Chromium.app`. Unterscheidung möglich über:
- `Chromium --version` → "Chromium 131.0.6778.108 Ungoogled" vs. "Chromium 131.0.6778.108"
- `brew list --cask` prüfen: `eloston-chromium` vs. `chromium`
- In der Praxis: Beide funktionieren identisch mit CDP, die Unterscheidung ist kosmetisch.
**Empfehlung:** Beide als `BrowserKind::Chromium` behandeln, es sei denn der User hat `--browser ungoogled` explizit gesetzt. Auf macOS reicht die `/Applications/Chromium.app`-Erkennung.
#### Linux
```rust
fn linux_paths() -> Vec<(BrowserKind, &'static str)> {
vec![
// Chrome
(BrowserKind::Chrome, "/usr/bin/google-chrome"),
(BrowserKind::Chrome, "/usr/bin/google-chrome-stable"),
// Edge
(BrowserKind::Edge, "/usr/bin/microsoft-edge"),
(BrowserKind::Edge, "/usr/bin/microsoft-edge-stable"),
// Chromium
(BrowserKind::Chromium, "/usr/bin/chromium"),
(BrowserKind::Chromium, "/usr/bin/chromium-browser"),
(BrowserKind::Chromium, "/snap/bin/chromium"),
(BrowserKind::Chromium, "/var/lib/flatpak/exports/bin/org.chromium.Chromium"),
]
}
```
#### Windows
```rust
fn windows_paths() -> Vec<(BrowserKind, &'static str)> {
vec![
// Chrome
(BrowserKind::Chrome,
r"C:\Program Files\Google\Chrome\Application\chrome.exe"),
(BrowserKind::Chrome,
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"),
// Edge (immer vorhanden auf Windows 10+)
(BrowserKind::Edge,
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"),
(BrowserKind::Edge,
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe"),
]
}
```
### Validierung einer gefundenen Binary
```rust
fn validate_browser(path: &Path) -> Result<DetectedBrowser> {
// 1. Existiert die Datei?
if !path.exists() {
return Err(NotFound);
}
// 2. Ist sie executable? (Unix)
#[cfg(unix)]
check_executable_permission(path)?;
// 3. Version auslesen
let version = run_version_check(path)?; // --version
// 4. Minimale Versionsprüfung
// chromiumoxide 0.8 braucht Chrome >= 64 (CDP-Protokoll)
// Praktisch: Chrome >= 90 empfohlen für stabile AXTree-API
if let Some(ref v) = version {
let major = parse_major_version(v);
if major < 90 {
warn!("Chrome {} is very old, recommend >= 90", v);
}
}
// 5. Optional: CDP-Capability-Check
// Schnelltest: `chrome --headless --dump-dom about:blank`
// Wenn das funktioniert, ist CDP verfügbar.
// → Nur bei `doctor`, nicht bei jedem Start.
Ok(DetectedBrowser { ... })
}
```
### which-Suchbegriffe pro BrowserKind
```rust
fn which_names(kind: BrowserKind) -> &'static [&'static str] {
match kind {
BrowserKind::Chrome => &["google-chrome", "google-chrome-stable", "chrome"],
BrowserKind::Edge => &["microsoft-edge", "microsoft-edge-stable"],
BrowserKind::Chromium => &["chromium", "chromium-browser"],
_ => &[],
}
}
```
---
## 7. Installationsstrategie
### `auditmysite browser install`
```
~/.auditmysite/
├── browsers/
│ ├── chrome-for-testing/ # Voller Chrome
│ │ ├── version.txt # "131.0.6778.108"
│ │ └── chrome-mac-arm64/ # Plattform-spezifisch
│ │ └── Google Chrome for Testing.app/...
│ └── headless-shell/ # Minimal (nur mit --headless-shell)
│ ├── version.txt
│ └── chrome-headless-shell
└── chromium/ # LEGACY (Phase 5 löschen)
```
### Download-Quellen
Chrome for Testing hat eine stabile JSON-API:
```
https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json
```
**Empfohlenes Vorgehen:**
1. Latest Stable Version von der API holen (nicht hardcoden)
2. Download-URL für Plattform + Architektur zusammenbauen
3. Herunterladen, extrahieren, Version in `version.txt` speichern
```rust
/// Installer-Logik
impl BrowserInstaller {
/// Chrome for Testing installieren
pub async fn install(target: InstallTarget) -> Result<InstalledBrowser> {
// 1. Aktuelle stabile Version ermitteln
let version = Self::fetch_latest_stable_version().await?;
// 2. Prüfen ob schon installiert
let install_dir = Self::install_dir(target);
if let Some(existing) = Self::check_existing(&install_dir, &version) {
info!("Already installed: {} v{}", target.name(), version);
return Ok(existing);
}
// 3. Download-URL zusammenbauen
let url = Self::download_url(target, &version)?;
// 4. Herunterladen mit Progress
info!("Downloading {} v{}...", target.name(), version);
let archive = Self::download_with_progress(&url).await?;
// 5. Extrahieren
Self::extract(&archive, &install_dir)?;
// 6. Version-Datei schreiben
fs::write(install_dir.join("version.txt"), &version)?;
// 7. Binary-Pfad zurückgeben
let binary_path = Self::binary_path(target, &install_dir);
Ok(InstalledBrowser { path: binary_path, version })
}
/// Stabile Version von API holen
async fn fetch_latest_stable_version() -> Result<String> {
// Primär: Chrome for Testing API
let url = "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json";
let resp: serde_json::Value = reqwest::get(url).await?.json().await?;
let version = resp["channels"]["Stable"]["version"]
.as_str()
.ok_or_else(|| anyhow!("Could not parse stable version"))?;
Ok(version.to_string())
}
}
```
### Version-Pinning
**Empfehlung:** Kein Pinning auf eine exakte Version. Stattdessen:
- `browser install` holt immer die aktuelle Stable
- Die installierte Version wird in `version.txt` gespeichert
- `browser install --update` aktualisiert auf neueste Stable
- Wer eine bestimmte Version braucht: `browser install --version 131.0.6778.108`
- **Kompatibilitätsgarantie:** Chrome for Testing Stable ist abwärtskompatibel mit CDP
### Integritätsprüfung
Chrome for Testing bietet keine offiziellen Checksums. Pragmatischer Ansatz:
1. Download über HTTPS (TLS-Integrität)
2. Nach Extraktion: Binary-Existenz prüfen
3. `--version` aufrufen und parsen
4. Wenn alles klappt → gültig
### Fehlerfälle
| Kein Internet | Klare Meldung, Hinweis auf `--browser-path` |
| Download-Fehler | 2 Retries mit Backoff, dann Fehler |
| Extraktion fehlgeschlagen | Cache-Dir löschen, Fehler melden |
| Kein Platz | Plattengröße prüfen, warnen bei < 500 MB |
| Binary nicht lauffähig | `--version`-Check schlägt fehl → Fehler + Hinweis |
---
## 8. Launch-Strategie
### Chrome-Flags nach Mode
```rust
fn build_launch_args(config: &LaunchConfig) -> Vec<String> {
let mut args = vec![
// Immer
"--headless=new".into(), // Neuer Headless-Mode (Chrome >= 112)
"--no-first-run".into(),
"--no-default-browser-check".into(),
"--disable-extensions".into(),
"--disable-background-networking".into(),
"--disable-sync".into(),
"--disable-translate".into(),
"--disable-features=TranslateUI".into(),
"--metrics-recording-only".into(),
"--mute-audio".into(),
"--disable-infobars".into(),
"--disable-popup-blocking".into(),
"--disable-background-timer-throttling".into(),
"--disable-backgrounding-occluded-windows".into(),
"--disable-renderer-backgrounding".into(),
format!("--window-size={},{}", config.window_size.0, config.window_size.1),
];
// macOS: Firewall-Problem vermeiden
if cfg!(target_os = "macos") {
// Loopback-Verbindung statt Netzwerk
args.push("--remote-debugging-address=127.0.0.1".into());
// Kein mDNS/Bonjour → keine Firewall-Nachfrage
args.push("--disable-features=NetworkService".into());
}
// Mode-spezifisch
match config.mode {
BrowserMode::Standard => {
args.push("--disable-gpu".into());
}
BrowserMode::Strict => {
args.push("--disable-gpu".into());
// Keine Extra-Features
}
BrowserMode::Fast => {
args.push("--disable-gpu".into());
args.push("--blink-settings=imagesEnabled=false".into());
args.push("--disable-javascript".into()); // Nur wenn JS nicht gebraucht
// ACHTUNG: JS wird für Web Vitals gebraucht!
// → JS-disable nur wenn --no-vitals gesetzt
}
}
// Sandbox
if config.no_sandbox {
args.push("--no-sandbox".into());
args.push("--disable-setuid-sandbox".into());
args.push("--disable-dev-shm-usage".into());
}
args
}
```
**Anmerkung zu `--headless=new` vs `--headless`:**
- `--headless` (alt): Chrome < 112, veralteter Headless-Mode
- `--headless=new`: Chrome >= 112, nutzt den echten Browser-Renderer
- **Empfehlung:** `--headless=new` für Chrome >= 112, Fallback auf `--headless` für ältere Versionen. Versionsprüfung in Detection.
### Port-Handling
```rust
/// Freien Port finden für CDP
fn find_free_port() -> u16 {
// Binde kurz an Port 0, OS vergibt freien Port
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
listener.local_addr().unwrap().port()
}
```
chromiumoxide macht das intern, aber wenn wir den Port selbst kontrollieren wollen (z.B. für `--remote-debugging-port`), brauchen wir das.
### Prozess-Lifecycle und Zombie-Vermeidung
```rust
impl BrowserManager {
pub async fn close(self) -> Result<()> {
// 1. Alle Pages schließen
if let Ok(pages) = self.browser.pages().await {
for page in pages {
page.close().await.ok();
}
}
// 2. Browser graceful beenden
// chromiumoxide sendet Browser.close über CDP
drop(self.browser);
// 3. Timeout: Wenn Browser nach 5s noch läuft, SIGTERM
// chromiumoxide handled das über Drop, aber wir
// sollten sicherstellen, dass kein Zombie bleibt
// 4. Handler-Task abbrechen
if let Some(handle) = self.handler.lock().await.take() {
handle.abort();
}
Ok(())
}
}
// Zusätzlich: Ctrl+C Handler im main.rs
// Stellt sicher, dass Browser auch bei SIGINT aufgeräumt wird
```
### Logging
```rust
// Beim Start
info!("Browser: {} v{} ({})",
resolved.browser.kind.display_name(),
resolved.browser.version.as_deref().unwrap_or("unknown"),
resolved.browser.source);
info!("Mode: {:?}", resolved.mode);
debug!("Launch args: {:?}", launch_args);
debug!("CDP port: {}", port);
// Bei Problemen
warn!("Browser process exited unexpectedly");
error!("CDP connection lost: {}", err);
```
---
## 9. Homebrew-Strategie
### Formula-Struktur
```ruby
class Auditmysite < Formula
desc "WCAG 2.1 Accessibility Checker with full audit reporting"
homepage "https://github.com/casoon/auditmysite"
url "https://github.com/casoon/auditmysite/archive/refs/tags/v0.4.5.tar.gz"
sha256 "..."
license "LGPL-3.0-or-later"
depends_on "rust" => :build
def install
system "cargo", "install", *std_cargo_args
end
def caveats
<<~EOS
auditmysite requires a Chromium-based browser (Chrome, Edge, or Chromium).
If you don't have one installed:
brew install --cask google-chrome
Or install a managed browser:
auditmysite browser install
Run 'auditmysite doctor' to verify your setup.
EOS
end
test do
# Prüfe nur dass die Binary läuft, nicht den Browser
assert_match "auditmysite", shell_output("#{bin}/auditmysite --version")
end
end
```
### Warum Browser NICHT als harte Dependency
1. **Viele User haben Chrome schon.** `depends_on "google-chrome"` würde es nochmal installieren oder Konflikte erzeugen.
2. **Cask-Dependencies sind fragil.** `depends_on cask: "google-chrome"` funktioniert in Homebrew nicht zuverlässig.
3. **Edge ist auf macOS oft vorinstalliert.** Warum Chrome erzwingen, wenn Edge schon da ist?
4. **CI/CD-Systeme** haben oft eigene Chrome-Installationen.
5. **Der User soll die Kontrolle haben**, welchen Browser er nutzt.
### Was bei `brew install auditmysite` passieren soll
1. Rust-Compiler baut die Binary
2. Binary wird nach `/opt/homebrew/bin/auditmysite` installiert
3. `caveats` zeigt den Hinweis auf Browser-Requirement
4. **Kein Browser-Download, kein Auto-Install, kein Netzwerkzugriff zur Laufzeit**
### Was NICHT passieren soll
- Kein automatischer Chrome-Download beim ersten `brew install`
- Keine Homebrew-Dependency auf einen Browser-Cask
- Kein Post-Install-Script das Netzwerkzugriffe macht
- Kein Abbruch der Installation wenn kein Browser vorhanden ist
### Fehlermeldung nach Installation (erster Run ohne Browser)
Wenn der User `auditmysite https://example.com` ausführt und kein Browser da ist:
```
Error: No compatible browser found.
You installed auditmysite via Homebrew — great!
Now you need a Chromium-based browser. Options:
brew install --cask google-chrome # Recommended
auditmysite browser install # Self-contained download
Run 'auditmysite doctor' to check your setup.
```
---
## 10. macOS-spezifische Probleme
### Problem 1: Firewall-Popups
**Ursache:** Chrome öffnet einen Listening-Socket für CDP. macOS Application Firewall fragt "Möchten Sie eingehende Verbindungen erlauben?"
**Lösung:**
```
--remote-debugging-address=127.0.0.1
```
Bindet nur an Loopback. Die Firewall fragt nur bei Nicht-Loopback-Verbindungen. chromiumoxide nutzt das standardmäßig, aber wir setzen es explizit.
**Zusätzlich:**
```
--disable-features=MediaRouter
```
Verhindert mDNS-Discovery (Chromecast etc.), das ebenfalls Firewall-Popups auslösen kann.
### Problem 2: Gatekeeper-Warnungen
**Ursache:** Selbst heruntergeladene Binaries haben kein Code-Signing und bekommen das `com.apple.quarantine`-Extended-Attribute.
**Lösung bei `browser install`:**
```rust
#[cfg(target_os = "macos")]
fn remove_quarantine(path: &Path) -> Result<()> {
// Quarantine-Attribut entfernen
Command::new("xattr")
.args(["-dr", "com.apple.quarantine"])
.arg(path)
.output()
.ok();
Ok(())
}
```
Nach der Extraktion das Quarantine-Attribut auf dem gesamten App-Bundle entfernen. Das ist sicher, weil wir die Datei selbst von einer bekannten Google-URL heruntergeladen haben.
### Problem 3: Unsignierte Browser-Binaries
**Chrome for Testing** ist von Google signiert (hat ein gültiges Code-Signing-Zertifikat). Das ist kein Problem.
**Selbst kompiliertes Chromium** (z.B. via `brew install chromium`) ist NICHT signiert und kann Gatekeeper-Probleme verursachen.
**Empfehlung:**
- System-Chrome und System-Edge sind immer signiert → bevorzugen
- Chrome for Testing ist signiert → sicher als Managed Install
- Unsigniertes Chromium → warnen, aber erlauben
### Problem 4: Unterschiede zwischen installiertem Chrome und Download
| Code-Signing | Ja (Apple Notarized) | Ja (Google signiert) |
| Firewall | Bereits erlaubt | Neue Regel nötig |
| Updates | Automatisch | Manuell (`browser install --update`) |
| Pfad | `/Applications/Google Chrome.app/...` | `~/.auditmysite/browsers/...` |
| Gatekeeper | Kein Problem | Quarantine entfernen nötig |
| User-Profil | Eigenes Profil | Kein Profil (headless) |
**Empfehlung:** System-Chrome ist **immer** die bessere Wahl auf macOS. Der Managed Install ist ein Fallback für Systeme ohne Chrome.
### Problem 5: SingletonLock
**Ursache:** Chrome erstellt ein `SingletonLock` im Profil-Verzeichnis. Wenn der Prozess unsauber beendet wird, bleibt das Lock liegen und verhindert den nächsten Start.
**Aktuelle Lösung:** User muss manuell `rm` aufrufen.
**Bessere Lösung:**
```rust
fn cleanup_stale_locks(profile_dir: &Path) {
let lock_file = profile_dir.join("SingletonLock");
if lock_file.exists() {
// Prüfe ob der Prozess noch läuft
// SingletonLock enthält hostname-PID
if let Ok(content) = fs::read_to_string(&lock_file) {
// Format: "hostname-PID"
if let Some(pid_str) = content.split('-').last() {
if let Ok(pid) = pid_str.parse::<u32>() {
// Prüfe ob PID noch lebt
if !process_exists(pid) {
warn!("Removing stale SingletonLock (PID {} no longer running)", pid);
fs::remove_file(&lock_file).ok();
}
}
}
}
}
}
```
Noch besser: **Eigenes temporäres Profil-Verzeichnis** pro Audit-Run:
```rust
let temp_profile = tempfile::tempdir()?;
args.push(format!("--user-data-dir={}", temp_profile.path().display()));
```
Das vermeidet das SingletonLock-Problem komplett und verhindert Konflikte mit laufenden Chrome-Instanzen des Users.
**Annahme:** chromiumoxide setzt bereits ein temporäres Profil-Verzeichnis. Prüfen ob das der Fall ist — wenn ja, ist das SingletonLock-Problem ein Bug in chromiumoxide. Die aktuelle Fehlermeldung deutet darauf hin, dass das feste Verzeichnis `/tmp/chromiumoxide-runner/` verwendet wird, was bei mehreren parallelen Runs kollidiert.
---
## 11. Migrationsplan
### Phase 1: Detection Refactor
**Ziel:** Neue Browser-Erkennung mit Registry-Pattern, Edge-Support, bessere Fehlermeldungen.
**Änderungen:**
- `browser/types.rs` anlegen (BrowserKind, DetectedBrowser, BrowserSource, etc.)
- `browser/registry.rs` anlegen (Pfade pro Plattform und BrowserKind)
- `browser/detection.rs` umschreiben (nutzt Registry, findet Edge + Ungoogled Chromium)
- `browser/resolver.rs` anlegen (priorisierte Auswahl)
- `cli/args.rs`: `--browser-path` als Alias für `--chrome-path` (beide akzeptieren)
- `cli/args.rs`: `--browser <kind>` Flag
- Env-Var `AUDITMYSITE_BROWSER` einführen, `CHROME_PATH` weiter akzeptieren
- Fehlermeldung bei ChromeNotFound verbessern
**Risiken:**
- Detection könnte auf manchen Systemen andere Browser finden als vorher → `--browser chrome` als Escape-Hatch
- Edge-Kompatibilität mit chromiumoxide: Sollte funktionieren (gleiche CDP-API), aber testen
**Tests:**
- Unit-Tests für Registry (Pfade pro Plattform)
- Unit-Tests für Resolver (Prioritätsreihenfolge)
- Integrationstest: `auditmysite browser detect` auf CI
- Manuell: Auf macOS mit Chrome + Edge testen
### Phase 2: Explicit Installer
**Ziel:** `auditmysite browser install` als expliziter Befehl. Kein stiller Download mehr.
**Änderungen:**
- `cli/browser.rs` anlegen (Subcommands: detect, install, remove, path)
- `browser/installer.rs` umschreiben:
- Version von API statt hardcoded
- Zielverzeichnis: `~/.auditmysite/browsers/chrome-for-testing/`
- Progress-Bar mit `indicatif`
- Quarantine-Handling auf macOS
- `browser/manager.rs`: Auto-Download-Fallback entfernen
- `browser/manager.rs`: Wenn kein Browser → `ChromeNotFound` Error (kein Download)
**Risiken:**
- Breaking Change: User die sich auf Auto-Download verlassen, bekommen jetzt einen Fehler
- Mitigation: Klare Fehlermeldung mit `auditmysite browser install` Hinweis
**Tests:**
- `browser install` in CI testen (Download + Extraktion + Lauffähigkeit)
- `browser remove` testen
- Test: Audit ohne Browser → Fehler mit korrekter Meldung
- Test: Audit nach `browser install` → funktioniert
### Phase 3: CLI Doctor
**Ziel:** `auditmysite doctor` für Systemdiagnose.
**Änderungen:**
- `cli/doctor.rs` anlegen
- `doctor.rs` im Root anlegen (Logik)
- Checks: Browser vorhanden, executable, Version, CDP-fähig, Netzwerk, Plattenplatz
**Risiken:** Gering. Rein additives Feature.
**Tests:**
- Doctor auf CI: Alle Checks grün mit installiertem Chrome
- Doctor ohne Browser: Browser-Check rot, Rest grün
### Phase 4: Headless-Shell Mode
**Ziel:** `--fast` Mode mit `chrome-headless-shell`.
**Änderungen:**
- `browser/installer.rs`: `InstallTarget::HeadlessShell` Support
- `browser/launcher.rs`: Spezielle Flags für headless-shell
- `cli/args.rs`: `--fast` Flag
- Prüfen: Ist AXTree über headless-shell verfügbar?
- Wenn ja: Fast-Mode ist vollwertig
- Wenn nein: Fast-Mode nur für Performance/SEO/Security, kein WCAG-Audit
**Risiken:**
- AXTree möglicherweise nicht verfügbar → Feature eingeschränkt nutzbar
- Muss empirisch getestet werden
**Tests:**
- `browser install --headless-shell` testen
- Audit mit `--fast` gegen bekannte Seite
- Vergleich: Standard vs. Fast Ergebnisse
### Phase 5: Cleanup Legacy
**Ziel:** Alten Auto-Download-Code und `~/.auditmysite/chromium/` aufräumen.
**Änderungen:**
- `--chrome-path` als Deprecated markieren (→ `--browser-path`)
- `CHROME_PATH` als Deprecated markieren (→ `AUDITMYSITE_BROWSER`)
- `~/.auditmysite/chromium/` Migration: Beim Start prüfen und Hinweis geben
- Alten Installer-Code entfernen
- `DetectionMethod::AutoDownload` entfernen
**Risiken:**
- User mit altem Setup müssen `browser install` ausführen oder System-Browser nutzen
- Mitigation: Klare Migrationsmeldung
**Tests:**
- Test: Alter `chromium/`-Ordner existiert → Migrationsmeldung
- Test: `--chrome-path` funktioniert noch (Deprecated-Warnung)
---
## 12. Konkrete Code-Skizzen
### Browser Registry
```rust
// browser/registry.rs
use super::types::BrowserKind;
/// Ein Eintrag im Browser-Registry
pub struct RegistryEntry {
pub kind: BrowserKind,
pub path: &'static str,
}
/// Bekannte Browser-Pfade für die aktuelle Plattform, in Prioritätsreihenfolge
pub fn system_browser_paths() -> Vec<RegistryEntry> {
let mut entries = Vec::new();
#[cfg(target_os = "macos")]
{
entries.extend([
RegistryEntry {
kind: BrowserKind::Chrome,
path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
},
RegistryEntry {
kind: BrowserKind::Edge,
path: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
},
RegistryEntry {
kind: BrowserKind::Chromium,
path: "/Applications/Chromium.app/Contents/MacOS/Chromium",
},
RegistryEntry {
kind: BrowserKind::Chromium,
path: "/opt/homebrew/bin/chromium",
},
RegistryEntry {
kind: BrowserKind::Chromium,
path: "/usr/local/bin/chromium",
},
]);
}
#[cfg(target_os = "linux")]
{
entries.extend([
RegistryEntry {
kind: BrowserKind::Chrome,
path: "/usr/bin/google-chrome",
},
RegistryEntry {
kind: BrowserKind::Chrome,
path: "/usr/bin/google-chrome-stable",
},
RegistryEntry {
kind: BrowserKind::Edge,
path: "/usr/bin/microsoft-edge",
},
RegistryEntry {
kind: BrowserKind::Edge,
path: "/usr/bin/microsoft-edge-stable",
},
RegistryEntry {
kind: BrowserKind::Chromium,
path: "/usr/bin/chromium",
},
RegistryEntry {
kind: BrowserKind::Chromium,
path: "/usr/bin/chromium-browser",
},
RegistryEntry {
kind: BrowserKind::Chromium,
path: "/snap/bin/chromium",
},
]);
}
#[cfg(target_os = "windows")]
{
entries.extend([
RegistryEntry {
kind: BrowserKind::Chrome,
path: r"C:\Program Files\Google\Chrome\Application\chrome.exe",
},
RegistryEntry {
kind: BrowserKind::Chrome,
path: r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
},
RegistryEntry {
kind: BrowserKind::Edge,
path: r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
},
RegistryEntry {
kind: BrowserKind::Edge,
path: r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
},
]);
}
entries
}
/// Suchbegriffe für `which`/`where` pro BrowserKind
pub fn which_names(kind: BrowserKind) -> &'static [&'static str] {
match kind {
BrowserKind::Chrome => &["google-chrome", "google-chrome-stable"],
BrowserKind::Edge => &["microsoft-edge", "microsoft-edge-stable"],
BrowserKind::Chromium => &["chromium", "chromium-browser"],
_ => &[],
}
}
/// Prioritätsreihenfolge der BrowserKinds für die automatische Suche
pub fn search_order() -> &'static [BrowserKind] {
&[
BrowserKind::Chrome,
BrowserKind::Edge,
BrowserKind::Chromium,
]
}
```
### Browser Resolver
```rust
// browser/resolver.rs
use std::path::PathBuf;
use tracing::{info, warn, debug};
use super::detection::{detect_all_browsers, validate_browser};
use super::registry;
use super::types::*;
/// CLI-Optionen die den Browser betreffen
pub struct BrowserCliOptions {
/// --browser-path /explicit/path
pub browser_path: Option<String>,
/// --browser chrome|edge|chromium|auto
pub browser_kind: Option<String>,
/// --fast
pub fast: bool,
/// --strict
pub strict: bool,
}
/// Findet den besten verfügbaren Browser
pub fn resolve_browser(opts: &BrowserCliOptions) -> Result<ResolvedBrowser, BrowserResolveError> {
let mode = if opts.strict {
BrowserMode::Strict
} else if opts.fast {
BrowserMode::Fast
} else {
BrowserMode::Standard
};
let mut all_candidates = Vec::new();
// 1. Expliziter Pfad (höchste Prio)
if let Some(ref path_str) = opts.browser_path {
let path = PathBuf::from(path_str);
let browser = validate_browser(&path, BrowserKind::Custom, BrowserSource::CliFlag)?;
info!("Using specified browser: {} v{}",
browser.kind.display_name(),
browser.version.as_deref().unwrap_or("unknown"));
return Ok(ResolvedBrowser { browser, mode, all_candidates: vec![] });
}
// 2. Environment-Variable
if let Ok(path_str) = std::env::var("AUDITMYSITE_BROWSER") {
let path = PathBuf::from(&path_str);
if path.exists() {
let browser = validate_browser(&path, BrowserKind::Custom, BrowserSource::EnvVar)?;
info!("Using browser from AUDITMYSITE_BROWSER: {}", path.display());
return Ok(ResolvedBrowser { browser, mode, all_candidates: vec![] });
}
warn!("AUDITMYSITE_BROWSER points to non-existent path: {}", path_str);
}
// 2b. Legacy: CHROME_PATH
if let Ok(path_str) = std::env::var("CHROME_PATH") {
let path = PathBuf::from(&path_str);
if path.exists() {
warn!("CHROME_PATH is deprecated, use AUDITMYSITE_BROWSER instead");
let browser = validate_browser(&path, BrowserKind::Chrome, BrowserSource::EnvVar)?;
return Ok(ResolvedBrowser { browser, mode, all_candidates: vec![] });
}
}
// 3. Spezifischer BrowserKind angefordert?
let filter_kind: Option<BrowserKind> = opts.browser_kind.as_deref().and_then(|s| match s {
"chrome" => Some(BrowserKind::Chrome),
"edge" => Some(BrowserKind::Edge),
"chromium" => Some(BrowserKind::Chromium),
"auto" | "" => None,
_ => {
warn!("Unknown browser kind '{}', using auto-detection", s);
None
}
});
// 4. System-Scan
all_candidates = detect_all_browsers();
debug!("Found {} browser candidates", all_candidates.len());
// Filter nach gewünschtem Kind
let candidates: Vec<&DetectedBrowser> = if let Some(kind) = filter_kind {
all_candidates.iter().filter(|b| b.kind == kind).collect()
} else {
all_candidates.iter().collect()
};
// Besten Kandidaten wählen (erster in Prioritätsreihenfolge)
if let Some(browser) = candidates.first() {
info!("Selected browser: {} v{} ({})",
browser.kind.display_name(),
browser.version.as_deref().unwrap_or("unknown"),
browser.path.display());
return Ok(ResolvedBrowser {
browser: (*browser).clone(),
mode,
all_candidates,
});
}
// 5. Managed Install prüfen (nicht im Strict-Mode)
if mode != BrowserMode::Strict {
if let Some(managed) = check_managed_install(mode == BrowserMode::Fast) {
info!("Using managed browser: {}", managed.path.display());
return Ok(ResolvedBrowser {
browser: managed,
mode,
all_candidates,
});
}
}
// 6. Nichts gefunden
Err(BrowserResolveError::NoBrowserFound { all_candidates })
}
fn check_managed_install(prefer_headless_shell: bool) -> Option<DetectedBrowser> {
let base = dirs::home_dir()?.join(".auditmysite").join("browsers");
if prefer_headless_shell {
let shell_path = managed_binary_path(&base, "headless-shell");
if shell_path.exists() {
return Some(DetectedBrowser {
kind: BrowserKind::HeadlessShell,
path: shell_path,
version: read_version_file(&base.join("headless-shell")),
source: BrowserSource::ManagedInstall,
});
}
}
let cft_path = managed_binary_path(&base, "chrome-for-testing");
if cft_path.exists() {
return Some(DetectedBrowser {
kind: BrowserKind::ChromeForTesting,
path: cft_path,
version: read_version_file(&base.join("chrome-for-testing")),
source: BrowserSource::ManagedInstall,
});
}
None
}
```
### Detection (alle Browser scannen)
```rust
// browser/detection.rs
use std::path::{Path, PathBuf};
use std::process::Command;
use tracing::debug;
use super::registry::{self, RegistryEntry};
use super::types::*;
/// Findet alle installierten Browser auf dem System
pub fn detect_all_browsers() -> Vec<DetectedBrowser> {
let mut found = Vec::new();
// 1. Bekannte Pfade scannen
for entry in registry::system_browser_paths() {
let path = PathBuf::from(entry.path);
if path.exists() {
if let Ok(browser) = validate_browser(&path, entry.kind, BrowserSource::SystemPath) {
// Duplikate vermeiden (gleicher Pfad)
if !found.iter().any(|b: &DetectedBrowser| b.path == browser.path) {
debug!("Found {} at {}", entry.kind.display_name(), entry.path);
found.push(browser);
}
}
}
}
// 2. which/where für jeden BrowserKind
for kind in registry::search_order() {
for name in registry::which_names(*kind) {
if let Some(path) = which_binary(name) {
if !found.iter().any(|b| b.path == path) {
if let Ok(browser) = validate_browser(&path, *kind, BrowserSource::PathSearch) {
debug!("Found {} via which: {}", kind.display_name(), path.display());
found.push(browser);
}
}
}
}
}
// Sortieren nach Priorität (Chrome > Edge > Chromium)
let order = registry::search_order();
found.sort_by_key(|b| {
order.iter().position(|k| *k == b.kind).unwrap_or(usize::MAX)
});
found
}
/// Validiert eine Browser-Binary
pub fn validate_browser(
path: &Path,
kind: BrowserKind,
source: BrowserSource,
) -> Result<DetectedBrowser, DetectionError> {
// Existenz
if !path.exists() {
return Err(DetectionError::NotFound(path.to_path_buf()));
}
// Executable-Permission (Unix)
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let meta = std::fs::metadata(path)
.map_err(|_| DetectionError::NotExecutable(path.to_path_buf()))?;
if meta.permissions().mode() & 0o111 == 0 {
return Err(DetectionError::NotExecutable(path.to_path_buf()));
}
}
// Version
let version = get_browser_version(path);
Ok(DetectedBrowser {
kind,
path: path.to_path_buf(),
version,
source,
})
}
fn get_browser_version(path: &Path) -> Option<String> {
Command::new(path)
.arg("--version")
.output()
.ok()
.and_then(|output| {
if output.status.success() {
let text = String::from_utf8_lossy(&output.stdout);
// Extrahiere Versionsnummer: "Google Chrome 131.0.6778.108" → "131.0.6778.108"
text.split_whitespace()
.find(|s| s.starts_with(|c: char| c.is_ascii_digit()))
.map(|s| s.trim().to_string())
} else {
None
}
})
}
fn which_binary(name: &str) -> Option<PathBuf> {
let cmd = if cfg!(target_os = "windows") { "where" } else { "which" };
Command::new(cmd)
.arg(name)
.output()
.ok()
.and_then(|output| {
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout)
.lines()
.next()?
.trim()
.to_string();
if !path.is_empty() {
Some(PathBuf::from(path))
} else {
None
}
} else {
None
}
})
}
```
### CLI Browser Subcommand
```rust
// cli/browser.rs
use clap::Subcommand;
#[derive(Subcommand)]
pub enum BrowserCommand {
/// Detect all installed browsers
Detect,
/// Install Chrome for Testing
Install {
/// Install headless-shell instead (smaller, faster)
#[arg(long)]
headless_shell: bool,
/// Install specific version
#[arg(long)]
version: Option<String>,
/// Force reinstall even if already present
#[arg(long)]
force: bool,
},
/// Remove managed browser installation
Remove {
/// Remove headless-shell
#[arg(long)]
headless_shell: bool,
/// Remove all managed browsers
#[arg(long)]
all: bool,
},
/// Print path of the active browser
Path,
}
pub async fn handle_browser_command(cmd: &BrowserCommand) -> anyhow::Result<()> {
match cmd {
BrowserCommand::Detect => {
let browsers = crate::browser::detection::detect_all_browsers();
if browsers.is_empty() {
println!("No browsers found.");
println!();
println!("Install one:");
println!(" brew install --cask google-chrome");
println!(" auditmysite browser install");
} else {
for browser in &browsers {
println!(" ✓ {:<25} {:<20} {}",
browser.kind.display_name(),
browser.version.as_deref().unwrap_or("unknown"),
browser.path.display());
}
}
// Managed installs prüfen
let managed_dir = dirs::home_dir()
.map(|h| h.join(".auditmysite").join("browsers"));
if let Some(dir) = managed_dir {
let cft = dir.join("chrome-for-testing");
let hs = dir.join("headless-shell");
if cft.exists() {
println!(" ✓ {:<25} {:<20} {}",
"Chrome for Testing",
read_version(&cft),
cft.display());
} else {
println!(" ✗ Chrome for Testing not installed");
}
if hs.exists() {
println!(" ✓ {:<25} {:<20} {}",
"Headless Shell",
read_version(&hs),
hs.display());
} else {
println!(" ✗ Headless Shell not installed");
}
}
Ok(())
}
BrowserCommand::Install { headless_shell, version, force } => {
let target = if *headless_shell {
crate::browser::types::InstallTarget::HeadlessShell
} else {
crate::browser::types::InstallTarget::ChromeForTesting
};
crate::browser::installer::BrowserInstaller::install(target, version.as_deref(), *force).await?;
Ok(())
}
BrowserCommand::Remove { headless_shell, all } => {
// Implementierung: Verzeichnisse löschen
todo!()
}
BrowserCommand::Path => {
let opts = crate::browser::resolver::BrowserCliOptions {
browser_path: None,
browser_kind: None,
fast: false,
strict: false,
};
match crate::browser::resolver::resolve_browser(&opts) {
Ok(resolved) => println!("{}", resolved.browser.path.display()),
Err(_) => {
eprintln!("No browser found");
std::process::exit(1);
}
}
Ok(())
}
}
}
```
### Doctor
```rust
// doctor.rs
pub struct DoctorReport {
pub checks: Vec<DoctorCheck>,
}
pub struct DoctorCheck {
pub name: String,
pub status: CheckStatus,
pub detail: String,
}
pub enum CheckStatus {
Ok,
Warning,
Error,
}
pub fn run_doctor() -> DoctorReport {
let mut checks = Vec::new();
// 1. Browser-Check
let browsers = crate::browser::detection::detect_all_browsers();
if browsers.is_empty() {
checks.push(DoctorCheck {
name: "Browser".into(),
status: CheckStatus::Error,
detail: "No compatible browser found".into(),
});
} else {
let best = &browsers[0];
checks.push(DoctorCheck {
name: "Browser".into(),
status: CheckStatus::Ok,
detail: format!("{} v{}",
best.kind.display_name(),
best.version.as_deref().unwrap_or("unknown")),
});
}
// 2. Permissions
if let Some(browser) = browsers.first() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let executable = std::fs::metadata(&browser.path)
.map(|m| m.permissions().mode() & 0o111 != 0)
.unwrap_or(false);
checks.push(DoctorCheck {
name: "Permissions".into(),
status: if executable { CheckStatus::Ok } else { CheckStatus::Error },
detail: if executable { "Executable".into() } else { "Not executable".into() },
});
}
}
// 3. Disk space
let home = dirs::home_dir();
if let Some(ref home) = home {
let cache_dir = home.join(".auditmysite");
// Einfacher Check: 500 MB frei?
checks.push(DoctorCheck {
name: "Disk".into(),
status: CheckStatus::Ok,
detail: format!("Cache dir: {}", cache_dir.display()),
});
}
// 4. Config
let config_exists = std::path::Path::new("auditmysite.toml").exists();
checks.push(DoctorCheck {
name: "Config".into(),
status: if config_exists { CheckStatus::Ok } else { CheckStatus::Warning },
detail: if config_exists {
"auditmysite.toml found".into()
} else {
"No config file (using defaults)".into()
},
});
DoctorReport { checks }
}
pub fn print_doctor_report(report: &DoctorReport) {
for check in &report.checks {
let icon = match check.status {
CheckStatus::Ok => "✓",
CheckStatus::Warning => "⚠",
CheckStatus::Error => "✗",
};
println!(" {} {:<15} {}", icon, check.name, check.detail);
}
}
```
---
## 13. Entscheidungsempfehlung
### Was sollte der Default sein?
**System-Browser automatisch erkennen und verwenden.** Chrome > Edge > Chromium. Kein Download, kein Installieren, keine Überraschungen. Die meisten Entwickler und CI-Systeme haben Chrome oder Edge installiert. Das funktioniert sofort.
### Was sollte optional sein?
- `auditmysite browser install` für User ohne System-Browser (selten auf macOS/Windows, häufiger auf minimalen Linux-Servern)
- `--fast` Mode mit headless-shell (nach Validierung dass AXTree damit funktioniert)
- `--browser edge` für User die Edge statt Chrome bevorzugen
- `--strict` für CI-Pipelines die keine Fallbacks wollen
### Was sollte ich vermeiden?
1. **Keinen stillen Auto-Download.** Das aktuelle Verhalten (Chromium still herunterladen) ist das Kernproblem. Es überrascht User, verursacht Firewall-Probleme, und ist schwer zu debuggen.
2. **Keinen eigenen Browser bundlen.** In der Formula, im Release-Archiv, nirgendwo. Der Browser ist eine Laufzeit-Dependency, keine Build-Dependency.
3. **Keine harte Browser-Dependency in Homebrew.** `depends_on cask: "google-chrome"` erzeugt mehr Probleme als es löst.
4. **Kein Version-Pinning im Installer.** Die aktuelle hardcoded Version (131.0.6778.108) wird schnell veralten. Die Chrome for Testing API liefert die aktuelle Stable.
### Kompromisse
| Kein Auto-Download | Keine Überraschungen, keine Firewall-Probleme | User ohne Chrome muss `browser install` ausführen |
| System-Chrome bevorzugen | Signiert, keine Gatekeeper-Probleme, immer aktuell | Nicht unter unserer Kontrolle (Version) |
| Edge als Fallback | Auf Windows fast immer vorhanden | Minimaler Zusatz-Testaufwand |
| Managed Install optional | User hat explizite Kontrolle | Ein zusätzlicher Schritt für User ohne System-Browser |
| Kein Version-Pinning | Immer aktuelle Version | Theoretisch könnte eine Chrome-Version einen Bug haben |
### Empfohlene Umsetzungsreihenfolge
1. **Phase 1 (Detection Refactor)** → größter Impact, macht alles robuster
2. **Phase 2 (Explicit Installer)** → entfernt den stillen Download
3. **Phase 5 (Cleanup)** → kann direkt nach Phase 2 passieren
4. **Phase 3 (Doctor)** → nice-to-have, reduziert Support-Anfragen
5. **Phase 4 (Headless-Shell)** → nur wenn AXTree damit funktioniert, sonst niedrige Prio