Open target URL during Chrome startup
This commit is contained in:
+5
-1
@@ -35,7 +35,11 @@ def _single_run(
|
|||||||
target_url or "<missing>",
|
target_url or "<missing>",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
with driver_session(headless=headless, recording_name=identifier) as driver:
|
with driver_session(
|
||||||
|
headless=headless,
|
||||||
|
recording_name=identifier,
|
||||||
|
initial_url=target_url,
|
||||||
|
) as driver:
|
||||||
FreshSessionScenario(driver, target_url).run()
|
FreshSessionScenario(driver, target_url).run()
|
||||||
result = DynamicFlow(driver, cfg).run() if cfg else None
|
result = DynamicFlow(driver, cfg).run() if cfg else None
|
||||||
|
|
||||||
|
|||||||
+44
-4
@@ -13,9 +13,10 @@ import tempfile
|
|||||||
import time
|
import time
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Generator
|
from typing import Any, Generator, cast
|
||||||
|
|
||||||
import undetected_chromedriver as uc
|
import undetected_chromedriver as uc
|
||||||
|
from selenium.common.exceptions import WebDriverException
|
||||||
from selenium.webdriver.chrome.options import Options
|
from selenium.webdriver.chrome.options import Options
|
||||||
from selenium_stealth import stealth
|
from selenium_stealth import stealth
|
||||||
|
|
||||||
@@ -90,10 +91,13 @@ def _build_options(
|
|||||||
chrome_binary: str,
|
chrome_binary: str,
|
||||||
profile_dir: str | None = None,
|
profile_dir: str | None = None,
|
||||||
display: str | None = None,
|
display: str | None = None,
|
||||||
|
initial_url: str | None = None,
|
||||||
) -> Options:
|
) -> Options:
|
||||||
opts = Options()
|
opts = Options()
|
||||||
opts.binary_location = chrome_binary
|
opts.binary_location = chrome_binary
|
||||||
opts.page_load_strategy = "eager"
|
# Never let ChromeDriver session creation block on the initial New Tab
|
||||||
|
# renderer. Scenarios perform their own explicit URL/readiness waits.
|
||||||
|
opts.page_load_strategy = "none"
|
||||||
if headless:
|
if headless:
|
||||||
opts.add_argument("--headless=new")
|
opts.add_argument("--headless=new")
|
||||||
if profile_dir:
|
if profile_dir:
|
||||||
@@ -108,6 +112,10 @@ def _build_options(
|
|||||||
opts.add_argument("--disable-blink-features=AutomationControlled")
|
opts.add_argument("--disable-blink-features=AutomationControlled")
|
||||||
opts.add_argument("--disable-infobars")
|
opts.add_argument("--disable-infobars")
|
||||||
opts.add_argument("--window-size=1920,1080")
|
opts.add_argument("--window-size=1920,1080")
|
||||||
|
if initial_url and initial_url.startswith(("http://", "https://")):
|
||||||
|
# Opening the configured page as a Chrome process argument avoids
|
||||||
|
# waiting for the initial New Tab renderer before navigation begins.
|
||||||
|
opts.add_argument(initial_url)
|
||||||
return opts
|
return opts
|
||||||
|
|
||||||
|
|
||||||
@@ -115,14 +123,27 @@ def make_driver(
|
|||||||
headless: bool | None = None,
|
headless: bool | None = None,
|
||||||
profile_dir: str | None = None,
|
profile_dir: str | None = None,
|
||||||
display: str | None = None,
|
display: str | None = None,
|
||||||
|
initial_url: str | None = None,
|
||||||
) -> uc.Chrome:
|
) -> 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, profile_dir, display)
|
opts = _build_options(
|
||||||
|
ua,
|
||||||
|
use_headless,
|
||||||
|
chrome_binary,
|
||||||
|
profile_dir,
|
||||||
|
display,
|
||||||
|
initial_url,
|
||||||
|
)
|
||||||
|
|
||||||
log.debug("Starting ChromeDriver (headless=%s, Chrome %d)", use_headless, major)
|
log.info(
|
||||||
|
"Starting ChromeDriver (headless=%s, Chrome %d, initial_url=%s)",
|
||||||
|
use_headless,
|
||||||
|
major,
|
||||||
|
initial_url or "<none>",
|
||||||
|
)
|
||||||
|
|
||||||
driver = uc.Chrome(
|
driver = uc.Chrome(
|
||||||
options=opts,
|
options=opts,
|
||||||
@@ -130,6 +151,23 @@ def make_driver(
|
|||||||
version_main=major,
|
version_main=major,
|
||||||
use_subprocess=True,
|
use_subprocess=True,
|
||||||
)
|
)
|
||||||
|
log.info("ChromeDriver connected")
|
||||||
|
|
||||||
|
if initial_url and initial_url.startswith(("http://", "https://")):
|
||||||
|
navigation = cast(
|
||||||
|
dict[str, Any],
|
||||||
|
driver.execute_cdp_cmd( # type: ignore[reportUnknownMemberType]
|
||||||
|
"Page.navigate", {"url": initial_url}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
error_text = navigation.get("errorText")
|
||||||
|
if error_text:
|
||||||
|
raise WebDriverException(
|
||||||
|
f"Chrome rejected initial navigation to {initial_url}: {error_text}"
|
||||||
|
)
|
||||||
|
log.info("Initial navigation dispatched to %s", initial_url)
|
||||||
|
|
||||||
|
log.info("Applying stealth settings")
|
||||||
|
|
||||||
if display:
|
if display:
|
||||||
driver.set_window_size(1920, 1080)
|
driver.set_window_size(1920, 1080)
|
||||||
@@ -191,6 +229,7 @@ def _terminate_profile_processes(profile_dir: str) -> None:
|
|||||||
def driver_session(
|
def driver_session(
|
||||||
headless: bool | None = None,
|
headless: bool | None = None,
|
||||||
recording_name: str | None = None,
|
recording_name: str | None = None,
|
||||||
|
initial_url: str | None = None,
|
||||||
) -> Generator[uc.Chrome, None, None]:
|
) -> Generator[uc.Chrome, None, None]:
|
||||||
profile_dir = tempfile.mkdtemp(prefix="seed-chrome-")
|
profile_dir = tempfile.mkdtemp(prefix="seed-chrome-")
|
||||||
driver: uc.Chrome | None = None
|
driver: uc.Chrome | None = None
|
||||||
@@ -207,6 +246,7 @@ def driver_session(
|
|||||||
effective_headless,
|
effective_headless,
|
||||||
profile_dir=profile_dir,
|
profile_dir=profile_dir,
|
||||||
display=display,
|
display=display,
|
||||||
|
initial_url=initial_url,
|
||||||
)
|
)
|
||||||
yield driver
|
yield driver
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ from logger import get_logger
|
|||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
_BLANK_URLS = {"", "about:blank", "data:,"}
|
|
||||||
|
|
||||||
|
|
||||||
class FreshSessionError(RuntimeError):
|
class FreshSessionError(RuntimeError):
|
||||||
"""The browser could not leave its initial blank page."""
|
"""The browser could not leave its initial blank page."""
|
||||||
|
|
||||||
@@ -32,6 +29,11 @@ def _normalise_url(url: str) -> str:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _is_web_url(url: str) -> bool:
|
||||||
|
parsed = urlsplit(url)
|
||||||
|
return parsed.scheme in {"http", "https"} and bool(parsed.netloc)
|
||||||
|
|
||||||
|
|
||||||
class FreshSessionScenario:
|
class FreshSessionScenario:
|
||||||
"""
|
"""
|
||||||
Opens the target URL in a brand-new browser profile (no stored cookies,
|
Opens the target URL in a brand-new browser profile (no stored cookies,
|
||||||
@@ -76,10 +78,18 @@ class FreshSessionScenario:
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.wait.until(lambda d: d.current_url not in _BLANK_URLS)
|
# Chrome's internal New Tab URLs (chrome://newtab and
|
||||||
|
# chrome://new-tab-page) are not successful navigation. Wait until
|
||||||
|
# the address bar actually contains an HTTP(S) page or redirect.
|
||||||
|
self.wait.until(lambda d: _is_web_url(d.current_url))
|
||||||
except TimeoutException as exc:
|
except TimeoutException as exc:
|
||||||
|
try:
|
||||||
|
current_url = self.driver.current_url
|
||||||
|
except WebDriverException:
|
||||||
|
current_url = "<unavailable>"
|
||||||
raise FreshSessionError(
|
raise FreshSessionError(
|
||||||
f"navigation to {self.url} never left Chrome's blank page"
|
f"navigation to {self.url} never reached a web page "
|
||||||
|
f"(Chrome remained at {current_url})"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
human_delay(1.0, 2.0)
|
human_delay(1.0, 2.0)
|
||||||
|
|||||||
+5
-1
@@ -314,7 +314,11 @@ def run_batch_job(batch_id: str, item: dict[str, Any]) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with driver_session(headless=headless, recording_name=identifier) as driver:
|
with driver_session(
|
||||||
|
headless=headless,
|
||||||
|
recording_name=identifier,
|
||||||
|
initial_url=target_url,
|
||||||
|
) as driver:
|
||||||
FreshSessionScenario(driver, target_url).run()
|
FreshSessionScenario(driver, target_url).run()
|
||||||
if cfg:
|
if cfg:
|
||||||
if scenario == "digipay":
|
if scenario == "digipay":
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ def run_otp(phone: str, cfg_id: int | None = None) -> None:
|
|||||||
target_url: str = flow_cfg.get("target_url") or "" if flow_cfg else ""
|
target_url: str = flow_cfg.get("target_url") or "" if flow_cfg else ""
|
||||||
t0 = time.monotonic()
|
t0 = time.monotonic()
|
||||||
try:
|
try:
|
||||||
with driver_session() as driver:
|
with driver_session(initial_url=target_url) as driver:
|
||||||
OTPLoginScenario(driver, phone, otp_resolver, target_url).run()
|
OTPLoginScenario(driver, phone, otp_resolver, target_url).run()
|
||||||
result = DynamicFlow(driver, flow_cfg).run() if flow_cfg else None
|
result = DynamicFlow(driver, flow_cfg).run() if flow_cfg else None
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -91,7 +91,7 @@ def run_fresh(cfg_id: int | None = None) -> None:
|
|||||||
target_url: str = flow_cfg.get("target_url") or "" if flow_cfg else ""
|
target_url: str = flow_cfg.get("target_url") or "" if flow_cfg else ""
|
||||||
t0 = time.monotonic()
|
t0 = time.monotonic()
|
||||||
try:
|
try:
|
||||||
with driver_session() as driver:
|
with driver_session(initial_url=target_url) as driver:
|
||||||
FreshSessionScenario(driver, target_url).run()
|
FreshSessionScenario(driver, target_url).run()
|
||||||
result = DynamicFlow(driver, flow_cfg).run() if flow_cfg else None
|
result = DynamicFlow(driver, flow_cfg).run() if flow_cfg else None
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
Reference in New Issue
Block a user