Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libxdamage1 libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 \
libcairo2 libasound2 libx11-xcb1 libfontconfig1 libx11-6 \
libxcb1 libxext6 libxshmfence1 \
libglvnd0 libegl1 libgles2 libglx0 libopengl0 \
libglib2.0-0 libgtk-3-0 libpangocairo-1.0-0 libcairo-gobject2 \
libgdk-pixbuf-2.0-0 libxss1 libxtst6 fonts-liberation \
fonts-noto-color-emoji fonts-unifont fonts-freefont-ttf \
Expand Down
29 changes: 25 additions & 4 deletions cloakbrowser/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ def launch(
human_preset: HumanPreset = "default",
human_config: HumanConfigOverrides | None = None,
extension_paths: list[str] | None = None,
gpu_accel: bool = False,
license_key: str | None = None,
browser_version: str | None = None,
release_channel: str | None = None,
Expand Down Expand Up @@ -226,7 +227,7 @@ def launch(
args = _resolve_webrtc_args(args, proxy)
args = _append_webrtc_exit_ip(args, exit_ip)

chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths, start_maximized=binary_supports_maximized_window(license_key, browser_version, release_channel) and not _suppress_maximize)
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths, gpu_accel=gpu_accel, start_maximized=binary_supports_maximized_window(license_key, browser_version, release_channel) and not _suppress_maximize)
_maybe_warn_windows_fonts(chrome_args)

logger.debug("Launching stealth Chromium (headless=%s, args=%d)", headless, len(chrome_args))
Expand Down Expand Up @@ -298,6 +299,7 @@ async def launch_async( # noqa: C901
human_preset: HumanPreset = "default",
human_config: HumanConfigOverrides | None = None,
extension_paths: list[str] | None = None,
gpu_accel: bool = False,
license_key: str | None = None,
browser_version: str | None = None,
release_channel: str | None = None,
Expand Down Expand Up @@ -345,7 +347,7 @@ async def launch_async( # noqa: C901
proxy_kwargs, proxy_extra_args = _resolve_proxy_config(proxy, browser_version, license_key, release_channel)
args = _resolve_webrtc_args(args, proxy)
args = _append_webrtc_exit_ip(args, exit_ip)
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths, start_maximized=binary_supports_maximized_window(license_key, browser_version, release_channel) and not _suppress_maximize)
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths, gpu_accel=gpu_accel, start_maximized=binary_supports_maximized_window(license_key, browser_version, release_channel) and not _suppress_maximize)
_maybe_warn_windows_fonts(chrome_args)

logger.debug("Launching stealth Chromium async (headless=%s, args=%d)", headless, len(chrome_args))
Expand Down Expand Up @@ -419,6 +421,7 @@ def launch_persistent_context(
human_preset: HumanPreset = "default",
human_config: HumanConfigOverrides | None = None,
extension_paths: list[str] | None = None,
gpu_accel: bool = False,
license_key: str | None = None,
browser_version: str | None = None,
release_channel: str | None = None,
Expand Down Expand Up @@ -475,7 +478,7 @@ def launch_persistent_context(
proxy_kwargs, proxy_extra_args = _resolve_proxy_config(proxy, browser_version, license_key, release_channel)
args = _resolve_webrtc_args(args, proxy)
args = _append_webrtc_exit_ip(args, exit_ip)
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths, start_maximized=binary_supports_maximized_window(license_key, browser_version, release_channel) and viewport is _VIEWPORT_UNSET and "viewport" not in kwargs and "no_viewport" not in kwargs)
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths, gpu_accel=gpu_accel, start_maximized=binary_supports_maximized_window(license_key, browser_version, release_channel) and viewport is _VIEWPORT_UNSET and "viewport" not in kwargs and "no_viewport" not in kwargs)
_maybe_warn_windows_fonts(chrome_args)

logger.debug(
Expand Down Expand Up @@ -568,6 +571,7 @@ async def launch_persistent_context_async(
human_preset: HumanPreset = "default",
human_config: HumanConfigOverrides | None = None,
extension_paths: list[str] | None = None,
gpu_accel: bool = False,
license_key: str | None = None,
browser_version: str | None = None,
release_channel: str | None = None,
Expand Down Expand Up @@ -626,7 +630,7 @@ async def launch_persistent_context_async(
proxy_kwargs, proxy_extra_args = _resolve_proxy_config(proxy, browser_version, license_key, release_channel)
args = _resolve_webrtc_args(args, proxy)
args = _append_webrtc_exit_ip(args, exit_ip)
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths, start_maximized=binary_supports_maximized_window(license_key, browser_version, release_channel) and viewport is _VIEWPORT_UNSET and "viewport" not in kwargs and "no_viewport" not in kwargs)
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths, gpu_accel=gpu_accel, start_maximized=binary_supports_maximized_window(license_key, browser_version, release_channel) and viewport is _VIEWPORT_UNSET and "viewport" not in kwargs and "no_viewport" not in kwargs)
_maybe_warn_windows_fonts(chrome_args)

logger.debug(
Expand Down Expand Up @@ -718,6 +722,7 @@ def launch_context(
human_preset: HumanPreset = "default",
human_config: HumanConfigOverrides | None = None,
extension_paths: list[str] | None = None,
gpu_accel: bool = False,
license_key: str | None = None,
browser_version: str | None = None,
release_channel: str | None = None,
Expand Down Expand Up @@ -764,6 +769,7 @@ def launch_context(
# locale and timezone are set via binary flags only — no CDP emulation.
browser = launch(headless=headless, proxy=proxy, args=args, stealth_args=stealth_args,
timezone=timezone, locale=locale, extension_paths=extension_paths,
gpu_accel=gpu_accel,
license_key=license_key, browser_version=browser_version,
release_channel=release_channel,
# Caller chose a viewport geometry → don't also auto-maximize
Expand Down Expand Up @@ -825,6 +831,7 @@ async def launch_context_async(
human_preset: HumanPreset = "default",
human_config: HumanConfigOverrides | None = None,
extension_paths: list[str] | None = None,
gpu_accel: bool = False,
license_key: str | None = None,
browser_version: str | None = None,
release_channel: str | None = None,
Expand Down Expand Up @@ -890,6 +897,7 @@ async def launch_context_async(
# locale and timezone are set via binary flags only — no CDP emulation.
browser = await launch_async(headless=headless, proxy=proxy, args=args, stealth_args=stealth_args,
timezone=timezone, locale=locale, extension_paths=extension_paths,
gpu_accel=gpu_accel,
license_key=license_key, browser_version=browser_version,
release_channel=release_channel,
# Caller chose a viewport geometry → don't also auto-maximize
Expand Down Expand Up @@ -1192,6 +1200,7 @@ def build_args(
locale: str | None = None,
headless: bool = True,
extension_paths: list[str] | None = None,
gpu_accel: bool = False,
start_maximized: bool = False,
) -> list[str]:
"""Combine stealth args with user-provided args and locale flags.
Expand All @@ -1215,6 +1224,18 @@ def build_args(
if not headless or _platform.system() == "Windows":
seen["--ignore-gpu-blocklist"] = "--ignore-gpu-blocklist"

# Opt-in Docker GPU acceleration (gpu_accel=True / CLOAKBROWSER_GPU_ACCEL=1).
# ANGLE over native EGL (--use-angle=gl-egl) is what actually engages the GPU on
# modern Chromium; the legacy --use-gl=egl value was removed and silently
# disables WebGL. Needs the GLVND EGL loader present (Docker image ships it).
if gpu_accel or os.environ.get("CLOAKBROWSER_GPU_ACCEL"):
seen["--use-gl"] = "--use-gl=angle"
seen["--use-angle"] = "--use-angle=gl-egl"
seen["--enable-gpu-rasterization"] = "--enable-gpu-rasterization"
seen["--ignore-gpu-blocklist"] = "--ignore-gpu-blocklist"
if sys.platform.startswith("linux"):
seen["--enable-features"] = "--enable-features=VaapiVideoDecoder"

if extra_args:
for arg in extra_args:
key = arg.split("=", 1)[0]
Expand Down
34 changes: 34 additions & 0 deletions examples/docker-compose.gpu.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Opt-in GPU acceleration for CloakBrowser in Docker. Requires the NVIDIA
# Container Toolkit on the host so the container can see the GPU.
#
# CLOAKBROWSER_GPU_ACCEL=1 adds the ANGLE/EGL + GPU-rasterization + VA-API flags.
# GPU acceleration engages for HEADLESS Chrome launched via the library API
# (Python launch(gpu_accel=True) / JS launch({ gpuAccel: true })), which uses a
# surfaceless EGL context — verify with chrome://gpu or CDP SystemInfo.getInfo
# (expect your GPU's renderer, e.g. "ANGLE (NVIDIA ... OpenGL ES ...)", not
# SwiftShader/llvmpipe). Point `command` at your own headless workload; the
# cloakserve line below is only a long-running placeholder for the healthcheck.
services:
cloakbrowser:
image: cloakhq/cloakbrowser
command: cloakserve
restart: unless-stopped
environment:
CLOAKBROWSER_GPU_ACCEL: "1"
# Expose the graphics + video driver libs to the container (not just compute).
NVIDIA_DRIVER_CAPABILITIES: all
ports:
- "127.0.0.1:9222:9222"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9222/json/version"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
12 changes: 12 additions & 0 deletions js/src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ export function buildArgs(options: LaunchOptions): string[] {
if (options.headless === false || process.platform === "win32") {
seen.set("--ignore-gpu-blocklist", "--ignore-gpu-blocklist");
}
// Opt-in Docker GPU acceleration (gpuAccel: true / CLOAKBROWSER_GPU_ACCEL=1).
// ANGLE over native EGL (--use-angle=gl-egl) engages the real GPU on modern
// Chromium; the legacy --use-gl=egl value was removed and silently disables WebGL.
if (options.gpuAccel || process.env.CLOAKBROWSER_GPU_ACCEL) {
seen.set("--use-gl", "--use-gl=angle");
seen.set("--use-angle", "--use-angle=gl-egl");
seen.set("--enable-gpu-rasterization", "--enable-gpu-rasterization");
seen.set("--ignore-gpu-blocklist", "--ignore-gpu-blocklist");
if (process.platform === "linux") {
seen.set("--enable-features", "--enable-features=VaapiVideoDecoder");
}
}
if (options.args) {
for (const arg of options.args) {
const key = arg.split("=")[0];
Expand Down
2 changes: 2 additions & 0 deletions js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export interface LaunchOptions {
locale?: string;
/** Auto-detect timezone/locale from proxy IP (requires: npm install mmdb-lib). */
geoip?: boolean;
/** Opt-in Docker GPU acceleration flags. Also reads CLOAKBROWSER_GPU_ACCEL=1. */
gpuAccel?: boolean;
/** Pro license key. Also reads from CLOAKBROWSER_LICENSE_KEY env var. */
licenseKey?: string;
/** Exact Chromium version pin. Also reads from CLOAKBROWSER_VERSION env var. */
Expand Down
49 changes: 48 additions & 1 deletion js/tests/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect } from "vitest";
import { afterEach, describe, it, expect } from "vitest";
import {
CHROMIUM_VERSION,
getArchiveExt,
Expand All @@ -15,6 +15,53 @@ import {
} from "../src/config.js";
import { _buildArgsForTest, resolveTimezone } from "../src/playwright.js";

describe("buildArgs GPU acceleration (issue #189)", () => {
const origPlatform = Object.getOwnPropertyDescriptor(process, "platform")!;
const origEnv = process.env.CLOAKBROWSER_GPU_ACCEL;
const setPlatform = (p: NodeJS.Platform) =>
Object.defineProperty(process, "platform", { ...origPlatform, value: p });
afterEach(() => {
Object.defineProperty(process, "platform", origPlatform);
if (origEnv === undefined) delete process.env.CLOAKBROWSER_GPU_ACCEL;
else process.env.CLOAKBROWSER_GPU_ACCEL = origEnv;
});

it("leaves GPU flags off by default", () => {
delete process.env.CLOAKBROWSER_GPU_ACCEL;
const args = _buildArgsForTest({});
expect(args).not.toContain("--use-angle=gl-egl");
expect(args).not.toContain("--enable-gpu-rasterization");
});

it("CLOAKBROWSER_GPU_ACCEL adds ANGLE-over-EGL flags (not the removed use-gl=egl)", () => {
process.env.CLOAKBROWSER_GPU_ACCEL = "1";
setPlatform("linux");
const args = _buildArgsForTest({});
expect(args).toContain("--use-gl=angle");
expect(args).toContain("--use-angle=gl-egl");
expect(args).not.toContain("--use-gl=egl");
expect(args).toContain("--enable-gpu-rasterization");
expect(args).toContain("--ignore-gpu-blocklist");
expect(args).toContain("--enable-features=VaapiVideoDecoder");
});

it("skips VaapiVideoDecoder off Linux", () => {
process.env.CLOAKBROWSER_GPU_ACCEL = "1";
setPlatform("darwin");
const args = _buildArgsForTest({});
expect(args).toContain("--use-angle=gl-egl");
expect(args).not.toContain("--enable-features=VaapiVideoDecoder");
});

it("gpuAccel option works without the environment variable", () => {
delete process.env.CLOAKBROWSER_GPU_ACCEL;
setPlatform("linux");
const args = _buildArgsForTest({ gpuAccel: true });
expect(args).toContain("--use-angle=gl-egl");
expect(args).toContain("--enable-features=VaapiVideoDecoder");
});
});

describe("config", () => {
it("CHROMIUM_VERSION matches expected format", () => {
expect(CHROMIUM_VERSION).toMatch(/^\d+\.\d+\.\d+\.\d+(\.\d+)?$/);
Expand Down
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ def isolated_cache_dir(tmp_path_factory, monkeypatch):
"CLOAKBROWSER_CACHE_DIR",
str(tmp_path_factory.mktemp("cloakbrowser-cache")),
)


@pytest.fixture(autouse=True)
def _clear_gpu_accel_env(monkeypatch):
"""A host-level CLOAKBROWSER_GPU_ACCEL must not leak into build_args tests."""
monkeypatch.delenv("CLOAKBROWSER_GPU_ACCEL", raising=False)
46 changes: 46 additions & 0 deletions tests/test_build_args.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Unit tests for build_args timezone/locale injection and timezone alias."""

import sys

from cloakbrowser.browser import build_args, _resolve_timezone


Expand Down Expand Up @@ -151,6 +153,50 @@ def test_non_value_flags_preserved():
assert "--no-sandbox" in args


# --- GPU acceleration (issue #189) ---


def test_gpu_accel_default_off(monkeypatch):
"""GPU flags stay off unless explicitly enabled."""
monkeypatch.delenv("CLOAKBROWSER_GPU_ACCEL", raising=False)
args = build_args(stealth_args=True, extra_args=None)
assert "--use-angle=gl-egl" not in args
assert "--enable-gpu-rasterization" not in args
assert "--enable-features=VaapiVideoDecoder" not in args


def test_gpu_accel_env_adds_linux_flags(monkeypatch):
"""CLOAKBROWSER_GPU_ACCEL enables ANGLE-over-EGL GPU flags (+ Vaapi on Linux)."""
monkeypatch.setenv("CLOAKBROWSER_GPU_ACCEL", "1")
monkeypatch.setattr(sys, "platform", "linux")
args = build_args(stealth_args=True, extra_args=None)
assert "--use-gl=angle" in args
assert "--use-angle=gl-egl" in args
assert "--use-gl=egl" not in args # obsolete value must never reappear (#189)
assert "--enable-gpu-rasterization" in args
assert "--ignore-gpu-blocklist" in args
assert "--enable-features=VaapiVideoDecoder" in args


def test_gpu_accel_skips_vaapi_off_linux(monkeypatch):
"""VaapiVideoDecoder is Linux-only; the ANGLE flags still apply elsewhere."""
monkeypatch.setenv("CLOAKBROWSER_GPU_ACCEL", "1")
monkeypatch.setattr(sys, "platform", "darwin")
args = build_args(stealth_args=True, extra_args=None)
assert "--use-angle=gl-egl" in args
assert "--enable-features=VaapiVideoDecoder" not in args


def test_gpu_accel_kwarg_adds_flags(monkeypatch):
"""gpu_accel=True enables the GPU flags without the environment variable."""
monkeypatch.delenv("CLOAKBROWSER_GPU_ACCEL", raising=False)
monkeypatch.setattr(sys, "platform", "linux")
args = build_args(stealth_args=True, extra_args=None, gpu_accel=True)
assert "--use-gl=angle" in args
assert "--use-angle=gl-egl" in args
assert "--enable-features=VaapiVideoDecoder" in args


def test_override_logs_debug(caplog):
"""Should log debug message when an override happens."""
import logging
Expand Down
27 changes: 27 additions & 0 deletions tests/test_launch_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,33 @@ def test_release_channel_forwarded(mock_launch, _mock_bin):
assert mock_launch.call_args.kwargs["release_channel"] == "preview"


@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser.launch")
def test_gpu_accel_forwarded_to_launch(mock_launch, _mock_bin):
"""launch_context(gpu_accel=True) forwards to launch(), not new_context()."""
browser, _ = _make_mock_browser()
mock_launch.return_value = browser

from cloakbrowser.browser import launch_context
launch_context(gpu_accel=True)

assert mock_launch.call_args.kwargs.get("gpu_accel") is True
assert "gpu_accel" not in browser.new_context.call_args.kwargs


@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser.launch")
def test_gpu_accel_default_false(mock_launch, _mock_bin):
"""launch_context() defaults gpu_accel=False into launch()."""
browser, _ = _make_mock_browser()
mock_launch.return_value = browser

from cloakbrowser.browser import launch_context
launch_context()

assert mock_launch.call_args.kwargs.get("gpu_accel") is False


@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser.launch")
def test_headed_no_viewport(mock_launch, _mock_bin):
Expand Down