From 6871ea6b67b6c9ab15b5ed0ab3973429553cf78b Mon Sep 17 00:00:00 2001 From: Ruslan Date: Sat, 25 Apr 2026 17:28:04 +0000 Subject: [PATCH] fix: improve web runtime resolution and restore x11vnc ncache --- app/main.py | 16 +++++++++++++++- app/templates/dashboard.html | 23 +++++++++++++++++++++-- kiosk/entrypoint.sh | 20 ++++++++++++++++++-- universal-runtime/entrypoint.sh | 20 ++++++++++++++++++-- universal-runtime/manager.py | 24 ++++++++++++++++++++++-- 5 files changed, 94 insertions(+), 9 deletions(-) diff --git a/app/main.py b/app/main.py index f7d7d33..d81894f 100644 --- a/app/main.py +++ b/app/main.py @@ -57,6 +57,7 @@ PREWARM_POOL_SIZE = int(os.getenv("PREWARM_POOL_SIZE", "0")) UNIVERSAL_POOL_SIZE = int(os.getenv("UNIVERSAL_POOL_SIZE", "0")) WEB_POOL_SIZE = int(os.getenv("WEB_POOL_SIZE", "5")) WEB_POOL_BUFFER = int(os.getenv("WEB_POOL_BUFFER", "2")) +X11VNC_FLAGS = os.getenv("X11VNC_FLAGS", "-wait 5 -defer 5 -ncache 10 -threads") MAX_ACTIVE_SERVICES_PER_USER = int(os.getenv("MAX_ACTIVE_SERVICES_PER_USER", "4")) WEB_RESOLUTION_MIN_WIDTH = int(os.getenv("WEB_RESOLUTION_MIN_WIDTH", "1024")) WEB_RESOLUTION_MIN_HEIGHT = int(os.getenv("WEB_RESOLUTION_MIN_HEIGHT", "720")) @@ -573,6 +574,7 @@ def ensure_universal_pool() -> None: "IDLE_TIMEOUT": str(SESSION_IDLE_SECONDS), "ENABLE_HEARTBEAT": "0", "SESSION_ID": f"universal-{i}", + "X11VNC_FLAGS": X11VNC_FLAGS, } try: c = d.containers.get(name) @@ -634,6 +636,7 @@ def ensure_web_pool(target_size: Optional[int] = None) -> None: "IDLE_TIMEOUT": str(SESSION_IDLE_SECONDS), "ENABLE_HEARTBEAT": "0", "SESSION_ID": f"webpool-{i}", + "X11VNC_FLAGS": X11VNC_FLAGS, } should_create = False try: @@ -869,6 +872,7 @@ def create_runtime_container(service: Service, session_id: str): "IDLE_TIMEOUT": str(SESSION_IDLE_SECONDS), "ENABLE_HEARTBEAT": "1", "TOUCH_PATH": f"/api/sessions/{session_id}/touch", + "X11VNC_FLAGS": X11VNC_FLAGS, } image = "portal-kiosk:latest" @@ -929,6 +933,7 @@ def ensure_warm_pool(service: Service, pool_size: Optional[int] = None) -> None: "IDLE_TIMEOUT": str(SESSION_IDLE_SECONDS), "ENABLE_HEARTBEAT": "0", "TOUCH_PATH": "", + "X11VNC_FLAGS": X11VNC_FLAGS, } if service.type == ServiceType.WEB: base_env["UNIVERSAL_WEB"] = "1" @@ -1903,7 +1908,7 @@ def go_service( payload.update(extra) log_event("go_service_timing", **payload) - log_event("session_open_requested", user_id=user.id, service_slug=slug) + log_event("session_open_requested", user_id=user.id, service_slug=slug, sw=sw, sh=sh) service = db.scalar(select(Service).where(Service.slug == slug, Service.active == True)) if not service: raise HTTPException(status_code=404, detail="Service not found") @@ -1913,6 +1918,15 @@ def go_service( raise HTTPException(status_code=403, detail="ACL denied") client_width, client_height = sanitize_client_resolution(sw, sh) + log_event( + "session_open_resolution", + user_id=user.id, + service_slug=slug, + sw=sw, + sh=sh, + client_width=client_width, + client_height=client_height, + ) user_lock_started = time.perf_counter() try: diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 4e00338..d7a82d0 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -107,8 +107,27 @@ } function currentScreenParams() { - const width = clamp(window.innerWidth || document.documentElement.clientWidth || 1280, 320, 7680); - const height = clamp(window.innerHeight || document.documentElement.clientHeight || 720, 240, 4320); + const screenWidth = + window.screen && Number.isFinite(window.screen.width) && window.screen.width > 0 + ? window.screen.width + : null; + const screenHeight = + window.screen && Number.isFinite(window.screen.height) && window.screen.height > 0 + ? window.screen.height + : null; + const viewportWidth = + (window.visualViewport && window.visualViewport.width) || + window.innerWidth || + document.documentElement.clientWidth || + 1280; + const viewportHeight = + (window.visualViewport && window.visualViewport.height) || + window.innerHeight || + document.documentElement.clientHeight || + 720; + // Prefer stable screen dimensions; viewport is fallback. + const width = clamp(Math.round(screenWidth || viewportWidth), 320, 7680); + const height = clamp(Math.round(screenHeight || viewportHeight), 240, 4320); const sp = new URLSearchParams(); sp.set('sw', String(width)); sp.set('sh', String(height)); diff --git a/kiosk/entrypoint.sh b/kiosk/entrypoint.sh index cf24d66..5643c67 100755 --- a/kiosk/entrypoint.sh +++ b/kiosk/entrypoint.sh @@ -4,7 +4,7 @@ set -euo pipefail TARGET_URL="${TARGET_URL:-https://example.com}" SESSION_ID="${SESSION_ID:-unknown}" IDLE_TIMEOUT="${IDLE_TIMEOUT:-1800}" -X11VNC_FLAGS="${X11VNC_FLAGS:--wait 5 -defer 5 -ncache 10 -ncache_cr -threads}" +X11VNC_FLAGS="${X11VNC_FLAGS:--wait 5 -defer 5 -ncache 10 -threads}" ENABLE_HEARTBEAT="${ENABLE_HEARTBEAT:-1}" TOUCH_PATH="${TOUCH_PATH:-/api/sessions/${SESSION_ID}/touch}" UNIVERSAL_WEB="${UNIVERSAL_WEB:-0}" @@ -183,6 +183,22 @@ else >/tmp/chromium.log 2>&1 & fi -x11vnc -display :1 -rfbport 5900 -forever -shared -nopw -noxdamage $X11VNC_FLAGS >/tmp/x11vnc.log 2>&1 & +start_x11vnc_with_retry() { + local display_arg="$1" + local attempt=0 + while [ "$attempt" -lt 12 ]; do + x11vnc -display "$display_arg" -rfbport 5900 -forever -shared -nopw -noxdamage $X11VNC_FLAGS >/tmp/x11vnc.log 2>&1 & + local pid=$! + sleep 1 + if kill -0 "$pid" 2>/dev/null; then + return 0 + fi + attempt=$((attempt + 1)) + sleep 0.5 + done + return 1 +} + +start_x11vnc_with_retry ":1" || true exec websockify --verbose --idle-timeout="$IDLE_TIMEOUT" --web=/opt/portal 6080 localhost:5900 diff --git a/universal-runtime/entrypoint.sh b/universal-runtime/entrypoint.sh index ba9d490..2b7d7d8 100755 --- a/universal-runtime/entrypoint.sh +++ b/universal-runtime/entrypoint.sh @@ -2,7 +2,7 @@ set -euo pipefail IDLE_TIMEOUT="${IDLE_TIMEOUT:-1800}" -X11VNC_FLAGS="${X11VNC_FLAGS:--wait 5 -defer 5 -ncache 10 -ncache_cr -threads}" +X11VNC_FLAGS="${X11VNC_FLAGS:--wait 5 -defer 5 -ncache 10 -threads}" SCREEN_GEOMETRY="${SCREEN_GEOMETRY:-1920x1080x24}" CHROME_WINDOW_SIZE="${CHROME_WINDOW_SIZE:-1920,1080}" ENABLE_HEARTBEAT="${ENABLE_HEARTBEAT:-1}" @@ -264,6 +264,22 @@ export CHROME_WINDOW_SIZE Xvfb "$DISPLAY_NUM" -screen 0 "$SCREEN_GEOMETRY" >/tmp/xvfb.log 2>&1 & fluxbox >/tmp/fluxbox.log 2>&1 & python3 /manager.py >/tmp/manager.log 2>&1 & -x11vnc -display "$DISPLAY_NUM" -rfbport 5900 -forever -shared -nopw -noxdamage $X11VNC_FLAGS >/tmp/x11vnc.log 2>&1 & +start_x11vnc_with_retry() { + local display_arg="$1" + local attempt=0 + while [ "$attempt" -lt 12 ]; do + x11vnc -display "$display_arg" -rfbport 5900 -forever -shared -nopw -noxdamage $X11VNC_FLAGS >/tmp/x11vnc.log 2>&1 & + local pid=$! + sleep 1 + if kill -0 "$pid" 2>/dev/null; then + return 0 + fi + attempt=$((attempt + 1)) + sleep 0.5 + done + return 1 +} + +start_x11vnc_with_retry "$DISPLAY_NUM" || true exec websockify --verbose --idle-timeout="$IDLE_TIMEOUT" --web=/opt/portal 6080 localhost:5900 diff --git a/universal-runtime/manager.py b/universal-runtime/manager.py index ab34b66..6d05752 100644 --- a/universal-runtime/manager.py +++ b/universal-runtime/manager.py @@ -71,15 +71,35 @@ def _sanitize_resolution(width: int | None, height: int | None) -> tuple[int, in def apply_resolution(width: int | None, height: int | None) -> tuple[int, int]: safe_w, safe_h = _sanitize_resolution(width, height) # Best effort: Xvfb usually exposes RandR and accepts xrandr -s. + applied = False try: - subprocess.run( # noqa: S603 + result = subprocess.run( # noqa: S603 ["xrandr", "-display", DISPLAY, "-s", f"{safe_w}x{safe_h}"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) + applied = result.returncode == 0 except Exception: - pass + applied = False + + if not applied: + # Fallback to default geometry if requested mode is unsupported. + try: + fallback_w, fallback_h = [int(x) for x in CHROME_WINDOW_SIZE.split(",", 1)] + except Exception: + fallback_w, fallback_h = 1920, 1080 + safe_w, safe_h = _sanitize_resolution(fallback_w, fallback_h) + try: + subprocess.run( # noqa: S603 + ["xrandr", "-display", DISPLAY, "-s", f"{safe_w}x{safe_h}"], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except Exception: + pass + _state["resolution"] = f"{safe_w},{safe_h}" return safe_w, safe_h