From 8863943d79841bbacb59622fbc8dc4df100c2e9c Mon Sep 17 00:00:00 2001 From: Ruslan Date: Fri, 24 Apr 2026 18:26:11 +0000 Subject: [PATCH 1/2] feat: propagate client screen size to web runtime --- app/main.py | 43 ++++++++++++++++++---- app/templates/dashboard.html | 26 +++++++++++++ universal-runtime/manager.py | 71 +++++++++++++++++++++++++++++++++--- 3 files changed, 128 insertions(+), 12 deletions(-) diff --git a/app/main.py b/app/main.py index cb7ad33..f7d7d33 100644 --- a/app/main.py +++ b/app/main.py @@ -16,7 +16,7 @@ from typing import Optional import docker import requests -from fastapi import Depends, FastAPI, File, Form, HTTPException, Request, UploadFile, status +from fastapi import Depends, FastAPI, File, Form, HTTPException, Query, Request, UploadFile, status from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates @@ -58,6 +58,10 @@ 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")) 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")) +WEB_RESOLUTION_MAX_WIDTH = int(os.getenv("WEB_RESOLUTION_MAX_WIDTH", "3840")) +WEB_RESOLUTION_MAX_HEIGHT = int(os.getenv("WEB_RESOLUTION_MAX_HEIGHT", "2160")) ENABLE_STARTUP_MAINTENANCE = os.getenv("ENABLE_STARTUP_MAINTENANCE", "1") == "1" ICON_UPLOAD_MAX_BYTES = 2 * 1024 * 1024 ICON_UPLOAD_TYPES = { @@ -776,13 +780,25 @@ def acquire_web_pool_slot(db: Session) -> int: return 0 -def dispatch_universal_target(slot: int, service: Service) -> None: +def sanitize_client_resolution(width: Optional[int], height: Optional[int]) -> tuple[Optional[int], Optional[int]]: + if width is None or height is None: + return None, None + clamped_width = max(WEB_RESOLUTION_MIN_WIDTH, min(int(width), WEB_RESOLUTION_MAX_WIDTH)) + clamped_height = max(WEB_RESOLUTION_MIN_HEIGHT, min(int(height), WEB_RESOLUTION_MAX_HEIGHT)) + return clamped_width, clamped_height + + +def dispatch_universal_target(slot: int, service: Service, width: Optional[int] = None, height: Optional[int] = None) -> None: name = universal_container_name(slot) url = "" payload = {} if service.type == ServiceType.WEB: url = f"http://{name}:7000/open" payload = {"url": normalize_web_target(service.target)} + width, height = sanitize_client_resolution(width, height) + if width and height: + payload["width"] = width + payload["height"] = height elif service.type == ServiceType.RDP: cfg = parse_rdp_target(service.target) url = f"http://{name}:7000/rdp" @@ -810,14 +826,19 @@ def dispatch_universal_target(slot: int, service: Service) -> None: raise last_exc -def dispatch_web_pool_target(slot: int, service: Service) -> None: +def dispatch_web_pool_target(slot: int, service: Service, width: Optional[int] = None, height: Optional[int] = None) -> None: name = web_pool_container_name(slot) target_url = normalize_web_target(service.target) url = f"http://{name}:7000/open" + payload = {"url": target_url} + width, height = sanitize_client_resolution(width, height) + if width and height: + payload["width"] = width + payload["height"] = height last_exc = None for _ in range(max(1, POOL_DISPATCH_RETRIES)): try: - resp = requests.post(url, json={"url": target_url}, timeout=POOL_DISPATCH_REQUEST_TIMEOUT_SECONDS) + resp = requests.post(url, json=payload, timeout=POOL_DISPATCH_REQUEST_TIMEOUT_SECONDS) resp.raise_for_status() return except Exception as exc: @@ -1858,7 +1879,13 @@ def logout(request: Request): @app.get("/go/{slug}") -def go_service(slug: str, user: User = Depends(require_user), db: Session = Depends(get_db)): +def go_service( + slug: str, + sw: Optional[int] = Query(default=None, ge=320, le=7680), + sh: Optional[int] = Query(default=None, ge=240, le=4320), + user: User = Depends(require_user), + db: Session = Depends(get_db), +): total_started = time.perf_counter() phase_ms = {} @@ -1885,6 +1912,8 @@ def go_service(slug: str, user: User = Depends(require_user), db: Session = Depe if not has_access(db, user.id, service.id): raise HTTPException(status_code=403, detail="ACL denied") + client_width, client_height = sanitize_client_resolution(sw, sh) + user_lock_started = time.perf_counter() try: with allocator_lock(db, 92000 + int(user.id), timeout_seconds=GO_USER_LOCK_TIMEOUT_SECONDS): @@ -1960,7 +1989,7 @@ def go_service(slug: str, user: User = Depends(require_user), db: Session = Depe t_dispatch = time.perf_counter() terminate_active_slot_sessions(db, slot_cid) - dispatch_web_pool_target(slot, service) + dispatch_web_pool_target(slot, service, width=client_width, height=client_height) _mark("dispatch_web_target_ms", t_dispatch) t_commit = time.perf_counter() @@ -2006,7 +2035,7 @@ def go_service(slug: str, user: User = Depends(require_user), db: Session = Depe t_dispatch = time.perf_counter() terminate_active_slot_sessions(db, slot_cid) - dispatch_universal_target(slot, service) + dispatch_universal_target(slot, service, width=client_width, height=client_height) _mark("dispatch_universal_target_ms", t_dispatch) t_commit = time.perf_counter() diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 1b9f6c4..4e00338 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -100,6 +100,32 @@ banner.style.display = 'none'; }); })(); + + (function () { + function clamp(value, min, max) { + return Math.max(min, Math.min(max, value)); + } + + 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 sp = new URLSearchParams(); + sp.set('sw', String(width)); + sp.set('sh', String(height)); + return sp; + } + + document.querySelectorAll('a.tile[href^="/go/"]').forEach(function (link) { + link.addEventListener('click', function () { + try { + const url = new URL(link.getAttribute('href'), window.location.origin); + const params = currentScreenParams(); + url.search = params.toString(); + link.setAttribute('href', url.pathname + '?' + url.searchParams.toString()); + } catch (e) {} + }, { capture: true }); + }); + })(); diff --git a/universal-runtime/manager.py b/universal-runtime/manager.py index 3477d1d..ab34b66 100644 --- a/universal-runtime/manager.py +++ b/universal-runtime/manager.py @@ -8,11 +8,16 @@ from http.server import BaseHTTPRequestHandler, HTTPServer DISPLAY = os.environ.get("DISPLAY", ":1") CHROME_WINDOW_SIZE = os.environ.get("CHROME_WINDOW_SIZE", "1920,1080") +RESOLUTION_MIN_WIDTH = int(os.environ.get("WEB_RESOLUTION_MIN_WIDTH", "1024")) +RESOLUTION_MIN_HEIGHT = int(os.environ.get("WEB_RESOLUTION_MIN_HEIGHT", "720")) +RESOLUTION_MAX_WIDTH = int(os.environ.get("WEB_RESOLUTION_MAX_WIDTH", "3840")) +RESOLUTION_MAX_HEIGHT = int(os.environ.get("WEB_RESOLUTION_MAX_HEIGHT", "2160")) _state = { "proc": None, "mode": "idle", "target": "", + "resolution": CHROME_WINDOW_SIZE, } _lock = threading.Lock() @@ -50,7 +55,37 @@ def _start_process(cmd: list[str], mode: str, target: str) -> None: _state["target"] = target -def open_web(url: str) -> None: +def _sanitize_resolution(width: int | None, height: int | None) -> tuple[int, int]: + if not width or not height: + try: + default_w, default_h = [int(x) for x in CHROME_WINDOW_SIZE.split(",", 1)] + return default_w, default_h + except Exception: + return 1920, 1080 + + safe_w = max(RESOLUTION_MIN_WIDTH, min(int(width), RESOLUTION_MAX_WIDTH)) + safe_h = max(RESOLUTION_MIN_HEIGHT, min(int(height), RESOLUTION_MAX_HEIGHT)) + return safe_w, safe_h + + +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. + 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 + + +def open_web(url: str, width: int | None = None, height: int | None = None) -> None: + safe_w, safe_h = apply_resolution(width, height) cmd = [ "chromium", "--no-sandbox", @@ -65,7 +100,7 @@ def open_web(url: str) -> None: "--ignore-certificate-errors", "--allow-insecure-localhost", "--allow-running-insecure-content", - f"--window-size={CHROME_WINDOW_SIZE}", + f"--window-size={safe_w},{safe_h}", "--no-first-run", "--no-default-browser-check", url, @@ -125,7 +160,16 @@ class Handler(BaseHTTPRequestHandler): if self.path == "/health": proc = _state.get("proc") running = bool(proc and proc.poll() is None) - self._json(200, {"ok": True, "mode": _state.get("mode", "idle"), "running": running, "target": _state.get("target", "")}) + self._json( + 200, + { + "ok": True, + "mode": _state.get("mode", "idle"), + "running": running, + "target": _state.get("target", ""), + "resolution": _state.get("resolution", CHROME_WINDOW_SIZE), + }, + ) return self._json(404, {"detail": "Not found"}) @@ -137,9 +181,26 @@ class Handler(BaseHTTPRequestHandler): if not (url.startswith("http://") or url.startswith("https://")): self._json(400, {"detail": "Invalid URL"}) return + width = data.get("width") + height = data.get("height") with _lock: - open_web(url) - self._json(200, {"ok": True, "mode": "web", "target": url}) + open_web(url, width=width, height=height) + self._json( + 200, + { + "ok": True, + "mode": "web", + "target": url, + "resolution": _state.get("resolution", CHROME_WINDOW_SIZE), + }, + ) + return + if self.path == "/resolution": + width = data.get("width") + height = data.get("height") + with _lock: + safe_w, safe_h = apply_resolution(width, height) + self._json(200, {"ok": True, "width": safe_w, "height": safe_h}) return if self.path == "/rdp": with _lock: From 32c2b4b9a55437021338a50ea9d2c669f6bae2bf Mon Sep 17 00:00:00 2001 From: Ruslan Date: Sat, 25 Apr 2026 08:21:27 +0000 Subject: [PATCH 2/2] perf(runtime): tune x11vnc flags and ignore backup artifacts --- .gitignore | 3 +++ kiosk/entrypoint.sh | 3 ++- universal-runtime/entrypoint.sh | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index db7d43a..e52c7d7 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ venv/ traefik/letsencrypt/acme.json docs/PROJECT_CONTEXT.md PROJECT_CONTEXT.md +*.bak* +*.env.bak* +docs/CONTEXT_TEST.md diff --git a/kiosk/entrypoint.sh b/kiosk/entrypoint.sh index 0418280..cf24d66 100755 --- a/kiosk/entrypoint.sh +++ b/kiosk/entrypoint.sh @@ -4,6 +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}" ENABLE_HEARTBEAT="${ENABLE_HEARTBEAT:-1}" TOUCH_PATH="${TOUCH_PATH:-/api/sessions/${SESSION_ID}/touch}" UNIVERSAL_WEB="${UNIVERSAL_WEB:-0}" @@ -182,6 +183,6 @@ else >/tmp/chromium.log 2>&1 & fi -x11vnc -display :1 -rfbport 5900 -forever -shared -nopw -noxdamage >/tmp/x11vnc.log 2>&1 & +x11vnc -display :1 -rfbport 5900 -forever -shared -nopw -noxdamage $X11VNC_FLAGS >/tmp/x11vnc.log 2>&1 & 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 40577d0..ba9d490 100755 --- a/universal-runtime/entrypoint.sh +++ b/universal-runtime/entrypoint.sh @@ -2,6 +2,7 @@ set -euo pipefail IDLE_TIMEOUT="${IDLE_TIMEOUT:-1800}" +X11VNC_FLAGS="${X11VNC_FLAGS:--wait 5 -defer 5 -ncache 10 -ncache_cr -threads}" SCREEN_GEOMETRY="${SCREEN_GEOMETRY:-1920x1080x24}" CHROME_WINDOW_SIZE="${CHROME_WINDOW_SIZE:-1920,1080}" ENABLE_HEARTBEAT="${ENABLE_HEARTBEAT:-1}" @@ -263,6 +264,6 @@ 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 >/tmp/x11vnc.log 2>&1 & +x11vnc -display "$DISPLAY_NUM" -rfbport 5900 -forever -shared -nopw -noxdamage $X11VNC_FLAGS >/tmp/x11vnc.log 2>&1 & exec websockify --verbose --idle-timeout="$IDLE_TIMEOUT" --web=/opt/portal 6080 localhost:5900