Files

149 lines
4.6 KiB
Python

"""
Download and patch ChromeDriver once. Run via: make patch-driver
Uses the official Chrome for Testing API to fetch the exact ChromeDriver
version matching the installed Chrome, then patches it with undetected_chromedriver.
"""
from __future__ import annotations
import io
import re
import shutil
import stat
import subprocess
import sys
import urllib.request
import zipfile
from pathlib import Path
import undetected_chromedriver as uc
from config import config
from logger import get_logger
log = get_logger(__name__)
_DOWNLOAD_HEADERS = {
"User-Agent": (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
)
}
def _get_chrome_full_version() -> tuple[int, str]:
candidates = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"]
if config.chrome_binary:
candidates.insert(0, config.chrome_binary)
for binary in candidates:
if not shutil.which(binary) and not Path(binary).exists():
continue
try:
out = subprocess.check_output(
[binary, "--version"], stderr=subprocess.DEVNULL, text=True, timeout=5
).strip()
match = re.search(r"\b(\d+(?:\.\d+){3})\b", out)
if match is None:
continue
full = match.group(1)
major = int(full.split(".")[0])
log.info("Detected Chrome %s via '%s'", full, binary)
return major, full
except Exception:
continue
log.critical("Could not detect Chrome version. Set CHROME_BINARY in .env")
sys.exit(1)
def _latest_chromedriver_version(major: int) -> str:
url = f"https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_{major}"
try:
request = urllib.request.Request(url, headers=_DOWNLOAD_HEADERS)
with urllib.request.urlopen(request, timeout=15) as resp:
version = resp.read().decode().strip()
log.info("Latest ChromeDriver for Chrome %d: %s", major, version)
return version
except Exception as exc:
log.critical("Could not fetch ChromeDriver version for Chrome %d: %s", major, exc)
sys.exit(1)
def _download_chromedriver(version: str, dest: Path) -> None:
zip_url = (
f"https://storage.googleapis.com/chrome-for-testing-public"
f"/{version}/linux64/chromedriver-linux64.zip"
)
log.info("Downloading ChromeDriver %s …", version)
try:
request = urllib.request.Request(zip_url, headers=_DOWNLOAD_HEADERS)
with urllib.request.urlopen(request, timeout=60) as resp:
data = resp.read()
except Exception as exc:
log.critical("Download failed: %s", exc)
sys.exit(1)
with zipfile.ZipFile(io.BytesIO(data)) as zf:
binary_name = next(
n for n in zf.namelist() if n.endswith("/chromedriver") or n == "chromedriver"
)
dest.write_bytes(zf.read(binary_name))
dest.chmod(dest.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
log.info("Extracted to %s", dest)
def _patch(dest: Path) -> None:
log.info("Patching binary …")
patcher = uc.Patcher(executable_path=str(dest))
patcher.patch_exe()
log.info("Patch applied successfully")
def _validate_existing_driver(dest: Path, chrome_major: int) -> bool:
if not dest.exists():
return False
try:
out = subprocess.check_output(
[str(dest), "--version"], stderr=subprocess.DEVNULL, text=True, timeout=5
).strip()
driver_major = int(out.split()[1].split(".")[0])
except Exception as exc:
log.critical("Could not inspect ChromeDriver at %s: %s", dest, exc)
sys.exit(1)
if driver_major != chrome_major:
log.critical(
"ChromeDriver major %d does not match Chrome major %d",
driver_major,
chrome_major,
)
sys.exit(1)
log.info("Using packaged ChromeDriver %s", out.split()[1])
return True
def main() -> None:
dest = Path(config.chromedriver_path)
dest.parent.mkdir(parents=True, exist_ok=True)
major, _ = _get_chrome_full_version()
if _validate_existing_driver(dest, major):
_patch(dest)
log.info("ChromeDriver ready at %s", dest)
return
cd_version = _latest_chromedriver_version(major)
_download_chromedriver(cd_version, dest)
_patch(dest)
log.info("ChromeDriver %s ready at %s", cd_version, dest)
log.info("Re-run 'make patch-driver' only when Chrome updates to a new major version.")
if __name__ == "__main__":
main()