Harden browser session cleanup
This commit is contained in:
+53
-4
@@ -6,8 +6,10 @@ import os
|
|||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -81,11 +83,19 @@ def _local_driver_path() -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _build_options(user_agent: str, headless: bool, chrome_binary: str) -> Options:
|
def _build_options(
|
||||||
|
user_agent: str,
|
||||||
|
headless: bool,
|
||||||
|
chrome_binary: str,
|
||||||
|
profile_dir: str | None = None,
|
||||||
|
) -> Options:
|
||||||
opts = Options()
|
opts = Options()
|
||||||
opts.binary_location = chrome_binary
|
opts.binary_location = chrome_binary
|
||||||
|
opts.page_load_strategy = "eager"
|
||||||
if headless:
|
if headless:
|
||||||
opts.add_argument("--headless=new")
|
opts.add_argument("--headless=new")
|
||||||
|
if profile_dir:
|
||||||
|
opts.add_argument(f"--user-data-dir={profile_dir}")
|
||||||
opts.add_argument(f"--user-agent={user_agent}")
|
opts.add_argument(f"--user-agent={user_agent}")
|
||||||
opts.add_argument("--no-sandbox")
|
opts.add_argument("--no-sandbox")
|
||||||
opts.add_argument("--disable-dev-shm-usage")
|
opts.add_argument("--disable-dev-shm-usage")
|
||||||
@@ -96,12 +106,15 @@ def _build_options(user_agent: str, headless: bool, chrome_binary: str) -> Optio
|
|||||||
return opts
|
return opts
|
||||||
|
|
||||||
|
|
||||||
def make_driver(headless: bool | None = None) -> uc.Chrome:
|
def make_driver(
|
||||||
|
headless: bool | None = None,
|
||||||
|
profile_dir: str | None = None,
|
||||||
|
) -> uc.Chrome:
|
||||||
"""Return a stealthed undetected Chrome instance."""
|
"""Return a stealthed undetected Chrome instance."""
|
||||||
use_headless = config.headless if headless is None else headless
|
use_headless = config.headless if headless is None else headless
|
||||||
chrome_binary, major = _detect_chrome()
|
chrome_binary, major = _detect_chrome()
|
||||||
ua = random.choice(_USER_AGENTS)
|
ua = random.choice(_USER_AGENTS)
|
||||||
opts = _build_options(ua, use_headless, chrome_binary)
|
opts = _build_options(ua, use_headless, chrome_binary, profile_dir)
|
||||||
|
|
||||||
log.debug("Starting ChromeDriver (headless=%s, Chrome %d)", use_headless, major)
|
log.debug("Starting ChromeDriver (headless=%s, Chrome %d)", use_headless, major)
|
||||||
|
|
||||||
@@ -116,6 +129,7 @@ def make_driver(headless: bool | None = None) -> uc.Chrome:
|
|||||||
driver.maximize_window()
|
driver.maximize_window()
|
||||||
else:
|
else:
|
||||||
driver.set_window_size(1920, 1080)
|
driver.set_window_size(1920, 1080)
|
||||||
|
driver.set_page_load_timeout(45)
|
||||||
|
|
||||||
stealth(
|
stealth(
|
||||||
driver,
|
driver,
|
||||||
@@ -139,13 +153,48 @@ def make_driver(headless: bool | None = None) -> uc.Chrome:
|
|||||||
return driver
|
return driver
|
||||||
|
|
||||||
|
|
||||||
|
def _profile_processes(profile_dir: str) -> list[int]:
|
||||||
|
marker = profile_dir.encode()
|
||||||
|
pids: list[int] = []
|
||||||
|
for entry in Path("/proc").iterdir():
|
||||||
|
if not entry.name.isdigit():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if marker in (entry / "cmdline").read_bytes():
|
||||||
|
pids.append(int(entry.name))
|
||||||
|
except (FileNotFoundError, PermissionError, ProcessLookupError):
|
||||||
|
continue
|
||||||
|
return pids
|
||||||
|
|
||||||
|
|
||||||
|
def _terminate_profile_processes(profile_dir: str) -> None:
|
||||||
|
for sig in (signal.SIGTERM, signal.SIGKILL):
|
||||||
|
pids = _profile_processes(profile_dir)
|
||||||
|
for pid in pids:
|
||||||
|
try:
|
||||||
|
os.kill(pid, sig)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
if sig == signal.SIGTERM and pids:
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def driver_session(headless: bool | None = None) -> Generator[uc.Chrome, None, None]:
|
def driver_session(headless: bool | None = None) -> Generator[uc.Chrome, None, None]:
|
||||||
driver = make_driver(headless)
|
profile_dir = tempfile.mkdtemp(prefix="seed-chrome-")
|
||||||
|
driver: uc.Chrome | None = None
|
||||||
try:
|
try:
|
||||||
|
driver = make_driver(headless, profile_dir=profile_dir)
|
||||||
yield driver
|
yield driver
|
||||||
finally:
|
finally:
|
||||||
|
_terminate_profile_processes(profile_dir)
|
||||||
|
if driver is not None:
|
||||||
|
try:
|
||||||
driver.quit()
|
driver.quit()
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning("Driver shutdown error: %s", exc)
|
||||||
|
_terminate_profile_processes(profile_dir)
|
||||||
|
shutil.rmtree(profile_dir, ignore_errors=True)
|
||||||
log.debug("Driver session closed")
|
log.debug("Driver session closed")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user