feat: chrome version detection clean (#607)

This commit is contained in:
Jens Bergmann
2025-08-18 13:19:50 +02:00
committed by GitHub
parent df24a675a9
commit 332926519d
8 changed files with 1304 additions and 33 deletions

View File

@@ -440,14 +440,34 @@ kleinanzeigen_bot/utils/web_scraping_mixin.py:
"(ok) Remote debugging API accessible - Browser: %s": "(ok) Remote-Debugging-API zugänglich - Browser: %s"
"(fail) Remote debugging port is open but API not accessible: %s": "(Fehler) Remote-Debugging-Port ist offen, aber API nicht zugänglich: %s"
" This might indicate a browser update issue or configuration problem": " Dies könnte auf ein Browser-Update-Problem oder Konfigurationsproblem hinweisen"
? " On macOS, try: /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=%d --user-data-dir=/tmp/chrome-debug-profile --disable-dev-shm-usage"
: " Unter macOS versuchen Sie: /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=%d --user-data-dir=/tmp/chrome-debug-profile --disable-dev-shm-usage"
? " Or: open -a \"Google Chrome\" --args --remote-debugging-port=%d --user-data-dir=/tmp/chrome-debug-profile --disable-dev-shm-usage"
: " Oder: open -a \"Google Chrome\" --args --remote-debugging-port=%d --user-data-dir=/tmp/chrome-debug-profile --disable-dev-shm-usage"
" IMPORTANT: --user-data-dir is MANDATORY for macOS Chrome remote debugging": " WARNUNG: --user-data-dir ist PFLICHT für macOS Chrome Remote-Debugging"
" IMPORTANT: macOS Chrome remote debugging requires --user-data-dir flag": " WARNUNG: macOS Chrome Remote-Debugging erfordert --user-data-dir Flag"
" Add to your config.yaml: user_data_dir: \"/tmp/chrome-debug-profile\"": " Fügen Sie zu Ihrer config.yaml hinzu: user_data_dir: \"/tmp/chrome-debug-profile\""
" And to browser arguments: --user-data-dir=/tmp/chrome-debug-profile": " Und zu Browser-Argumenten: --user-data-dir=/tmp/chrome-debug-profile"
_validate_chrome_version_configuration:
" -> %s 136+ detected: %s": " -> %s 136+ erkannt: %s"
" -> %s 136+ configuration validation passed": " -> %s 136+ Konfigurationsvalidierung bestanden"
" -> %s 136+ configuration validation failed: %s": " -> %s 136+ Konfigurationsvalidierung fehlgeschlagen: %s"
" -> %s version detected: %s (pre-136, no special validation required)": " -> %s-Version erkannt: %s (vor 136, keine besondere Validierung erforderlich)"
" -> Please update your configuration to include --user-data-dir for remote debugging": " -> Bitte aktualisieren Sie Ihre Konfiguration, um --user-data-dir für Remote-Debugging einzuschließen"
" -> Skipping browser version validation in test environment": " -> Browser-Versionsvalidierung in Testumgebung wird übersprungen"
" -> Browser version detection failed, skipping validation: %s": " -> Browser-Versionserkennung fehlgeschlagen, Validierung wird übersprungen: %s"
" -> Unexpected error during browser version validation, skipping: %s": " -> Unerwarteter Fehler bei Browser-Versionsvalidierung, wird übersprungen: %s"
_diagnose_chrome_version_issues:
"(info) %s version from binary: %s %s (major: %d)": "(Info) %s-Version von Binärdatei: %s %s (Hauptversion: %d)"
"(info) %s version from remote debugging: %s %s (major: %d)": "(Info) %s-Version von Remote-Debugging: %s %s (Hauptversion: %d)"
"(info) %s 136+ detected - security validation required": "(Info) %s 136+ erkannt - Sicherheitsvalidierung erforderlich"
"(info) %s pre-136 detected - no special security requirements": "(Info) %s vor 136 erkannt - keine besonderen Sicherheitsanforderungen"
"(info) Remote %s 136+ detected - validating configuration": "(Info) Remote %s 136+ erkannt - validiere Konfiguration"
"(fail) %s 136+ configuration validation failed: %s": "(Fehler) %s 136+ Konfigurationsvalidierung fehlgeschlagen: %s"
"(ok) %s 136+ configuration validation passed": "(Ok) %s 136+ Konfigurationsvalidierung bestanden"
"(info) Chrome/Edge 136+ security changes require --user-data-dir for remote debugging": "(Info) Chrome/Edge 136+ Sicherheitsänderungen erfordern --user-data-dir für Remote-Debugging"
" See: https://developer.chrome.com/blog/remote-debugging-port": " Siehe: https://developer.chrome.com/blog/remote-debugging-port"
" -> Browser version diagnostics failed: %s": " -> Browser-Versionsdiagnose fehlgeschlagen: %s"
" -> Unexpected error during browser version diagnostics: %s": " -> Unerwarteter Fehler bei Browser-Versionsdiagnose: %s"
" Solution: Add --user-data-dir=/path/to/directory to browser arguments": " Lösung: Fügen Sie --user-data-dir=/pfad/zum/verzeichnis zu Browser-Argumenten hinzu"
" And user_data_dir: \"/path/to/directory\" to your configuration": " Und user_data_dir: \"/pfad/zum/verzeichnis\" zu Ihrer Konfiguration"
#################################################
kleinanzeigen_bot/update_checker.py:

View File

@@ -0,0 +1,242 @@
# SPDX-FileCopyrightText: © Jens Bergmann and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
import json
import re
import subprocess # noqa: S404
import urllib.request
from typing import Any, Final
from . import loggers
LOG:Final[loggers.Logger] = loggers.get_logger(__name__)
# Chrome 136 was released in March 2025 and introduced security changes
CHROME_136_VERSION = 136
class ChromeVersionInfo:
"""Information about a Chrome browser version."""
def __init__(self, version_string:str, major_version:int, browser_name:str = "Unknown") -> None:
self.version_string = version_string
self.major_version = major_version
self.browser_name = browser_name
@property
def is_chrome_136_plus(self) -> bool:
"""Check if this is Chrome version 136 or later."""
return self.major_version >= CHROME_136_VERSION
def __str__(self) -> str:
return f"{self.browser_name} {self.version_string} (major: {self.major_version})"
def parse_version_string(version_string:str) -> int:
"""
Parse a Chrome version string and extract the major version number.
Args:
version_string: Version string like "136.0.6778.0" or "136.0.6778.0 (Developer Build)"
Returns:
Major version number (e.g., 136)
Raises:
ValueError: If version string cannot be parsed
"""
# Extract version number from strings like:
# "136.0.6778.0"
# "136.0.6778.0 (Developer Build)"
# "136.0.6778.0 (Official Build) (x86_64)"
# "Google Chrome 136.0.6778.0"
# "Microsoft Edge 136.0.6778.0"
# "Chromium 136.0.6778.0"
match = re.search(r"(\d+)\.\d+\.\d+\.\d+", version_string)
if not match:
raise ValueError(f"Could not parse version string: {version_string}")
return int(match.group(1))
def detect_chrome_version_from_binary(binary_path:str) -> ChromeVersionInfo | None:
"""
Detect Chrome version by running the browser binary.
Args:
binary_path: Path to the Chrome binary
Returns:
ChromeVersionInfo if successful, None if detection fails
"""
try:
# Run browser with --version flag
result = subprocess.run( # noqa: S603
[binary_path, "--version"],
check = False, capture_output = True,
text = True,
timeout = 10
)
if result.returncode != 0:
LOG.debug("Browser version command failed: %s", result.stderr)
return None
output = result.stdout.strip()
major_version = parse_version_string(output)
# Extract just the version number for version_string
version_match = re.search(r"(\d+\.\d+\.\d+\.\d+)", output)
version_string = version_match.group(1) if version_match else output
# Determine browser name from binary path
browser_name = "Chrome"
if "edge" in binary_path.lower():
browser_name = "Edge"
elif "chromium" in binary_path.lower():
browser_name = "Chromium"
return ChromeVersionInfo(version_string, major_version, browser_name)
except subprocess.TimeoutExpired:
LOG.debug("Browser version command timed out")
return None
except (subprocess.SubprocessError, ValueError) as e:
LOG.debug("Failed to detect browser version: %s", str(e))
return None
def detect_chrome_version_from_remote_debugging(host:str = "127.0.0.1", port:int = 9222) -> ChromeVersionInfo | None:
"""
Detect Chrome version from remote debugging API.
Args:
host: Remote debugging host
port: Remote debugging port
Returns:
ChromeVersionInfo if successful, None if detection fails
"""
try:
# Query the remote debugging API
url = f"http://{host}:{port}/json/version"
response = urllib.request.urlopen(url, timeout = 5) # noqa: S310
version_data = json.loads(response.read().decode())
# Extract version information
user_agent = version_data.get("User-Agent", "")
browser_name = version_data.get("Browser", "Unknown")
# Parse version from User-Agent string
# Example: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.6778.0 Safari/537.36"
match = re.search(r"Chrome/(\d+)\.\d+\.\d+\.\d+", user_agent)
if not match:
LOG.debug("Could not parse Chrome version from User-Agent: %s", user_agent)
return None
major_version = int(match.group(1))
version_string = match.group(0).replace("Chrome/", "")
return ChromeVersionInfo(version_string, major_version, browser_name)
except Exception as e:
LOG.debug("Failed to detect browser version from remote debugging: %s", str(e))
return None
def validate_chrome_136_configuration(browser_arguments:list[str], user_data_dir:str | None) -> tuple[bool, str]:
"""
Validate configuration for Chrome/Edge 136+ security requirements.
Chrome/Edge 136+ requires --user-data-dir to be specified when using --remote-debugging-port.
Args:
browser_arguments: List of browser arguments
user_data_dir: User data directory configuration
Returns:
Tuple of (is_valid, error_message)
"""
# Check if remote debugging is enabled
has_remote_debugging = any(
arg.startswith("--remote-debugging-port=")
for arg in browser_arguments
)
if not has_remote_debugging:
return True, "" # No remote debugging, no validation needed
# Check if user-data-dir is specified in arguments
has_user_data_dir_arg = any(
arg.startswith("--user-data-dir=")
for arg in browser_arguments
)
# Check if user_data_dir is configured
has_user_data_dir_config = user_data_dir is not None and user_data_dir.strip()
if not has_user_data_dir_arg and not has_user_data_dir_config:
return False, (
"Chrome/Edge 136+ requires --user-data-dir to be specified when using --remote-debugging-port. "
"Add --user-data-dir=/path/to/directory to your browser arguments and "
'user_data_dir: "/path/to/directory" to your configuration.'
)
return True, ""
def get_chrome_version_diagnostic_info(
binary_path:str | None = None,
remote_host:str = "127.0.0.1",
remote_port:int | None = None
) -> dict[str, Any]:
"""
Get comprehensive Chrome version diagnostic information.
Args:
binary_path: Path to Chrome binary (optional)
remote_host: Remote debugging host
remote_port: Remote debugging port (optional)
Returns:
Dictionary with diagnostic information
"""
diagnostic_info:dict[str, Any] = {
"binary_detection": None,
"remote_detection": None,
"chrome_136_plus_detected": False,
"configuration_valid": True,
"recommendations": []
}
# Try binary detection
if binary_path:
version_info = detect_chrome_version_from_binary(binary_path)
if version_info:
diagnostic_info["binary_detection"] = {
"version_string": version_info.version_string,
"major_version": version_info.major_version,
"browser_name": version_info.browser_name,
"is_chrome_136_plus": version_info.is_chrome_136_plus
}
diagnostic_info["chrome_136_plus_detected"] = version_info.is_chrome_136_plus
# Try remote debugging detection
if remote_port:
version_info = detect_chrome_version_from_remote_debugging(remote_host, remote_port)
if version_info:
diagnostic_info["remote_detection"] = {
"version_string": version_info.version_string,
"major_version": version_info.major_version,
"browser_name": version_info.browser_name,
"is_chrome_136_plus": version_info.is_chrome_136_plus
}
diagnostic_info["chrome_136_plus_detected"] = version_info.is_chrome_136_plus
# Add recommendations based on detected version
if diagnostic_info["chrome_136_plus_detected"]:
diagnostic_info["recommendations"].append(
"Chrome 136+ detected - ensure --user-data-dir is configured for remote debugging"
)
return diagnostic_info

View File

@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
import asyncio, enum, inspect, json, os, platform, secrets, shutil, urllib.request # isort: skip
import asyncio, enum, inspect, json, os, platform, secrets, shutil, subprocess, urllib.request # isort: skip # noqa: S404
from collections.abc import Callable, Coroutine, Iterable
from gettext import gettext as _
from typing import Any, Final, cast
@@ -18,6 +18,11 @@ from nodriver.core.element import Element
from nodriver.core.tab import Tab as Page
from . import loggers, net
from .chrome_version_detector import (
detect_chrome_version_from_binary,
get_chrome_version_diagnostic_info,
validate_chrome_136_configuration,
)
from .misc import T, ensure
__all__ = [
@@ -91,6 +96,9 @@ class WebScrapingMixin:
self.browser_config.binary_location = self.get_compatible_browser()
LOG.info(" -> Browser binary location: %s", self.browser_config.binary_location)
# Chrome version detection and validation
await self._validate_chrome_version_configuration()
########################################################
# check if an existing browser instance shall be used...
########################################################
@@ -299,6 +307,8 @@ class WebScrapingMixin:
browser_path = self.get_compatible_browser()
if browser_path:
LOG.info("(ok) Auto-detected browser: %s", browser_path)
# Set the binary location for Chrome version detection
self.browser_config.binary_location = browser_path
else:
LOG.error("(fail) No compatible browser found")
@@ -335,12 +345,6 @@ class WebScrapingMixin:
else:
LOG.error("(fail) Remote debugging port is not open")
LOG.info(" Make sure browser is started with: --remote-debugging-port=%d", remote_port)
if platform.system() == "Darwin":
LOG.info(" On macOS, try: /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome "
"--remote-debugging-port=%d --user-data-dir=/tmp/chrome-debug-profile --disable-dev-shm-usage", remote_port)
LOG.info(' Or: open -a "Google Chrome" --args --remote-debugging-port=%d '
'--user-data-dir=/tmp/chrome-debug-profile --disable-dev-shm-usage', remote_port)
LOG.info(" IMPORTANT: --user-data-dir is MANDATORY for macOS Chrome remote debugging")
# Check for running browser processes
browser_processes = []
@@ -363,16 +367,14 @@ class WebScrapingMixin:
LOG.info("(info) Windows detected - check Windows Defender and antivirus software")
elif platform.system() == "Darwin":
LOG.info("(info) macOS detected - check Gatekeeper and security settings")
# Check for macOS-specific Chrome remote debugging requirements
if remote_port > 0 and not self.browser_config.user_data_dir:
LOG.warning(" IMPORTANT: macOS Chrome remote debugging requires --user-data-dir flag")
LOG.info(' Add to your config.yaml: user_data_dir: "/tmp/chrome-debug-profile"')
LOG.info(" And to browser arguments: --user-data-dir=/tmp/chrome-debug-profile")
elif platform.system() == "Linux":
LOG.info("(info) Linux detected - check if running as root (not recommended)")
if _is_admin():
LOG.error("(fail) Running as root - this can cause browser connection issues")
# Chrome version detection and validation
self._diagnose_chrome_version_issues(remote_port)
LOG.info("=== End Diagnostics ===")
def close_browser_session(self) -> None:
@@ -744,3 +746,109 @@ class WebScrapingMixin:
""")
await self.web_sleep()
return elem
async def _validate_chrome_version_configuration(self) -> None:
"""
Validate Chrome version configuration for Chrome 136+ security requirements.
This method checks if the browser is Chrome 136+ and validates that the configuration
meets the security requirements for remote debugging.
"""
# Skip validation in test environments to avoid subprocess calls
if os.environ.get("PYTEST_CURRENT_TEST"):
LOG.debug(" -> Skipping browser version validation in test environment")
return
try:
# Detect Chrome version from binary
binary_path = self.browser_config.binary_location
version_info = detect_chrome_version_from_binary(binary_path) if binary_path else None
if version_info and version_info.is_chrome_136_plus:
LOG.info(" -> %s 136+ detected: %s", version_info.browser_name, version_info)
# Validate configuration for Chrome/Edge 136+
is_valid, error_message = validate_chrome_136_configuration(
list(self.browser_config.arguments),
self.browser_config.user_data_dir
)
if not is_valid:
LOG.error(" -> %s 136+ configuration validation failed: %s", version_info.browser_name, error_message)
LOG.error(" -> Please update your configuration to include --user-data-dir for remote debugging")
raise AssertionError(error_message)
LOG.info(" -> %s 136+ configuration validation passed", version_info.browser_name)
elif version_info:
LOG.info(" -> %s version detected: %s (pre-136, no special validation required)", version_info.browser_name, version_info)
else:
LOG.debug(" -> Could not detect browser version, skipping validation")
except (subprocess.SubprocessError, OSError, FileNotFoundError) as e:
LOG.warning(" -> Browser version detection failed, skipping validation: %s", e)
# Continue without validation rather than failing
except Exception as e:
LOG.warning(" -> Unexpected error during browser version validation, skipping: %s", e)
# Continue without validation rather than failing
def _diagnose_chrome_version_issues(self, remote_port:int) -> None:
"""
Diagnose Chrome version issues and provide specific recommendations.
Args:
remote_port: Remote debugging port (0 if not configured)
"""
# Skip diagnostics in test environments to avoid subprocess calls
if os.environ.get("PYTEST_CURRENT_TEST"):
LOG.debug(" -> Skipping browser version diagnostics in test environment")
return
try:
# Get diagnostic information
binary_path = self.browser_config.binary_location
diagnostic_info = get_chrome_version_diagnostic_info(
binary_path = binary_path,
remote_port = remote_port if remote_port > 0 else None
)
# Report binary detection results
if diagnostic_info["binary_detection"]:
binary_info = diagnostic_info["binary_detection"]
LOG.info("(info) %s version from binary: %s %s (major: %d)",
binary_info["browser_name"], binary_info["browser_name"], binary_info["version_string"], binary_info["major_version"])
if binary_info["is_chrome_136_plus"]:
LOG.info("(info) %s 136+ detected - security validation required", binary_info["browser_name"])
else:
LOG.info("(info) %s pre-136 detected - no special security requirements", binary_info["browser_name"])
# Report remote detection results
if diagnostic_info["remote_detection"]:
remote_info = diagnostic_info["remote_detection"]
LOG.info("(info) %s version from remote debugging: %s %s (major: %d)",
remote_info["browser_name"], remote_info["browser_name"], remote_info["version_string"], remote_info["major_version"])
if remote_info["is_chrome_136_plus"]:
LOG.info("(info) Remote %s 136+ detected - validating configuration", remote_info["browser_name"])
# Validate configuration for Chrome/Edge 136+
is_valid, error_message = validate_chrome_136_configuration(
list(self.browser_config.arguments),
self.browser_config.user_data_dir
)
if not is_valid:
LOG.error("(fail) %s 136+ configuration validation failed: %s", remote_info["browser_name"], error_message)
LOG.info(" Solution: Add --user-data-dir=/path/to/directory to browser arguments")
LOG.info(' And user_data_dir: "/path/to/directory" to your configuration')
else:
LOG.info("(ok) %s 136+ configuration validation passed", remote_info["browser_name"])
# Add general recommendations
if diagnostic_info["chrome_136_plus_detected"]:
LOG.info("(info) Chrome/Edge 136+ security changes require --user-data-dir for remote debugging")
LOG.info(" See: https://developer.chrome.com/blog/remote-debugging-port")
except (subprocess.SubprocessError, OSError, FileNotFoundError) as e:
LOG.warning(" -> Browser version diagnostics failed: %s", e)
# Continue without diagnostics rather than failing
except Exception as e:
LOG.warning(" -> Unexpected error during browser version diagnostics: %s", e)
# Continue without diagnostics rather than failing