Files
daynivo-kiosk-os/src/daynivo_kiosk/cdp.py
T
rothmanandClaude Opus 5.5 6733aacb6e
CI / test-bookworm (push) Successful in 29s
CI / test-latest (push) Successful in 33s
Rename the Python package to daynivo_kiosk
src/voxip_kiosk -> src/daynivo_kiosk, with every import, the launchers,
install.sh, the Makefile and the manager's gunicorn app path. An upgrade
replaces /usr/lib/voxip-kiosk/python as a whole, so the old package
disappears; tested by upgrading an installation of the previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 23:52:39 +01:00

127 lines
4.8 KiB
Python

"""Minimal Chrome DevTools Protocol client for the local kiosk browser.
Chromium listens on 127.0.0.1 only (``--remote-debugging-address``); this
client refuses any other host. Each call opens a short-lived WebSocket to
the page target, which keeps the client stateless and robust against
browser restarts.
"""
from __future__ import annotations
import base64
import itertools
import json
import urllib.request
from typing import Any
DEFAULT_PORT = 9222
_LOCAL_HOSTS = {"127.0.0.1", "localhost", "::1"}
class CDPError(Exception):
pass
class CDP:
def __init__(self, host: str = "127.0.0.1", port: int = DEFAULT_PORT, timeout: float = 5.0):
if host not in _LOCAL_HOSTS:
raise ValueError("DevTools is alleen lokaal bereikbaar")
self.host = host
self.port = port
self.timeout = timeout
self._ids = itertools.count(1)
# -- HTTP endpoints -------------------------------------------------
def _http_json(self, path: str, method: str = "GET") -> Any:
req = urllib.request.Request(f"http://{self.host}:{self.port}{path}", method=method)
try:
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
except (OSError, ValueError) as exc:
raise CDPError(f"DevTools niet bereikbaar: {exc}") from exc
def version(self) -> dict[str, Any]:
return self._http_json("/json/version")
def pages(self) -> list[dict[str, Any]]:
return [t for t in self._http_json("/json/list") if t.get("type") == "page"]
def main_page(self) -> dict[str, Any] | None:
pages = self.pages()
return pages[0] if pages else None
def new_page(self, url: str) -> dict[str, Any]:
from urllib.parse import quote
return self._http_json("/json/new?" + quote(url, safe=""), method="PUT")
# -- WebSocket commands ---------------------------------------------
def call(self, ws_url: str, method: str, params: dict[str, Any] | None = None, timeout: float | None = None) -> dict[str, Any]:
try:
import websocket # websocket-client (python3-websocket)
except ImportError as exc: # pragma: no cover - dependency missing
raise CDPError("python3-websocket is niet geïnstalleerd") from exc
msg_id = next(self._ids)
try:
# Chromium rejects DevTools WebSockets that carry a foreign Origin;
# we send none at all.
ws = websocket.create_connection(ws_url, timeout=timeout or self.timeout, suppress_origin=True)
except Exception as exc:
raise CDPError(f"Verbinding met DevTools mislukt: {exc}") from exc
try:
ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
while True:
reply = json.loads(ws.recv())
if reply.get("id") == msg_id:
break
except Exception as exc:
raise CDPError(f"DevTools-opdracht {method} mislukt: {exc}") from exc
finally:
try:
ws.close()
except Exception:
pass
if "error" in reply:
raise CDPError(f"{method}: {reply['error'].get('message', reply['error'])}")
return reply.get("result", {})
def _page_ws(self) -> str:
page = self.main_page()
if not page or not page.get("webSocketDebuggerUrl"):
raise CDPError("Geen browserpagina gevonden")
return page["webSocketDebuggerUrl"]
def current_url(self) -> str | None:
page = self.main_page()
return page.get("url") if page else None
def navigate(self, url: str) -> None:
self.call(self._page_ws(), "Page.navigate", {"url": url})
def reload(self, ignore_cache: bool = False) -> None:
self.call(self._page_ws(), "Page.reload", {"ignoreCache": ignore_cache})
def responsive(self, ws_url: str | None = None, timeout: float = 5.0) -> bool:
"""True when the page's renderer still executes JavaScript."""
try:
result = self.call(ws_url or self._page_ws(), "Runtime.evaluate", {"expression": "1+1", "returnByValue": True}, timeout=timeout)
except CDPError:
return False
return result.get("result", {}).get("value") == 2
def screenshot(self, quality: int = 70) -> bytes:
result = self.call(self._page_ws(), "Page.captureScreenshot", {"format": "jpeg", "quality": quality}, timeout=15)
try:
return base64.b64decode(result["data"])
except (KeyError, ValueError) as exc:
raise CDPError("Ongeldige screenshot ontvangen") from exc
def close_browser(self) -> None:
ws_url = self.version().get("webSocketDebuggerUrl")
if not ws_url:
raise CDPError("Geen browser-endpoint gevonden")
self.call(ws_url, "Browser.close")