mirror of
https://github.com/daijro/camoufox.git
synced 2026-10-05 00:00:20 +00:00
fix(pythonlib): handle headless='virtual' in launch_server
launch_server() is documented to take the same arguments as Camoufox(), but passed headless='virtual' straight to launch_options(), so the server launched with no Xvfb display. Start a VirtualDisplay the way Camoufox() does, launch headful on it, and kill it when the server process exits or the launch fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d3d9d73c2d
commit
fd99a0d85f
@@ -8,6 +8,7 @@ from playwright._impl._driver import compute_driver_executable
|
||||
|
||||
from camoufox.pkgman import LOCAL_DATA
|
||||
from camoufox.utils import launch_options
|
||||
from camoufox.virtdisplay import VirtualDisplay
|
||||
|
||||
LAUNCH_SCRIPT: Path = LOCAL_DATA / "launchServer.js"
|
||||
|
||||
@@ -60,7 +61,20 @@ def launch_server(**kwargs) -> NoReturn:
|
||||
)
|
||||
kwargs.pop(unsupported, None)
|
||||
|
||||
config = launch_options(**kwargs)
|
||||
virtual_display = None
|
||||
if kwargs.get('headless') == 'virtual':
|
||||
virtual_display = VirtualDisplay(debug=kwargs.get('debug'))
|
||||
kwargs['virtual_display'] = virtual_display.get()
|
||||
kwargs['headless'] = False
|
||||
try:
|
||||
_serve(launch_options(**kwargs))
|
||||
finally:
|
||||
if virtual_display:
|
||||
virtual_display.kill()
|
||||
|
||||
|
||||
def _serve(config: Dict[str, Any]) -> NoReturn:
|
||||
"""Run launchServer.js with `config` until the Node process exits."""
|
||||
nodejs = get_nodejs()
|
||||
|
||||
data = orjson.dumps(to_camel_case_dict(config))
|
||||
|
||||
@@ -892,8 +892,8 @@ def launch_options(
|
||||
To prevent leaks, only use this for special cases.
|
||||
headless (Optional[bool]):
|
||||
Whether to run the browser in headless mode. Defaults to False.
|
||||
Note: If you are running linux, passing headless='virtual' to Camoufox & AsyncCamoufox
|
||||
will use Xvfb.
|
||||
Note: If you are running linux, passing headless='virtual' to Camoufox, AsyncCamoufox
|
||||
or launch_server will use Xvfb.
|
||||
main_world_eval (Optional[bool]):
|
||||
Whether to enable running scripts in the main world.
|
||||
To use this, prepend "mw:" to the script: page.evaluate("mw:" + script).
|
||||
@@ -921,7 +921,7 @@ def launch_options(
|
||||
debug (Optional[bool]):
|
||||
Prints the config being sent to Camoufox.
|
||||
virtual_display (Optional[str]):
|
||||
Virtual display number. Ex: ':99'. This is handled by Camoufox & AsyncCamoufox.
|
||||
Virtual display number. Ex: ':99'. This is handled by Camoufox, AsyncCamoufox and launch_server.
|
||||
pin_cpu_cores (Optional[bool]):
|
||||
Pin the browser to navigator.hardwareConcurrency cores
|
||||
(Linux/Windows) so the fingerprint's own core count can be kept:
|
||||
|
||||
@@ -275,3 +275,79 @@ def test_launch_server_surfaces_child_exit_instead_of_pipe_error(monkeypatch, tm
|
||||
server.launch_server()
|
||||
|
||||
assert "3" in str(excinfo.value), str(excinfo.value)
|
||||
|
||||
|
||||
class _FakeVirtualDisplay:
|
||||
instances = []
|
||||
|
||||
def __init__(self, debug=None):
|
||||
self.debug = debug
|
||||
self.killed = False
|
||||
_FakeVirtualDisplay.instances.append(self)
|
||||
|
||||
def get(self):
|
||||
return ":99"
|
||||
|
||||
def kill(self):
|
||||
self.killed = True
|
||||
|
||||
|
||||
class _ExitedProcess:
|
||||
def __init__(self):
|
||||
self.stdin = open(os.devnull, "w")
|
||||
self.returncode = 0
|
||||
|
||||
def poll(self):
|
||||
return self.returncode
|
||||
|
||||
def wait(self, timeout=None):
|
||||
return self.returncode
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_virtual_display(monkeypatch):
|
||||
_FakeVirtualDisplay.instances = []
|
||||
monkeypatch.setattr(server, "VirtualDisplay", _FakeVirtualDisplay, raising=False)
|
||||
monkeypatch.setattr(server, "get_nodejs", lambda: "/node")
|
||||
return _FakeVirtualDisplay.instances
|
||||
|
||||
|
||||
def test_launch_server_runs_virtual_headless_on_a_virtual_display(
|
||||
monkeypatch, fake_virtual_display
|
||||
):
|
||||
# headless='virtual' is a Camoufox() option, not a Playwright one: the
|
||||
# server must start Xvfb, launch headful on it, and kill it when the
|
||||
# server process exits.
|
||||
launched = {}
|
||||
|
||||
def fake_launch_options(**kwargs):
|
||||
launched.update(kwargs)
|
||||
return {}
|
||||
|
||||
monkeypatch.setattr(server, "launch_options", fake_launch_options)
|
||||
monkeypatch.setattr(
|
||||
server.subprocess, "Popen", lambda *args, **kwargs: _ExitedProcess()
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
server.launch_server(headless="virtual")
|
||||
|
||||
assert len(fake_virtual_display) == 1
|
||||
assert launched["headless"] is False
|
||||
assert launched["virtual_display"] == ":99"
|
||||
assert fake_virtual_display[0].killed
|
||||
|
||||
|
||||
def test_launch_server_kills_virtual_display_when_launch_fails(
|
||||
monkeypatch, fake_virtual_display
|
||||
):
|
||||
def failing_launch_options(**kwargs):
|
||||
raise ValueError("invalid options")
|
||||
|
||||
monkeypatch.setattr(server, "launch_options", failing_launch_options)
|
||||
|
||||
with pytest.raises(ValueError, match="invalid options"):
|
||||
server.launch_server(headless="virtual")
|
||||
|
||||
assert len(fake_virtual_display) == 1
|
||||
assert fake_virtual_display[0].killed
|
||||
|
||||
Reference in New Issue
Block a user