From befb85b99336b9fdaf13506300d3b3ea88401860 Mon Sep 17 00:00:00 2001 From: PaoloC68 Date: Wed, 8 Apr 2026 14:54:22 +0000 Subject: [PATCH 01/17] fix: software rendering, Stop/Start buttons, VNC browser icon, and watchdog Changes: - api/camofox_setup.py: Add software rendering env vars to _start_server() (LIBGL_ALWAYS_SOFTWARE=1, GALLIUM_DRIVER=llvmpipe, MOZ_WEBRENDER=0, MOZ_DISABLE_OOP_COMPOSITING=1, MOZ_DISABLE_RDD_SANDBOX=1) These vars are inherited by camoufox-bin via ...process.env spread. - api/camofox_health.py: Fix Stop/Start buttons with subprocess fallback when CamofoxCli is not installed. Stop uses pkill; Start uses direct node subprocess with software rendering env vars. Add asyncio import. - tools/camofox_session.py: Fix browser icon not showing active in Agent Zero UI. Node.js API does not return vncUrl in toggle-display response; construct fallback URL from config when mode is virtual. - scripts/vnc-watchdog.sh: New VNC watchdog script that monitors which display camoufox-bin is rendering on and auto-restarts x11vnc on the correct display. Handles display changes, crashes, and respawns. CamoFox spawns its own Xvfb displays (:100, :101) in virtual mode. --- api/camofox_health.py | 79 ++++++++++++++++++++++++++++++++++++++-- api/camofox_setup.py | 6 +++ scripts/vnc-watchdog.sh | 43 ++++++++++++++++++++++ tools/camofox_session.py | 10 ++++- 4 files changed, 133 insertions(+), 5 deletions(-) create mode 100755 scripts/vnc-watchdog.sh diff --git a/api/camofox_health.py b/api/camofox_health.py index 3f5944b..d02e767 100644 --- a/api/camofox_health.py +++ b/api/camofox_health.py @@ -1,5 +1,6 @@ """API handler: CamoFox server health check, start, and stop.""" +import asyncio import os import shutil @@ -151,6 +152,7 @@ async def _diagnose(self) -> dict: async def _server_control(self, action: str) -> dict: cfg = get_config() + # Try CamofoxCli first; fall back to direct subprocess control try: cli = CamofoxCli( binary_path=cfg.get("binary_path") or None, @@ -158,7 +160,76 @@ async def _server_control(self, action: str) -> dict: ) result = await cli.execute("server", action) return {"ok": True, "action": action, "result": result} - except CamofoxCliNotFoundError as e: - return {"ok": False, "error": str(e)} - except CamofoxCliError as e: - return {"ok": False, "error": str(e)} + except (CamofoxCliNotFoundError, CamofoxCliError): + pass # Fall through to subprocess fallback + except Exception: + pass + + # Subprocess fallback (no camofox CLI required) + if action == "stop": + try: + proc = await asyncio.create_subprocess_exec( + "pkill", "-f", "node.*server.js", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + await proc.communicate() + # Also kill any lingering camoufox-bin processes + proc2 = await asyncio.create_subprocess_exec( + "pkill", "-f", "camoufox-bin", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + await proc2.communicate() + return {"ok": True, "action": "stop", "result": "Server stopped via pkill."} + except Exception as e: + return {"ok": False, "error": f"Stop failed: {e}"} + + elif action == "start": + import shutil as _shutil + # Find server.js + server_js = None + try: + result = await asyncio.create_subprocess_exec( + "npm", "root", "-g", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, _ = await result.communicate() + if result.returncode == 0: + candidate = os.path.join(stdout.decode().strip(), "camofox-browser", "dist", "src", "server.js") + if os.path.isfile(candidate): + server_js = candidate + except Exception: + pass + + if not server_js: + return {"ok": False, "error": "server.js not found — cannot start server."} + + port = cfg.get("server_url", "http://localhost:9377").split(":")[-1].rstrip("/") + env = os.environ.copy() + env["CAMOFOX_PORT"] = str(port) + if cfg.get("api_key"): + env["CAMOFOX_API_KEY"] = cfg["api_key"] + if cfg.get("admin_key"): + env["CAMOFOX_ADMIN_KEY"] = cfg["admin_key"] + # Software rendering env vars + env["LIBGL_ALWAYS_SOFTWARE"] = "1" + env["GALLIUM_DRIVER"] = "llvmpipe" + env["MOZ_DISABLE_OOP_COMPOSITING"] = "1" + env["MOZ_WEBRENDER"] = "0" + env["MOZ_DISABLE_RDD_SANDBOX"] = "1" + + try: + await asyncio.create_subprocess_exec( + "node", server_js, + env=env, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + ) + await asyncio.sleep(4) + return {"ok": True, "action": "start", "result": "Server start command sent."} + except Exception as e: + return {"ok": False, "error": f"Start failed: {e}"} + + return {"ok": False, "error": f"Unknown action: {action!r}"} diff --git a/api/camofox_setup.py b/api/camofox_setup.py index a0108e8..0d9c65d 100644 --- a/api/camofox_setup.py +++ b/api/camofox_setup.py @@ -128,6 +128,12 @@ async def _start_server(self, input: dict) -> dict: env["CAMOFOX_API_KEY"] = api_key if admin_key: env["CAMOFOX_ADMIN_KEY"] = admin_key + # Software rendering — required for correct rendering in Xvfb virtual displays + env["LIBGL_ALWAYS_SOFTWARE"] = "1" + env["GALLIUM_DRIVER"] = "llvmpipe" + env["MOZ_DISABLE_OOP_COMPOSITING"] = "1" + env["MOZ_WEBRENDER"] = "0" + env["MOZ_DISABLE_RDD_SANDBOX"] = "1" # Find server.js from installed npm package server_js = self._find_server_js() diff --git a/scripts/vnc-watchdog.sh b/scripts/vnc-watchdog.sh new file mode 100755 index 0000000..edc5a66 --- /dev/null +++ b/scripts/vnc-watchdog.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# CamoFox VNC Watchdog +# Monitors camoufox-bin display and keeps x11vnc pointed at the correct one. +# Handles crashes, respawns, and display number changes. + +LAST_DISP="" + +echo "[watchdog] Started PID=$$" + +while true; do + # Detect current display used by camoufox-bin + DISP=$(for pid in $(pgrep -f 'camoufox-bin' 2>/dev/null); do + cat /proc/$pid/environ 2>/dev/null | tr '\0' '\n' | grep ^DISPLAY= + done | sort -u | head -1 | sed 's/DISPLAY=//') + + if [ -z "$DISP" ]; then + # camoufox-bin not running — kill stale x11vnc if any + if pgrep -f x11vnc > /dev/null 2>&1; then + echo "[watchdog] camoufox-bin gone, killing stale x11vnc" + pkill -f x11vnc 2>/dev/null + fi + LAST_DISP="" + sleep 3 + continue + fi + + X11VNC_RUNNING=$(pgrep -f x11vnc > /dev/null 2>&1 && echo yes || echo no) + X11VNC_DISP=$(pgrep -fa x11vnc 2>/dev/null | grep -o 'display :[0-9]*' | awk '{print $2}' | head -1) + + # Restart x11vnc if: not running, or watching wrong display + if [ "$X11VNC_RUNNING" = "no" ] || [ "$X11VNC_DISP" != "$DISP" ]; then + echo "[watchdog] $(date): x11vnc state=$X11VNC_RUNNING watching=$X11VNC_DISP target=$DISP — restarting" + pkill -f x11vnc 2>/dev/null + sleep 1 + nohup x11vnc -display $DISP -rfbport 5999 -nopw -forever -shared \ + -listen 127.0.0.1 -noxdamage >> /tmp/x11vnc.log 2>&1 & + echo "[watchdog] x11vnc started on $DISP (PID=$!)" + LAST_DISP="$DISP" + sleep 3 + fi + + sleep 3 +done diff --git a/tools/camofox_session.py b/tools/camofox_session.py index 1309279..b3fdb47 100644 --- a/tools/camofox_session.py +++ b/tools/camofox_session.py @@ -81,10 +81,18 @@ async def _dispatch(self, action: str, user_id: str) -> str: display_mode = "headless" elif mode == "virtual": display_mode = "virtual" + # Node.js server doesn't return vncUrl — construct it from config + if not vnc_url: + cfg = get_config() + server_url = cfg.get("server_url", "http://localhost:9377") + port = server_url.rstrip("/").split(":")[-1] + if not port.isdigit(): + port = "6080" + vnc_url = f"http://localhost:6080/vnc.html?autoconnect=true&resize=scale" else: display_mode = "headed" shared_state.set_vnc(user_id, vnc_url, display_mode) - if vnc_url: + if vnc_url and display_mode in ("virtual", "headed"): return ( f"Browser now visible via VNC. All previous tabs are invalidated — " f"create new tabs and snapshot before interacting.\nVNC URL: {vnc_url}" From 8e44c02fe42a320f57c5ac3115578c18de7e4977 Mon Sep 17 00:00:00 2001 From: PaoloC68 Date: Wed, 8 Apr 2026 15:35:00 +0000 Subject: [PATCH 02/17] docs: add user.js template and document Google OAuth / Xvfb popup fix - scripts/user.js.template: Firefox preferences that fix black popup rendering in Xvfb virtual displays. Disables GPU/WebRender acceleration and forces popup windows to open as tabs (browser.link.open_newwindow=3) which prevents Google OAuth and similar popups from opening as separate black windows. - README.md: Expanded with Virtual Display Mode section, Google OAuth fix documentation with step-by-step user.js instructions, software rendering env vars reference, xdotool popup close command, and Scripts reference. --- README.md | 99 ++++++++++++++++++++++++++++++++++++++++ scripts/user.js.template | 43 +++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 scripts/user.js.template diff --git a/README.md b/README.md index 5935d07..532e086 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,102 @@ Anti-detection browser integration for Agent Zero. Provides: - Optional: `camofox` CLI for auth vault and scripting - For visible browser mode: `xvfb`, `x11vnc`, `websockify`, and noVNC assets at `/opt/noVNC` - The embedded viewer now runs through the normal Agent Zero URL, so users do not need to publish extra VNC ports in Docker + +--- + +## Virtual Display Mode (Xvfb) + +When running in Docker or headless Linux environments, CamoFox can use a virtual display via Xvfb for the VNC panel. Key points: + +- CamoFox spawns its **own Xvfb displays** (`:100`, `:101`) in virtual mode — separate from any manually started display +- `x11vnc` must be pointed at the **CamoFox display**, not the base `:99` display +- The VNC watchdog script (`scripts/vnc-watchdog.sh`) handles this automatically + +### Startup Sequence + +```bash +# 1. Start base Xvfb display +nohup Xvfb :99 -screen 0 1920x1080x24 -ac -nolisten tcp > /tmp/xvfb.log 2>&1 & + +# 2. Start websockify +nohup /usr/bin/python3 /usr/bin/websockify --web /opt/noVNC 6080 127.0.0.1:5999 > /tmp/websockify.log 2>&1 & + +# 3. Start CamoFox server with software rendering env vars +DISPLAY=:99 LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe \ +MOZ_DISABLE_OOP_COMPOSITING=1 MOZ_WEBRENDER=0 MOZ_DISABLE_RDD_SANDBOX=1 \ +nohup node /usr/local/lib/node_modules/camofox-browser/dist/src/server.js > /tmp/camofox.log 2>&1 & + +# 4. Toggle to virtual mode via Agent Zero tool: camofox_session toggle_display headless=virtual + +# 5. Start VNC watchdog (auto-detects correct display, keeps x11vnc alive) +bash /path/to/camofox-plugin/scripts/vnc-watchdog.sh >> /tmp/camofox-watchdog.log 2>&1 & +``` + +--- + +## Google OAuth / Popup Windows in Xvfb + +When using Google OAuth ("Sign in with Google") or any site that opens a popup window, the popup may render as a **black screen** in Xvfb virtual display environments. This is caused by GPU-accelerated compositing being unavailable in headless environments. + +### Fix: Apply Firefox User Preferences + +A `user.js` template is included in `scripts/user.js.template`. Apply it to your CamoFox browser profile: + +```bash +# Find your profile directory +ls /root/.camofox/profiles/ + +# Copy the template (replace 'a0-agent-0' with your profile name) +cp scripts/user.js.template /root/.camofox/profiles/a0-agent-0/user.js + +# Restart the browser session to apply +``` + +This template: +- **Disables GPU/WebRender acceleration** — fixes black rendering in Xvfb +- **Forces popup windows to open as tabs** (`browser.link.open_newwindow=3`) — prevents Google OAuth and similar popups from opening as separate black windows; they open as normal tabs instead + +### Additional: Software Rendering Environment Variables + +The plugin's Start button (and the startup sequence above) automatically sets these environment variables on the CamoFox server process, which are inherited by the browser: + +``` +LIBGL_ALWAYS_SOFTWARE=1 +GALLIUM_DRIVER=llvmpipe +MOZ_DISABLE_OOP_COMPOSITING=1 +MOZ_WEBRENDER=0 +MOZ_DISABLE_RDD_SANDBOX=1 +``` + +### Closing a Stuck Black Popup + +If a black popup is already open and can't be closed through the browser UI: + +```bash +# Install xdotool if missing +apt-get install -y xdotool + +# Find and close popups on the CamoFox display +for wid in $(DISPLAY=:100 xdotool search --name '' 2>/dev/null); do + name=$(DISPLAY=:100 xdotool getwindowname $wid 2>/dev/null) + if echo "$name" | grep -qi 'google\|sign in'; then + DISPLAY=:100 xdotool windowclose $wid 2>/dev/null && echo "Closed: $name" + fi +done +``` + +--- + +## Scripts + +### `scripts/vnc-watchdog.sh` + +A background watchdog that monitors which display `camoufox-bin` is rendering on and auto-restarts `x11vnc` on the correct display whenever it crashes or changes. Run it after toggling to virtual display mode: + +```bash +bash scripts/vnc-watchdog.sh >> /tmp/camofox-watchdog.log 2>&1 & +``` + +### `scripts/user.js.template` + +Firefox user preferences template for fixing popup rendering in Xvfb environments. See [Google OAuth / Popup Windows in Xvfb](#google-oauth--popup-windows-in-xvfb) above. diff --git a/scripts/user.js.template b/scripts/user.js.template new file mode 100644 index 0000000..0fc12c8 --- /dev/null +++ b/scripts/user.js.template @@ -0,0 +1,43 @@ +// CamoFox Firefox User Preferences Template +// Apply to: /root/.camofox/profiles//user.js +// +// WHAT THIS FIXES: +// When running CamoFox in virtual display mode (Xvfb), Google OAuth and other +// popup windows render as a black screen. This is caused by GPU-accelerated +// compositing being unavailable in headless/virtual environments. +// +// HOW TO APPLY: +// 1. Find your profile directory: +// ls /root/.camofox/profiles/ +// 2. Copy this file: +// cp scripts/user.js.template /root/.camofox/profiles/a0-agent-0/user.js +// (Replace 'a0-agent-0' with your actual profile directory name) +// 3. Restart the CamoFox browser session for prefs to take effect. +// +// NOTE: These prefs are written to user.js which is re-applied on every Firefox +// startup, overriding any conflicting prefs.js settings. + +// --- Disable GPU / hardware acceleration --- +user_pref("gfx.webrender.all", false); +user_pref("gfx.webrender.software", true); +user_pref("gfx.webrender.enabled", false); +user_pref("gfx.webrender.force-disabled", true); +user_pref("layers.acceleration.disabled", true); +user_pref("layers.acceleration.force-enabled", false); +user_pref("layers.gpu-process.enabled", false); +user_pref("layers.offmainthreadcomposition.enabled", false); +user_pref("gfx.canvas.azure.backends", "cairo"); +user_pref("gfx.content.azure.backends", "cairo"); +user_pref("gfx.direct2d.disabled", true); + +// --- Disable media and sandbox processes (avoid rendering isolation issues) --- +user_pref("media.rdd-process.enabled", false); +user_pref("security.sandbox.content.level", 0); +user_pref("dom.ipc.sandbox.content.level", 0); + +// --- Force popup windows to open as tabs --- +// This prevents Google OAuth and similar popup windows from opening as +// separate browser windows that render black in virtual displays. +// Instead, they open as new tabs in the same window — fully visible in VNC. +user_pref("browser.link.open_newwindow", 3); +user_pref("browser.link.open_newwindow.restriction", 0); From d8e52f13a7c9634af27738152dc6c380e3321613 Mon Sep 17 00:00:00 2001 From: PaoloC68 Date: Wed, 8 Apr 2026 17:35:46 +0000 Subject: [PATCH 03/17] feat: proper full-stack lifecycle management for Start/Stop/Restart buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces patched individual handlers with a complete solution: - api/camofox_startup.py (NEW): Full CamoFox+VNC stack lifecycle in one API handler (action: start/stop/restart/status). Start does everything: 1. Ensure Xvfb :99 is running 2. Ensure websockify is running 3. Start CamoFox Node.js server with software rendering env vars 4. Toggle browser to virtual display mode 5. Start VNC watchdog (auto-detects camoufox-bin display, starts x11vnc) Stop kills all components (watchdog + x11vnc + camoufox-bin + server). Each step returns ok/msg for status display in the UI. - webui/config.html: Updated Start/Stop/Restart buttons to call the new camofox_startup endpoint. Start button now shows step-by-step results (e.g. 'xvfb:✓ websockify:✓ server:✓ virtual_mode:✓ watchdog:✓'). Added Restart button. Status message updates inline so users know what is happening without needing to check logs. --- api/camofox_startup.py | 342 +++++++++++++++++++++++++++++++++++++++++ webui/config.html | 71 +++++++-- 2 files changed, 397 insertions(+), 16 deletions(-) create mode 100644 api/camofox_startup.py diff --git a/api/camofox_startup.py b/api/camofox_startup.py new file mode 100644 index 0000000..cff18c7 --- /dev/null +++ b/api/camofox_startup.py @@ -0,0 +1,342 @@ +"""API handler: CamoFox complete stack startup and shutdown. + +This handler manages the FULL CamoFox+VNC stack lifecycle: + - Start: Node.js server + virtual display toggle + x11vnc watchdog + - Stop: kill server + browser + x11vnc + watchdog + - Status: comprehensive health of all stack components + +This is the proper solution vs. patching individual pieces. +""" + +import asyncio +import os +import shutil +import subprocess +from pathlib import Path + +from helpers.api import ApiHandler, Request +from usr.plugins.camofox_browser.helpers.config import get_config, normalize_headless_mode +from usr.plugins.camofox_browser.helpers.client import CamofoxClient, CamofoxConnectionError + + +SERVER_JS_SEARCH = [ + "/usr/local/lib/node_modules/camofox-browser/dist/src/server.js", + "/usr/lib/node_modules/camofox-browser/dist/src/server.js", +] + +SOFTWARE_RENDERING_ENV = { + "LIBGL_ALWAYS_SOFTWARE": "1", + "GALLIUM_DRIVER": "llvmpipe", + "MOZ_DISABLE_OOP_COMPOSITING": "1", + "MOZ_WEBRENDER": "0", + "MOZ_DISABLE_RDD_SANDBOX": "1", + "DISPLAY": ":99", +} + +WATCHDOG_SCRIPT = str(Path(__file__).parent.parent / "scripts" / "vnc-watchdog.sh") + + +class CamofoxStartup(ApiHandler): + """Full CamoFox+VNC stack lifecycle management. + + Input: {"action": "start" | "stop" | "status" | "restart"} + """ + + @classmethod + def requires_auth(cls) -> bool: + return True + + async def process(self, input: dict, request: Request) -> dict: + action = input.get("action", "status") + + if action == "start": + return await self._full_start() + elif action == "stop": + return await self._full_stop() + elif action == "restart": + await self._full_stop() + await asyncio.sleep(2) + return await self._full_start() + elif action == "status": + return await self._stack_status() + else: + return {"ok": False, "error": f"Unknown action: {action!r}"} + + def _find_server_js(self) -> str | None: + for path in SERVER_JS_SEARCH: + if os.path.isfile(path): + return path + # Try npm root -g + try: + result = subprocess.run( + ["npm", "root", "-g"], capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0: + path = os.path.join( + result.stdout.strip(), "camofox-browser", "dist", "src", "server.js" + ) + if os.path.isfile(path): + return path + except Exception: + pass + return None + + def _is_server_running(self) -> bool: + return bool(subprocess.run( + ["pgrep", "-f", "node.*server.js"], + capture_output=True + ).returncode == 0) + + def _is_watchdog_running(self) -> bool: + return bool(subprocess.run( + ["pgrep", "-f", "vnc-watchdog"], + capture_output=True + ).returncode == 0) + + def _is_x11vnc_running(self) -> bool: + return bool(subprocess.run( + ["pgrep", "-f", "x11vnc"], + capture_output=True + ).returncode == 0) + + def _is_xvfb_running(self) -> bool: + return bool(subprocess.run( + ["pgrep", "-f", "Xvfb"], + capture_output=True + ).returncode == 0) + + def _is_websockify_running(self) -> bool: + return bool(subprocess.run( + ["pgrep", "-f", "websockify"], + capture_output=True + ).returncode == 0) + + async def _ensure_xvfb(self) -> bool: + """Ensure Xvfb :99 is running.""" + if self._is_xvfb_running(): + return True + try: + await asyncio.create_subprocess_exec( + "Xvfb", ":99", "-screen", "0", "1920x1080x24", "-ac", "-nolisten", "tcp", + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + ) + await asyncio.sleep(2) + return self._is_xvfb_running() + except Exception: + return False + + async def _ensure_websockify(self) -> bool: + """Ensure websockify is running on port 6080.""" + if self._is_websockify_running(): + return True + novnc = "/opt/noVNC" + if not os.path.isdir(novnc): + return False + websockify = shutil.which("websockify") + if not websockify: + return False + try: + await asyncio.create_subprocess_exec( + "python3", websockify, "--web", novnc, "6080", "127.0.0.1:5999", + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + ) + await asyncio.sleep(1) + return self._is_websockify_running() + except Exception: + return False + + async def _ensure_server(self) -> bool: + """Ensure CamoFox Node.js server is running with software rendering.""" + if self._is_server_running(): + return True + server_js = self._find_server_js() + if not server_js: + return False + cfg = get_config() + env = os.environ.copy() + env.update(SOFTWARE_RENDERING_ENV) + port = cfg.get("server_url", "http://localhost:9377").split(":")[-1].rstrip("/") + env["CAMOFOX_PORT"] = str(port) + env["CAMOFOX_HEADLESS"] = normalize_headless_mode(cfg.get("default_headless", True)) + if cfg.get("api_key"): + env["CAMOFOX_API_KEY"] = cfg["api_key"] + if cfg.get("admin_key"): + env["CAMOFOX_ADMIN_KEY"] = cfg["admin_key"] + try: + await asyncio.create_subprocess_exec( + "node", server_js, + env=env, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + ) + # Wait for server to be ready + for _ in range(10): + await asyncio.sleep(1) + try: + cfg2 = get_config() + client = CamofoxClient( + base_url=cfg2["server_url"], + api_key=cfg2.get("api_key", ""), + admin_key=cfg2.get("admin_key", ""), + ) + await client.get("/health") + await client.close() + return True + except Exception: + pass + except Exception: + pass + return False + + async def _toggle_to_virtual(self) -> bool: + """Force the browser into virtual display mode via the Node.js API.""" + cfg = get_config() + client = CamofoxClient( + base_url=cfg["server_url"], + api_key=cfg.get("api_key", ""), + admin_key=cfg.get("admin_key", ""), + ) + try: + # Step 1: Force headless to sync toggle state + await client.post("/sessions/a0-agent-0/toggle-display", data={"headless": True}) + await asyncio.sleep(1) + # Step 2: Toggle to virtual + result = await client.post("/sessions/a0-agent-0/toggle-display", data={"headless": "virtual"}) + await asyncio.sleep(4) # Wait for camoufox-bin to spawn + return True + except Exception: + return False + finally: + await client.close() + + async def _start_watchdog(self) -> bool: + """Start the VNC watchdog script.""" + if self._is_watchdog_running(): + return True + if not os.path.isfile(WATCHDOG_SCRIPT): + return False + try: + await asyncio.create_subprocess_exec( + "bash", WATCHDOG_SCRIPT, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + ) + await asyncio.sleep(5) # Give watchdog time to detect display and start x11vnc + return True + except Exception: + return False + + async def _full_start(self) -> dict: + """Start the complete CamoFox+VNC stack.""" + steps = {} + + # 1. Xvfb + xvfb_ok = await self._ensure_xvfb() + steps["xvfb"] = {"ok": xvfb_ok, "msg": "running" if xvfb_ok else "failed to start"} + + # 2. Websockify + ws_ok = await self._ensure_websockify() + steps["websockify"] = {"ok": ws_ok, "msg": "running" if ws_ok else "failed or not installed"} + + # 3. CamoFox Node.js server with software rendering + server_ok = await self._ensure_server() + steps["server"] = {"ok": server_ok, "msg": "running" if server_ok else "failed to start"} + + if not server_ok: + return { + "ok": False, + "error": "CamoFox server failed to start. Check that Node.js and camofox-browser npm package are installed.", + "steps": steps, + } + + # 4. Toggle to virtual display mode + virtual_ok = await self._toggle_to_virtual() + steps["virtual_mode"] = {"ok": virtual_ok, "msg": "active" if virtual_ok else "failed to toggle"} + + # 5. Start watchdog (auto-detects CamoFox display and starts x11vnc) + watchdog_ok = await self._start_watchdog() + steps["watchdog"] = {"ok": watchdog_ok, "msg": "running" if watchdog_ok else "not started"} + steps["x11vnc"] = {"ok": self._is_x11vnc_running(), "msg": "running" if self._is_x11vnc_running() else "starting..."} + + all_ok = server_ok and virtual_ok + return { + "ok": all_ok, + "message": "CamoFox VNC stack started successfully." if all_ok else "Stack started with warnings — check steps.", + "vnc_url": "http://localhost:6080/vnc.html?autoconnect=true&resize=scale", + "steps": steps, + } + + async def _full_stop(self) -> dict: + """Stop all CamoFox+VNC stack components.""" + stopped = [] + + # Kill watchdog + r = await asyncio.create_subprocess_exec( + "pkill", "-f", "vnc-watchdog", + stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL + ) + await r.communicate() + stopped.append("watchdog") + + # Kill x11vnc + r = await asyncio.create_subprocess_exec( + "pkill", "-f", "x11vnc", + stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL + ) + await r.communicate() + stopped.append("x11vnc") + + # Kill camoufox-bin + r = await asyncio.create_subprocess_exec( + "pkill", "-f", "camoufox-bin", + stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL + ) + await r.communicate() + stopped.append("camoufox-bin") + + # Kill Node.js server + r = await asyncio.create_subprocess_exec( + "pkill", "-f", "node.*server.js", + stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL + ) + await r.communicate() + stopped.append("server") + + await asyncio.sleep(1) + return { + "ok": True, + "message": "CamoFox VNC stack stopped.", + "stopped": stopped, + } + + async def _stack_status(self) -> dict: + """Return status of all stack components.""" + cfg = get_config() + server_health = None + browser_connected = False + try: + client = CamofoxClient( + base_url=cfg["server_url"], + api_key=cfg.get("api_key", ""), + admin_key=cfg.get("admin_key", ""), + ) + server_health = await client.get("/health") + browser_connected = server_health.get("browserConnected", False) + await client.close() + except Exception: + pass + + return { + "ok": True, + "components": { + "xvfb": self._is_xvfb_running(), + "websockify": self._is_websockify_running(), + "server": server_health is not None, + "browser_connected": browser_connected, + "x11vnc": self._is_x11vnc_running(), + "watchdog": self._is_watchdog_running(), + }, + "server_health": server_health, + } diff --git a/webui/config.html b/webui/config.html index 9c78907..aadf055 100644 --- a/webui/config.html +++ b/webui/config.html @@ -272,26 +272,65 @@
-
Start / Stop Server
-
Manually control the CamoFox server process
+
Start / Stop / Restart
+
-
- - + From dc7ea77b5721bc4cf37819a8aebcbea3ed07144a Mon Sep 17 00:00:00 2001 From: PaoloC68 Date: Wed, 8 Apr 2026 17:40:38 +0000 Subject: [PATCH 04/17] fix: change default display mode from headless to virtual MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Headless was the default, making the VNC panel non-functional out of the box — extremely counter-intuitive for a plugin whose main feature IS the visual browser. Virtual mode (VNC viewable) is now the default. Users who specifically want headless for performance can change it in the Config screen. --- default_config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default_config.yaml b/default_config.yaml index 0f569b9..0e5b20b 100644 --- a/default_config.yaml +++ b/default_config.yaml @@ -2,6 +2,6 @@ server_url: "http://localhost:9377" api_key: "" admin_key: "" default_user_id: "" -default_headless: true +default_headless: "virtual" default_geo_preset: "" auto_start_server: true From 937b346c1b6968fe8f6feb305c64834c94d87a4a Mon Sep 17 00:00:00 2001 From: PaoloC68 Date: Wed, 8 Apr 2026 17:45:57 +0000 Subject: [PATCH 05/17] fix: show live current display mode separately from startup preference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The config dropdown showed the SAVED PREFERENCE (config.default_headless) but the ACTUAL LIVE RUNTIME MODE can be different (changed by toggle at runtime). This was deeply confusing — users saw 'Headless' selected even when the browser was actually running in virtual/VNC mode. Fix: Add a live mode indicator below the dropdown that reads directly from the Alpine.js camofox store (store.displayMode), which reflects the actual running state. Also shows a warning when headless is active (VNC panel will not show browser content) and a confirmation when virtual is active. The dropdown is now clearly labeled '(for next startup)' and the description explains it does not change the current running session. --- webui/config.html | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/webui/config.html b/webui/config.html index aadf055..dd46e74 100644 --- a/webui/config.html +++ b/webui/config.html @@ -222,15 +222,22 @@
-
Default Display Mode
-
Initial browser display mode
+
Default Display Mode (for next startup)
+
Mode applied when the server starts. Does NOT change the current running session.
-
+
+
+ circle + Live mode now: + + ⚠ VNC panel will not show browser content + ✓ VNC panel active +
From 8fa2cf20b7d8f312de332b42521bb0d041f38aa6 Mon Sep 17 00:00:00 2001 From: PaoloC68 Date: Wed, 8 Apr 2026 17:53:40 +0000 Subject: [PATCH 06/17] fix: proper button states and rename Refresh to Check Status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Start button: disabled when server is already running - Stop button: disabled when server is not running, shows 'Stopping...' - Restart button: always available (handles crash recovery) - 'Refresh' renamed to 'Check Status' — clear intent: polls current stack status and reports all 6 components - Status description shows 🟢 Stack is running / 🔴 Stack is stopped as default text (no more ambiguous empty state) - Added stopping/checkingStatus state vars for in-progress feedback --- webui/config.html | 141 ++++++++++++++++++++++++++++------------------ 1 file changed, 86 insertions(+), 55 deletions(-) diff --git a/webui/config.html b/webui/config.html index dd46e74..a41f040 100644 --- a/webui/config.html +++ b/webui/config.html @@ -13,18 +13,26 @@ setting_up: false, generating: false, starting: false, + stopping: false, + serverRunning: false, + checkingStatus: false, installInfo: null, async checkStatus() { + this.checkingStatus = true; try { - const res = await fetchApi('/plugins/camofox_browser/camofox_setup', { + const res = await fetchApi('/plugins/camofox_browser/camofox_startup', { method: 'POST', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({action: 'full_status'}) + body: JSON.stringify({action: 'status'}) }); - this.installInfo = await res.json(); + const data = await res.json(); + this.installInfo = data; + this.serverRunning = data?.components?.server === true; } catch(e) { this.installInfo = {ok: false, error: e.message}; + this.serverRunning = false; } + this.checkingStatus = false; }, async generateKeys() { this.generating = true; @@ -279,70 +287,93 @@
-
Start / Stop / Restart
-
+
Stack Control
+
- - - -
+
From 130fec420086005c30c39ce3ea554e155c129932 Mon Sep 17 00:00:00 2001 From: PaoloC68 Date: Wed, 8 Apr 2026 17:56:25 +0000 Subject: [PATCH 07/17] fix: remove stray closing div that broke Alpine.js reactive button states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A leftover from a previous patch closed the camofox-config wrapper div prematurely, placing the Start/Stop/Restart/Check Status buttons outside the Alpine.js x-data scope. Alpine reactive bindings like :disabled broke silently — undefined variables evaluate to falsy, so all buttons appeared permanently enabled regardless of serverRunning/stopping/starting state. --- webui/config.html | 1 - 1 file changed, 1 deletion(-) diff --git a/webui/config.html b/webui/config.html index a41f040..3be4cef 100644 --- a/webui/config.html +++ b/webui/config.html @@ -373,7 +373,6 @@ - From 69eadb2dba71639d2002948b95f27531699c0e01 Mon Sep 17 00:00:00 2001 From: PaoloC68 Date: Wed, 8 Apr 2026 18:10:13 +0000 Subject: [PATCH 08/17] feat: open default home page after startup to avoid black VNC screen - api/camofox_startup.py: After watchdog starts, opens a configurable home URL tab so the VNC panel shows content immediately instead of black screen. Defaults to https://www.google.com. Home tab is skipped if default_home_url is empty in config. - default_config.yaml: Added default_home_url: 'https://www.google.com' - webui/config.html: Added 'Home Page URL' config field (type=url input) under Server Management. Users can set any URL or leave empty to disable. Also removed a duplicate 'Server Management' section title. --- api/camofox_startup.py | 17 +++++++++++++++++ default_config.yaml | 1 + webui/config.html | 11 +++++++++++ 3 files changed, 29 insertions(+) diff --git a/api/camofox_startup.py b/api/camofox_startup.py index cff18c7..a980079 100644 --- a/api/camofox_startup.py +++ b/api/camofox_startup.py @@ -260,6 +260,23 @@ async def _full_start(self) -> dict: steps["watchdog"] = {"ok": watchdog_ok, "msg": "running" if watchdog_ok else "not started"} steps["x11vnc"] = {"ok": self._is_x11vnc_running(), "msg": "running" if self._is_x11vnc_running() else "starting..."} + # 6. Open default home tab so VNC panel shows a page instead of black screen + if virtual_ok: + cfg = get_config() + home_url = cfg.get("default_home_url", "https://www.google.com") + if home_url and home_url.strip(): + try: + client = CamofoxClient( + base_url=cfg["server_url"], + api_key=cfg.get("api_key", ""), + admin_key=cfg.get("admin_key", ""), + ) + await client.post("/sessions/a0-agent-0/new-tab", data={"url": home_url.strip()}) + await client.close() + steps["home_tab"] = {"ok": True, "msg": home_url.strip()} + except Exception as e: + steps["home_tab"] = {"ok": False, "msg": str(e)} + all_ok = server_ok and virtual_ok return { "ok": all_ok, diff --git a/default_config.yaml b/default_config.yaml index 0e5b20b..1968ccb 100644 --- a/default_config.yaml +++ b/default_config.yaml @@ -5,3 +5,4 @@ default_user_id: "" default_headless: "virtual" default_geo_preset: "" auto_start_server: true +default_home_url: "https://www.google.com" diff --git a/webui/config.html b/webui/config.html index 3be4cef..94d7408 100644 --- a/webui/config.html +++ b/webui/config.html @@ -272,6 +272,17 @@
Server Management
+
+
+
Home Page URL
+
Page loaded automatically after startup so the VNC panel shows content instead of a black screen. Leave empty to disable.
+
+
+ +
+
+ +
Auto-start Server
From ddbea9a396a1d919acfc714c9247481f76585dbe Mon Sep 17 00:00:00 2001 From: PaoloC68 Date: Wed, 8 Apr 2026 18:16:26 +0000 Subject: [PATCH 09/17] fix: always kill stale websockify before starting fresh in _ensure_websockify Previously _ensure_websockify checked if any websockify was running and skipped starting a new one if yes. This caused stale/frozen connections: the noVNC client would be connected to an old websockify that lost its x11vnc link after a restart, resulting in a static frozen image with no mouse/keyboard interaction. Fix: always pkill existing websockify instances and start a fresh single one on every startup. This ensures the noVNC client always reconnects to a clean websockify with an active x11vnc link. --- api/camofox_startup.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/api/camofox_startup.py b/api/camofox_startup.py index a980079..a48cbfa 100644 --- a/api/camofox_startup.py +++ b/api/camofox_startup.py @@ -127,18 +127,23 @@ async def _ensure_xvfb(self) -> bool: return False async def _ensure_websockify(self) -> bool: - """Ensure websockify is running on port 6080.""" - if self._is_websockify_running(): - return True + """Start a FRESH single websockify instance, killing any stale ones first. + + Always restarts websockify to avoid stale/frozen connections where the + noVNC client is connected to an old websockify that lost its x11vnc link. + """ novnc = "/opt/noVNC" if not os.path.isdir(novnc): - return False - websockify = shutil.which("websockify") - if not websockify: - return False + return self._is_websockify_running() # Can't manage it, just check + websockify_bin = shutil.which("websockify") + if not websockify_bin: + return self._is_websockify_running() + # Always kill stale instances first + subprocess.run(["pkill", "-f", "websockify"], capture_output=True) + await asyncio.sleep(1) try: await asyncio.create_subprocess_exec( - "python3", websockify, "--web", novnc, "6080", "127.0.0.1:5999", + "python3", websockify_bin, "--web", novnc, "6080", "127.0.0.1:5999", stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, ) From ccc25d3086fbe7cd3f67fa17d23f0f60fefc7c23 Mon Sep 17 00:00:00 2001 From: PaoloC68 Date: Thu, 9 Apr 2026 21:36:01 +0000 Subject: [PATCH 10/17] fix: VNC panel proportional resize on window resize + CSS drag-resize handles --- webui/camofox-viewer-panel.js | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/webui/camofox-viewer-panel.js b/webui/camofox-viewer-panel.js index f52e1a9..b811447 100644 --- a/webui/camofox-viewer-panel.js +++ b/webui/camofox-viewer-panel.js @@ -19,6 +19,16 @@ export function initPanel(panelEl) { panelEl.style.width = w + "px"; panelEl.style.height = h + "px"; + // Enable CSS corner-drag resize handles + panelEl.style.resize = "both"; + panelEl.style.overflow = "hidden"; + panelEl.style.minWidth = "320px"; + panelEl.style.minHeight = "240px"; + + // Track panel size as fraction of viewport — used to scale on window resize + let widthRatio = w / window.innerWidth; + let heightRatio = h / window.innerHeight; + // Position: use saved position or default to top-right area if (store.panelPosition.x !== null && store.panelPosition.y !== null) { panelEl.style.left = store.panelPosition.x + "px"; @@ -65,22 +75,32 @@ export function initPanel(panelEl) { store.savePosition(rect.left, rect.top); }); - // Resize observer + // Resize observer — save size and update viewport ratios after manual resize const resizeObserver = new ResizeObserver(() => { if (!isDragging) { store.saveSize(panelEl.offsetWidth, panelEl.offsetHeight); + // Update ratios so next window resize scales from the new size + widthRatio = panelEl.offsetWidth / window.innerWidth; + heightRatio = panelEl.offsetHeight / window.innerHeight; } }); resizeObserver.observe(panelEl); - // Keep in viewport on window resize + // Keep in viewport AND scale proportionally on window resize window.addEventListener("resize", () => { + // Scale panel to maintain same viewport fraction + const newW = Math.max(320, Math.round(widthRatio * window.innerWidth)); + const newH = Math.max(240, Math.round(heightRatio * window.innerHeight)); + panelEl.style.width = newW + "px"; + panelEl.style.height = newH + "px"; + + // Reposition if panel drifted out of viewport const rect = panelEl.getBoundingClientRect(); if (rect.left > window.innerWidth - 100) { - panelEl.style.left = Math.max(0, window.innerWidth - panelEl.offsetWidth - 20) + "px"; + panelEl.style.left = Math.max(0, window.innerWidth - newW - 20) + "px"; } if (rect.top > window.innerHeight - 40) { - panelEl.style.top = Math.max(0, window.innerHeight - panelEl.offsetHeight - 20) + "px"; + panelEl.style.top = Math.max(0, window.innerHeight - newH - 20) + "px"; } }); } From 100c8e22e5b7c6be641b52e156362bd88991062b Mon Sep 17 00:00:00 2001 From: PaoloC68 Date: Thu, 9 Apr 2026 21:51:56 +0000 Subject: [PATCH 11/17] feat: proportional resize on window resize + maximize/restore button for VNC panel --- webui/camofox-viewer-panel.js | 105 ++++++++++++++++++++++++++-------- 1 file changed, 80 insertions(+), 25 deletions(-) diff --git a/webui/camofox-viewer-panel.js b/webui/camofox-viewer-panel.js index b811447..50e2a87 100644 --- a/webui/camofox-viewer-panel.js +++ b/webui/camofox-viewer-panel.js @@ -10,6 +10,8 @@ export function initPanel(panelEl) { let isDragging = false; let dragStartX = 0, dragStartY = 0, panelStartX = 0, panelStartY = 0; + let isMaximized = false; + let preMaxW, preMaxH, preMaxX, preMaxY; const store = Alpine.store("camofox"); @@ -19,16 +21,6 @@ export function initPanel(panelEl) { panelEl.style.width = w + "px"; panelEl.style.height = h + "px"; - // Enable CSS corner-drag resize handles - panelEl.style.resize = "both"; - panelEl.style.overflow = "hidden"; - panelEl.style.minWidth = "320px"; - panelEl.style.minHeight = "240px"; - - // Track panel size as fraction of viewport — used to scale on window resize - let widthRatio = w / window.innerWidth; - let heightRatio = h / window.innerHeight; - // Position: use saved position or default to top-right area if (store.panelPosition.x !== null && store.panelPosition.y !== null) { panelEl.style.left = store.panelPosition.x + "px"; @@ -44,9 +36,49 @@ export function initPanel(panelEl) { panelEl.style.right = "auto"; panelEl.style.bottom = "auto"; + // --- Maximize/Restore button --- + const controls = titleBar.querySelector(".camofox-controls"); + if (controls) { + const maxBtn = document.createElement("button"); + maxBtn.title = "Maximize / Restore"; + maxBtn.style.cssText = "background:none;border:none;cursor:pointer;color:inherit;font-size:14px;padding:0 4px;line-height:1;"; + maxBtn.textContent = "⛶"; + controls.insertBefore(maxBtn, controls.firstChild); + + maxBtn.addEventListener("click", (e) => { + e.stopPropagation(); + if (!isMaximized) { + // Save current state + preMaxW = panelEl.offsetWidth; + preMaxH = panelEl.offsetHeight; + preMaxX = parseFloat(panelEl.style.left) || 0; + preMaxY = parseFloat(panelEl.style.top) || 0; + // Maximize with small margin + const margin = 8; + panelEl.style.left = margin + "px"; + panelEl.style.top = margin + "px"; + panelEl.style.width = (window.innerWidth - margin * 2) + "px"; + panelEl.style.height = (window.innerHeight - margin * 2) + "px"; + maxBtn.textContent = "❐"; + maxBtn.title = "Restore"; + isMaximized = true; + } else { + // Restore + panelEl.style.left = preMaxX + "px"; + panelEl.style.top = preMaxY + "px"; + panelEl.style.width = preMaxW + "px"; + panelEl.style.height = preMaxH + "px"; + maxBtn.textContent = "⛶"; + maxBtn.title = "Maximize"; + isMaximized = false; + } + }); + } + // Drag: mousedown on title bar titleBar.addEventListener("mousedown", (e) => { if (e.target.closest(".camofox-controls")) return; + if (isMaximized) return; // don't drag when maximized isDragging = true; dragStartX = e.clientX; dragStartY = e.clientY; @@ -75,32 +107,55 @@ export function initPanel(panelEl) { store.savePosition(rect.left, rect.top); }); - // Resize observer — save size and update viewport ratios after manual resize + // Resize observer — save size when user manually resizes const resizeObserver = new ResizeObserver(() => { - if (!isDragging) { + if (!isDragging && !isMaximized) { store.saveSize(panelEl.offsetWidth, panelEl.offsetHeight); - // Update ratios so next window resize scales from the new size - widthRatio = panelEl.offsetWidth / window.innerWidth; - heightRatio = panelEl.offsetHeight / window.innerHeight; } }); resizeObserver.observe(panelEl); - // Keep in viewport AND scale proportionally on window resize + // Proportional resize on window resize + // Track ratio of panel size to viewport at init time + let vpW = window.innerWidth; + let vpH = window.innerHeight; + window.addEventListener("resize", () => { - // Scale panel to maintain same viewport fraction - const newW = Math.max(320, Math.round(widthRatio * window.innerWidth)); - const newH = Math.max(240, Math.round(heightRatio * window.innerHeight)); - panelEl.style.width = newW + "px"; + if (isMaximized) { + // Keep maximized panel filling the viewport + const margin = 8; + panelEl.style.left = margin + "px"; + panelEl.style.top = margin + "px"; + panelEl.style.width = (window.innerWidth - margin * 2) + "px"; + panelEl.style.height = (window.innerHeight - margin * 2) + "px"; + return; + } + + const curW = panelEl.offsetWidth; + const curH = panelEl.offsetHeight; + const newVpW = window.innerWidth; + const newVpH = window.innerHeight; + + // Scale panel proportionally with viewport change + const scaleX = newVpW / vpW; + const scaleY = newVpH / vpH; + const scale = Math.min(scaleX, scaleY); // uniform scale + const newW = Math.max(320, Math.round(curW * scale)); + const newH = Math.max(240, Math.round(curH * scale)); + panelEl.style.width = newW + "px"; panelEl.style.height = newH + "px"; + store.saveSize(newW, newH); - // Reposition if panel drifted out of viewport + // Keep panel in viewport const rect = panelEl.getBoundingClientRect(); - if (rect.left > window.innerWidth - 100) { - panelEl.style.left = Math.max(0, window.innerWidth - newW - 20) + "px"; + if (rect.left + newW > newVpW) { + panelEl.style.left = Math.max(0, newVpW - newW - 20) + "px"; } - if (rect.top > window.innerHeight - 40) { - panelEl.style.top = Math.max(0, window.innerHeight - newH - 20) + "px"; + if (rect.top + newH > newVpH) { + panelEl.style.top = Math.max(0, newVpH - newH - 20) + "px"; } + + vpW = newVpW; + vpH = newVpH; }); } From f731f1c9836b88a6fd7b654b082f577183714b59 Mon Sep 17 00:00:00 2001 From: PaoloC68 Date: Thu, 9 Apr 2026 22:15:22 +0000 Subject: [PATCH 12/17] fix: openbox WM + window maximize + resize=remote for proper VNC panel scaling --- api/camofox_startup.py | 50 +++++++++++++++++++++++++++++++++++++++- tools/camofox_session.py | 2 +- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/api/camofox_startup.py b/api/camofox_startup.py index a48cbfa..21ab18b 100644 --- a/api/camofox_startup.py +++ b/api/camofox_startup.py @@ -265,6 +265,54 @@ async def _full_start(self) -> dict: steps["watchdog"] = {"ok": watchdog_ok, "msg": "running" if watchdog_ok else "not started"} steps["x11vnc"] = {"ok": self._is_x11vnc_running(), "msg": "running" if self._is_x11vnc_running() else "starting..."} + # 6. Start openbox WM and maximize browser window to fill Xvfb display + if virtual_ok: + try: + # Get the display camoufox-bin is using + r = subprocess.run(["pgrep", "-f", "camoufox-bin"], capture_output=True, text=True) + pids = [p for p in r.stdout.strip().split("\n") if p.strip()] + disp = ":100" + for pid in pids: + try: + env_data = open(f"/proc/{pid}/environ", "rb").read().decode("utf-8", "replace") + for var in env_data.split("\x00"): + if var.startswith("DISPLAY="): + disp = var.split("=", 1)[1] + break + except Exception: + pass + # Start openbox WM (needed for wmctrl EWMH maximize) + env_disp = os.environ.copy() + env_disp["DISPLAY"] = disp + subprocess.Popen( + ["openbox", "--sm-disable"], + env=env_disp, + stdout=open("/tmp/openbox.log", "w"), + stderr=subprocess.STDOUT, + ) + await asyncio.sleep(2) + # Maximize all Firefox/Camoufox windows + wins = subprocess.run( + ["xdotool", "search", "--name", ""], + capture_output=True, text=True, env=env_disp + ).stdout.strip().split("\n") + for wid in wins: + if not wid.strip(): + continue + name = subprocess.run( + ["xdotool", "getwindowname", wid], + capture_output=True, text=True, env=env_disp + ).stdout.strip().lower() + if any(k in name for k in ["firefox", "camoufox", "mozilla"]): + wid_hex = hex(int(wid)) + subprocess.run( + ["wmctrl", "-i", "-r", wid_hex, "-b", "add,maximized_vert,maximized_horz"], + capture_output=True, env=env_disp + ) + steps["wm_maximize"] = {"ok": True, "msg": f"openbox+maximize on {disp}"} + except Exception as e: + steps["wm_maximize"] = {"ok": False, "msg": str(e)} + # 6. Open default home tab so VNC panel shows a page instead of black screen if virtual_ok: cfg = get_config() @@ -286,7 +334,7 @@ async def _full_start(self) -> dict: return { "ok": all_ok, "message": "CamoFox VNC stack started successfully." if all_ok else "Stack started with warnings — check steps.", - "vnc_url": "http://localhost:6080/vnc.html?autoconnect=true&resize=scale", + "vnc_url": "http://localhost:6080/vnc.html?autoconnect=true&resize=remote", "steps": steps, } diff --git a/tools/camofox_session.py b/tools/camofox_session.py index b3fdb47..199377f 100644 --- a/tools/camofox_session.py +++ b/tools/camofox_session.py @@ -88,7 +88,7 @@ async def _dispatch(self, action: str, user_id: str) -> str: port = server_url.rstrip("/").split(":")[-1] if not port.isdigit(): port = "6080" - vnc_url = f"http://localhost:6080/vnc.html?autoconnect=true&resize=scale" + vnc_url = f"http://localhost:6080/vnc.html?autoconnect=true&resize=remote" else: display_mode = "headed" shared_state.set_vnc(user_id, vnc_url, display_mode) From 911af564f9ab4cdc04d811be9f77b326bc05d4a5 Mon Sep 17 00:00:00 2001 From: PaoloC68 Date: Thu, 9 Apr 2026 23:04:42 +0000 Subject: [PATCH 13/17] fix: replace openbox with direct xdotool maximize; revert resize=remote to resize=scale; remove black strip --- api/camofox_startup.py | 21 +++++++-------------- tools/camofox_session.py | 2 +- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/api/camofox_startup.py b/api/camofox_startup.py index 21ab18b..cc0bd37 100644 --- a/api/camofox_startup.py +++ b/api/camofox_startup.py @@ -281,17 +281,11 @@ async def _full_start(self) -> dict: break except Exception: pass - # Start openbox WM (needed for wmctrl EWMH maximize) + # Force browser window to fill Xvfb display using xdotool env_disp = os.environ.copy() env_disp["DISPLAY"] = disp - subprocess.Popen( - ["openbox", "--sm-disable"], - env=env_disp, - stdout=open("/tmp/openbox.log", "w"), - stderr=subprocess.STDOUT, - ) + # Get Xvfb resolution from display await asyncio.sleep(2) - # Maximize all Firefox/Camoufox windows wins = subprocess.run( ["xdotool", "search", "--name", ""], capture_output=True, text=True, env=env_disp @@ -304,11 +298,10 @@ async def _full_start(self) -> dict: capture_output=True, text=True, env=env_disp ).stdout.strip().lower() if any(k in name for k in ["firefox", "camoufox", "mozilla"]): - wid_hex = hex(int(wid)) - subprocess.run( - ["wmctrl", "-i", "-r", wid_hex, "-b", "add,maximized_vert,maximized_horz"], - capture_output=True, env=env_disp - ) + subprocess.run(["xdotool", "windowmove", "--sync", wid, "0", "0"], + capture_output=True, env=env_disp) + subprocess.run(["xdotool", "windowsize", "--sync", wid, "1920", "1080"], + capture_output=True, env=env_disp) steps["wm_maximize"] = {"ok": True, "msg": f"openbox+maximize on {disp}"} except Exception as e: steps["wm_maximize"] = {"ok": False, "msg": str(e)} @@ -334,7 +327,7 @@ async def _full_start(self) -> dict: return { "ok": all_ok, "message": "CamoFox VNC stack started successfully." if all_ok else "Stack started with warnings — check steps.", - "vnc_url": "http://localhost:6080/vnc.html?autoconnect=true&resize=remote", + "vnc_url": "http://localhost:6080/vnc.html?autoconnect=true&resize=scale", "steps": steps, } diff --git a/tools/camofox_session.py b/tools/camofox_session.py index 199377f..b3fdb47 100644 --- a/tools/camofox_session.py +++ b/tools/camofox_session.py @@ -88,7 +88,7 @@ async def _dispatch(self, action: str, user_id: str) -> str: port = server_url.rstrip("/").split(":")[-1] if not port.isdigit(): port = "6080" - vnc_url = f"http://localhost:6080/vnc.html?autoconnect=true&resize=remote" + vnc_url = f"http://localhost:6080/vnc.html?autoconnect=true&resize=scale" else: display_mode = "headed" shared_state.set_vnc(user_id, vnc_url, display_mode) From 37c9ce9940713d292babf01a47458e2f7ae07b94 Mon Sep 17 00:00:00 2001 From: PaoloC68 Date: Thu, 9 Apr 2026 23:49:02 +0000 Subject: [PATCH 14/17] =?UTF-8?q?fix:=20remove=20broken=20home=5Ftab=20API?= =?UTF-8?q?=20call,=20use=20xdotool;=20fix=20websockify=20timing=20(1s?= =?UTF-8?q?=E2=86=923s);=20add=20fission.autostart=3Dfalse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/camofox_startup.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/api/camofox_startup.py b/api/camofox_startup.py index cc0bd37..e5a8ad0 100644 --- a/api/camofox_startup.py +++ b/api/camofox_startup.py @@ -147,7 +147,7 @@ async def _ensure_websockify(self) -> bool: stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, ) - await asyncio.sleep(1) + await asyncio.sleep(3) return self._is_websockify_running() except Exception: return False @@ -302,23 +302,37 @@ async def _full_start(self) -> dict: capture_output=True, env=env_disp) subprocess.run(["xdotool", "windowsize", "--sync", wid, "1920", "1080"], capture_output=True, env=env_disp) - steps["wm_maximize"] = {"ok": True, "msg": f"openbox+maximize on {disp}"} + steps["wm_maximize"] = {"ok": True, "msg": f"maximize on {disp}"} except Exception as e: steps["wm_maximize"] = {"ok": False, "msg": str(e)} - # 6. Open default home tab so VNC panel shows a page instead of black screen + # 7. Open default home tab using xdotool (API endpoint doesn't exist) if virtual_ok: cfg = get_config() home_url = cfg.get("default_home_url", "https://www.google.com") if home_url and home_url.strip(): try: - client = CamofoxClient( - base_url=cfg["server_url"], - api_key=cfg.get("api_key", ""), - admin_key=cfg.get("admin_key", ""), - ) - await client.post("/sessions/a0-agent-0/new-tab", data={"url": home_url.strip()}) - await client.close() + # Find browser display + disp = ":100" + r = subprocess.run(["pgrep", "-f", "camoufox-bin"], capture_output=True, text=True) + for pid in r.stdout.strip().split("\n"): + if not pid.strip(): continue + try: + env_data = open(f"/proc/{pid}/environ", "rb").read().decode("utf-8", "replace") + for var in env_data.split("\x00"): + if var.startswith("DISPLAY="): + disp = var.split("=", 1)[1] + break + except Exception: + pass + env_disp = os.environ.copy() + env_disp["DISPLAY"] = disp + # Use xdotool to type URL in browser address bar + subprocess.run(["xdotool", "key", "ctrl+l"], capture_output=True, env=env_disp) + await asyncio.sleep(0.5) + subprocess.run(["xdotool", "type", "--clearmodifiers", home_url.strip()], capture_output=True, env=env_disp) + await asyncio.sleep(0.3) + subprocess.run(["xdotool", "key", "Return"], capture_output=True, env=env_disp) steps["home_tab"] = {"ok": True, "msg": home_url.strip()} except Exception as e: steps["home_tab"] = {"ok": False, "msg": str(e)} From 9b3f02b4149a7aaf2e3f766769d0cdccca2af5a3 Mon Sep 17 00:00:00 2001 From: PaoloC68 Date: Thu, 9 Apr 2026 23:49:13 +0000 Subject: [PATCH 15/17] fix: add fission.autostart=false to user.js.template (fixes black cookie modals) --- scripts/user.js.template | 34 +++++++--------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/scripts/user.js.template b/scripts/user.js.template index 0fc12c8..1ae6226 100644 --- a/scripts/user.js.template +++ b/scripts/user.js.template @@ -1,23 +1,4 @@ -// CamoFox Firefox User Preferences Template -// Apply to: /root/.camofox/profiles//user.js -// -// WHAT THIS FIXES: -// When running CamoFox in virtual display mode (Xvfb), Google OAuth and other -// popup windows render as a black screen. This is caused by GPU-accelerated -// compositing being unavailable in headless/virtual environments. -// -// HOW TO APPLY: -// 1. Find your profile directory: -// ls /root/.camofox/profiles/ -// 2. Copy this file: -// cp scripts/user.js.template /root/.camofox/profiles/a0-agent-0/user.js -// (Replace 'a0-agent-0' with your actual profile directory name) -// 3. Restart the CamoFox browser session for prefs to take effect. -// -// NOTE: These prefs are written to user.js which is re-applied on every Firefox -// startup, overriding any conflicting prefs.js settings. - -// --- Disable GPU / hardware acceleration --- +// Force software rendering for ALL windows including popups user_pref("gfx.webrender.all", false); user_pref("gfx.webrender.software", true); user_pref("gfx.webrender.enabled", false); @@ -29,15 +10,14 @@ user_pref("layers.offmainthreadcomposition.enabled", false); user_pref("gfx.canvas.azure.backends", "cairo"); user_pref("gfx.content.azure.backends", "cairo"); user_pref("gfx.direct2d.disabled", true); - -// --- Disable media and sandbox processes (avoid rendering isolation issues) --- user_pref("media.rdd-process.enabled", false); user_pref("security.sandbox.content.level", 0); user_pref("dom.ipc.sandbox.content.level", 0); - -// --- Force popup windows to open as tabs --- -// This prevents Google OAuth and similar popup windows from opening as -// separate browser windows that render black in virtual displays. -// Instead, they open as new tabs in the same window — fully visible in VNC. +// Force popups to open as tabs user_pref("browser.link.open_newwindow", 3); user_pref("browser.link.open_newwindow.restriction", 0); +// Disable Fission (site isolation) — fixes black cross-origin iframes and modals +user_pref("fission.autostart", false); +user_pref("fission.webContentIsolationStrategy", 0); +user_pref("gfx.webrender.compositor", false); +user_pref("gfx.webrender.compositor.force-enabled", false); From 83d56c32da52adb472c1fa12f65413a2125497fe Mon Sep 17 00:00:00 2001 From: PaoloC68 Date: Thu, 9 Apr 2026 23:52:19 +0000 Subject: [PATCH 16/17] fix: websockify pkill -9 + longer sleep after kill --- api/camofox_startup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/camofox_startup.py b/api/camofox_startup.py index e5a8ad0..c38e984 100644 --- a/api/camofox_startup.py +++ b/api/camofox_startup.py @@ -138,9 +138,9 @@ async def _ensure_websockify(self) -> bool: websockify_bin = shutil.which("websockify") if not websockify_bin: return self._is_websockify_running() - # Always kill stale instances first - subprocess.run(["pkill", "-f", "websockify"], capture_output=True) - await asyncio.sleep(1) + # Always kill stale instances first (use -9 to ensure clean kill) + subprocess.run(["pkill", "-9", "-f", "websockify"], capture_output=True) + await asyncio.sleep(2) # Wait for port 6080 to be released try: await asyncio.create_subprocess_exec( "python3", websockify_bin, "--web", novnc, "6080", "127.0.0.1:5999", From 685520d4fbe845dbd9b46568c1c46533bd6c4ab0 Mon Sep 17 00:00:00 2001 From: PaoloC68 Date: Fri, 10 Apr 2026 00:40:03 +0000 Subject: [PATCH 17/17] feat: configurable VNC resolution via CAMOFOX_VNC_RESOLUTION (default 1280x800) --- api/camofox_startup.py | 20 ++++++++++++++++---- default_config.yaml | 1 + 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/api/camofox_startup.py b/api/camofox_startup.py index c38e984..116774c 100644 --- a/api/camofox_startup.py +++ b/api/camofox_startup.py @@ -24,6 +24,15 @@ "/usr/lib/node_modules/camofox-browser/dist/src/server.js", ] +def _get_vnc_resolution(): + """Get VNC resolution from config, returns (width, height, depth_str).""" + cfg = get_config() + res = cfg.get("vnc_resolution", "1280x800") + parts = res.lower().split("x") + w = int(parts[0]) if len(parts) >= 1 else 1280 + h = int(parts[1]) if len(parts) >= 2 else 800 + return w, h, f"{w}x{h}x24" + SOFTWARE_RENDERING_ENV = { "LIBGL_ALWAYS_SOFTWARE": "1", "GALLIUM_DRIVER": "llvmpipe", @@ -116,8 +125,9 @@ async def _ensure_xvfb(self) -> bool: if self._is_xvfb_running(): return True try: + _, _, res_str = _get_vnc_resolution() await asyncio.create_subprocess_exec( - "Xvfb", ":99", "-screen", "0", "1920x1080x24", "-ac", "-nolisten", "tcp", + "Xvfb", ":99", "-screen", "0", res_str, "-ac", "-nolisten", "tcp", stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, ) @@ -162,6 +172,8 @@ async def _ensure_server(self) -> bool: cfg = get_config() env = os.environ.copy() env.update(SOFTWARE_RENDERING_ENV) + _, _, res_str = _get_vnc_resolution() + env["CAMOFOX_VNC_RESOLUTION"] = res_str port = cfg.get("server_url", "http://localhost:9377").split(":")[-1].rstrip("/") env["CAMOFOX_PORT"] = str(port) env["CAMOFOX_HEADLESS"] = normalize_headless_mode(cfg.get("default_headless", True)) @@ -284,7 +296,7 @@ async def _full_start(self) -> dict: # Force browser window to fill Xvfb display using xdotool env_disp = os.environ.copy() env_disp["DISPLAY"] = disp - # Get Xvfb resolution from display + vnc_w, vnc_h, _ = _get_vnc_resolution() await asyncio.sleep(2) wins = subprocess.run( ["xdotool", "search", "--name", ""], @@ -300,9 +312,9 @@ async def _full_start(self) -> dict: if any(k in name for k in ["firefox", "camoufox", "mozilla"]): subprocess.run(["xdotool", "windowmove", "--sync", wid, "0", "0"], capture_output=True, env=env_disp) - subprocess.run(["xdotool", "windowsize", "--sync", wid, "1920", "1080"], + subprocess.run(["xdotool", "windowsize", "--sync", wid, str(vnc_w), str(vnc_h)], capture_output=True, env=env_disp) - steps["wm_maximize"] = {"ok": True, "msg": f"maximize on {disp}"} + steps["wm_maximize"] = {"ok": True, "msg": f"maximize on {disp} at {vnc_w}x{vnc_h}"} except Exception as e: steps["wm_maximize"] = {"ok": False, "msg": str(e)} diff --git a/default_config.yaml b/default_config.yaml index 1968ccb..3ea8fb6 100644 --- a/default_config.yaml +++ b/default_config.yaml @@ -6,3 +6,4 @@ default_headless: "virtual" default_geo_preset: "" auto_start_server: true default_home_url: "https://www.google.com" +vnc_resolution: "1280x800"