116 lines
3.6 KiB
Python
116 lines
3.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 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()
|
|
full = out.split()[-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 main() -> None:
|
|
dest = Path(config.chromedriver_path)
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
major, _ = _get_chrome_full_version()
|
|
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()
|