feat: propagate client screen size to web runtime

This commit is contained in:
2026-04-24 18:26:11 +00:00
parent 627910f07b
commit 8863943d79
3 changed files with 128 additions and 12 deletions
+36 -7
View File
@@ -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()
+26
View File
@@ -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 });
});
})();
</script>
</body>
</html>