mirror of
https://github.com/daijro/camoufox.git
synced 2026-09-08 16:01:00 +00:00
feat(python): warn when a supplied binary predates the Playwright in use
A managed install below the version floor is upgraded by pkgman, but
executable_path deliberately bypasses that -- the caller supplied the binary,
so we neither replace it nor download another. That left one pairing nothing
checked: an old build driven by Playwright >= 1.61, which sends viewport fields
the older Juggler schema rejects. The user saw a bare
Protocol error (Browser.setDefaultViewport)
with nothing naming the cause.
Warn rather than raise, because the pairing is not always fatal. Camoufox
defaults to no_viewport when it spoofs window dimensions (sync_api), and
Playwright then never sends Browser.setDefaultViewport -- so the default path
works fine on an old build. Measured against a real beta.29 binary on
Playwright 1.62:
default path WORKS
new_context(viewport=...) BREAKS
new_context(no_viewport=False) BREAKS
new_context(viewport=..., is_mobile=False) BREAKS
Refusing to launch would break the setups in the first row. A build with no
version.json beside it -- an unpackaged objdir build -- tells us nothing, so it
is left alone rather than nagged about.
Verified end to end: warns on the real beta.29 build under Playwright 1.62,
silent on beta.30.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b68a4fb940
commit
8cb7914328
@@ -25,9 +25,13 @@ from .fingerprints import from_browserforge, from_preset, generate_fingerprint,
|
||||
from .geolocation import geoip_allowed, get_geolocation
|
||||
from .ip import Proxy, public_ip, valid_ipv4, valid_ipv6
|
||||
from .locales import handle_locales
|
||||
import warnings
|
||||
|
||||
from .pkgman import (
|
||||
INSTALL_DIR,
|
||||
OS_NAME,
|
||||
Version,
|
||||
effective_version_min,
|
||||
ensure_browser_profile_dir,
|
||||
get_path,
|
||||
installed_verstr,
|
||||
@@ -86,6 +90,57 @@ def _generate_fontconfig(fontconfig_path: str, path: Optional[Path] = None) -> s
|
||||
return runtime_conf
|
||||
|
||||
|
||||
def warn_if_executable_predates_playwright(path: Optional[Path]) -> None:
|
||||
"""Warn when a caller's own binary is older than their Playwright needs.
|
||||
|
||||
A managed install below the floor is simply upgraded (pkgman resolves it),
|
||||
but `executable_path` deliberately bypasses that -- the caller supplied the
|
||||
binary, so we neither replace it nor download another. That leaves the one
|
||||
pairing nothing checks: an old build driven by Playwright >= 1.61, which
|
||||
sends viewport fields the older Juggler schema rejects.
|
||||
|
||||
This warns rather than raises, because the pairing is not always fatal.
|
||||
Camoufox defaults to no_viewport when it spoofs window dimensions
|
||||
(sync_api), and Playwright then never sends Browser.setDefaultViewport --
|
||||
so the default path works on an old build. It breaks only when a viewport
|
||||
is set explicitly, and then the error is a bare "Protocol error
|
||||
(Browser.setDefaultViewport)" with nothing pointing at the real cause.
|
||||
Refusing to launch would break setups that currently work.
|
||||
|
||||
A build with no version.json beside it -- an unpackaged objdir build, say --
|
||||
tells us nothing, so it is left alone.
|
||||
"""
|
||||
if path is None:
|
||||
return
|
||||
try:
|
||||
installed = Version.from_path(Path(path).parent)
|
||||
except (FileNotFoundError, KeyError, ValueError):
|
||||
return
|
||||
|
||||
required = effective_version_min()
|
||||
if installed >= required:
|
||||
return
|
||||
|
||||
warnings.warn(
|
||||
f"The Camoufox build at {path} is {installed.build}, but Playwright "
|
||||
f"{_resolved_playwright_version_str()} needs at least {required.build}. "
|
||||
"Contexts created with an explicit viewport will fail with "
|
||||
'"Protocol error (Browser.setDefaultViewport)". Update the build, or pin '
|
||||
"playwright<1.61.",
|
||||
RuntimeWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
|
||||
|
||||
def _resolved_playwright_version_str() -> str:
|
||||
from importlib.metadata import version
|
||||
|
||||
try:
|
||||
return version('playwright')
|
||||
except Exception:
|
||||
return 'the installed version'
|
||||
|
||||
|
||||
def get_env_vars(
|
||||
config_map: Dict[str, str],
|
||||
user_agent_os: str,
|
||||
@@ -951,6 +1006,7 @@ def launch_options(
|
||||
pprint(config)
|
||||
|
||||
# Validate the config
|
||||
warn_if_executable_predates_playwright(executable_path)
|
||||
validate_config(config, path=executable_path)
|
||||
|
||||
# Prepare environment variables to pass to Camoufox
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""A caller's own binary is never replaced -- but it should still be checked.
|
||||
|
||||
A managed install below the floor gets upgraded by pkgman. `executable_path`
|
||||
deliberately bypasses that, which leaves one pairing nothing checks: an old
|
||||
build driven by Playwright >= 1.61, whose viewport fields the older Juggler
|
||||
schema rejects. Without this the user sees a bare "Protocol error
|
||||
(Browser.setDefaultViewport)" and nothing naming the cause.
|
||||
|
||||
It warns rather than raises on purpose: camoufox defaults to no_viewport when
|
||||
it spoofs window dimensions, so the default path works on an old build. Only an
|
||||
explicit viewport breaks, so refusing to launch would break working setups.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from camoufox import pkgman, utils
|
||||
|
||||
|
||||
def _bundle(tmp_path, build):
|
||||
"""A browser directory with version.json beside the binary, as a release has."""
|
||||
d = tmp_path / f"152.0.4-{build}"
|
||||
d.mkdir()
|
||||
(d / "version.json").write_text(json.dumps({"version": "152.0.4", "build": build}))
|
||||
return d / "camoufox-bin"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def floor_at_beta30(monkeypatch):
|
||||
monkeypatch.setattr(utils, "effective_version_min", lambda: pkgman.Version(build="beta.30"))
|
||||
|
||||
|
||||
def test_warns_when_the_supplied_build_is_too_old(tmp_path, floor_at_beta30):
|
||||
exe = _bundle(tmp_path, "beta.29")
|
||||
|
||||
with pytest.warns(RuntimeWarning, match=r"beta\.29.*beta\.30"):
|
||||
utils.warn_if_executable_predates_playwright(exe)
|
||||
|
||||
|
||||
def test_names_the_symptom_the_user_will_actually_see(tmp_path, floor_at_beta30):
|
||||
exe = _bundle(tmp_path, "beta.29")
|
||||
|
||||
with pytest.warns(RuntimeWarning) as caught:
|
||||
utils.warn_if_executable_predates_playwright(exe)
|
||||
|
||||
assert "Browser.setDefaultViewport" in str(caught[0].message)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("build", ["beta.30", "beta.31"])
|
||||
def test_silent_when_the_build_is_new_enough(tmp_path, floor_at_beta30, build, recwarn):
|
||||
utils.warn_if_executable_predates_playwright(_bundle(tmp_path, build))
|
||||
|
||||
assert not [w for w in recwarn if issubclass(w.category, RuntimeWarning)]
|
||||
|
||||
|
||||
def test_silent_for_a_custom_build_with_no_version_json(tmp_path, floor_at_beta30, recwarn):
|
||||
"""An unpackaged objdir build tells us nothing; do not nag about it."""
|
||||
(tmp_path / "dist").mkdir()
|
||||
|
||||
utils.warn_if_executable_predates_playwright(tmp_path / "dist" / "camoufox-bin")
|
||||
|
||||
assert not [w for w in recwarn if issubclass(w.category, RuntimeWarning)]
|
||||
|
||||
|
||||
def test_silent_when_no_executable_path_was_given(floor_at_beta30, recwarn):
|
||||
utils.warn_if_executable_predates_playwright(None)
|
||||
|
||||
assert not [w for w in recwarn if issubclass(w.category, RuntimeWarning)]
|
||||
Reference in New Issue
Block a user