diff --git a/apps/tesm/pyproject.toml b/apps/tesm/pyproject.toml index af9325b..0580e19 100644 --- a/apps/tesm/pyproject.toml +++ b/apps/tesm/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm" -version = "2.8.19" +version = "2.8.20" description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen" requires-python = ">=3.11" dependencies = [ diff --git a/apps/tesm/src/tesm/blueprints/screen.py b/apps/tesm/src/tesm/blueprints/screen.py index 60c438f..c3e6d4f 100644 --- a/apps/tesm/src/tesm/blueprints/screen.py +++ b/apps/tesm/src/tesm/blueprints/screen.py @@ -50,6 +50,10 @@ VERBINDUNGSLIMIT = 6.0 #: bis ein Schub weitergereicht wird. BLOCK = 32 * 1024 +#: Wie oft nachgesehen wird, ob die Sitzung noch etwas tut. Kurz genug, dass +#: das Trennen puenktlich ist, lang genug, dass das Nachsehen nichts kostet. +TAKT = 1.0 + def _geraet(conn: Any, device_id: int) -> dict[str, Any] | None: device = inventory.get_device(conn, device_id) @@ -231,14 +235,19 @@ def _strecke(ws, device_id: int) -> None: # type: ignore[no-untyped-def] except OSError as exc: ws.close(1011, f"{host}:{port} antwortet nicht ({exc}).") return - # Das Zeitlimit galt dem *Aufbau* -- danach muss es weg. + # Das Zeitlimit galt dem *Aufbau* -- als Abbruchgrund taugt es nicht. # # ``create_connection`` setzt es an den Socket, und dort gilt es fuer jede # weitere Leseoperation. Ein Bildschirm, auf dem sich gerade nichts tut, # schickt sekundenlang nichts; nach sechs Sekunden Stille brach das Lesen # mit einer Zeitueberschreitung ab, und die Sitzung war weg. Genau so # beobachtet: Bild steht, nach fuenf bis zehn Sekunden Schluss. - gegenstelle.settimeout(None) + # + # Stattdessen ein kurzer Takt: das Lesen laeuft immer wieder in ein + # Zeitlimit, und *das* ist kein Abbruch, sondern der Anlass nachzusehen, + # ob die Sitzung ueberhaupt noch etwas tut. + gegenstelle.settimeout(TAKT) + leerlauf = int(extension.settings.get(conn, "vnc_idle_timeout") or 0) # Ins **laufende** Protokoll, nicht in die Aenderungskette: eine # Bildschirmsitzung aendert nichts an der Anlage. Das Aenderungsprotokoll @@ -253,21 +262,64 @@ def _strecke(ws, device_id: int) -> None: # type: ignore[no-untyped-def] laeuft = threading.Event() laeuft.set() + #: Wann zuletzt etwas floss -- in *irgendeine* Richtung. Eine Liste, weil + #: beide Richtungen daran schreiben und eine Zuweisung in Python nicht in + #: die andere hineinlaufen kann. + zuletzt = [time.monotonic()] + #: Grund des Endes, falls wir selbst getrennt haben. + schluss = [""] + #: Kam ueberhaupt schon ein Byte vom Geraet? + gehoert = [False] def vom_geraet() -> None: """Alles, was der VNC-Server schickt, geht als Binaerrahmen weiter.""" try: while laeuft.is_set(): - daten = gegenstelle.recv(BLOCK) + try: + daten = gegenstelle.recv(BLOCK) + except socket.timeout: + # Kein Fehler, nur Stille -- und die bedeutet zweierlei, + # je nachdem, ob die Sitzung schon stand. + # + # **Vor dem ersten Byte** ist Stille ein Befund: der Port + # nimmt an, aber es kommt nichts. Genau so verhaelt sich + # ein RealVNC Server, der die Adresse gerade aussperrt. Wer + # das nicht erfaehrt, sieht ewig "Verbindung wird + # aufgebaut" -- und sucht den Fehler bei uns. + if not gehoert[0] and time.monotonic() - begonnen > VERBINDUNGSLIMIT: + schluss[0] = ( + f"{host}:{port} nimmt die Verbindung an, schickt aber nichts. " + "So verhält sich ein VNC-Server, der die Adresse gerade " + "aussperrt (RealVNC: BlacklistTimeout) – ein Neustart des " + "VNC-Dienstes auf dem Gerät räumt das ab." + ) + break + # **Danach** ist Stille der Normalfall: ein Bildschirm, auf + # dem sich nichts tut, schickt nichts. + if gehoert[0] and leerlauf and time.monotonic() - zuletzt[0] > leerlauf: + schluss[0] = ( + f"Sitzung nach {leerlauf} Sekunden ohne Bewegung getrennt" + ) + break + continue if not daten: + if not gehoert[0]: + schluss[0] = ( + f"{host}:{port} hat die Verbindung sofort wieder geschlossen." + ) break + gehoert[0] = True + zuletzt[0] = time.monotonic() ws.send(daten) except Exception: # noqa: BLE001 - Abbruch ist der Normalfall, nicht die Stoerung pass finally: laeuft.clear() try: - ws.close() + if schluss[0]: + ws.close(1000 if gehoert[0] else 1011, schluss[0]) + else: + ws.close() except Exception: # noqa: BLE001 pass @@ -284,6 +336,7 @@ def _strecke(ws, device_id: int) -> None: # type: ignore[no-untyped-def] # waere entweder ein Irrtum oder ein Versuch -- beides endet # hier und nicht auf dem Geraet. continue + zuletzt[0] = time.monotonic() gegenstelle.sendall(nachricht) except Exception: # noqa: BLE001 logger.debug("Bildschirmsitzung beendet (%s)", device["name"], exc_info=True) @@ -298,5 +351,6 @@ def _strecke(ws, device_id: int) -> None: # type: ignore[no-untyped-def] if live is not None: live.info( "vnc", - f"Bildschirmsitzung {device['name']} beendet ({dauer}s)", + f"Bildschirmsitzung {device['name']} beendet ({dauer}s)" + + (f" – {schluss[0]}" if schluss[0] else ""), ) diff --git a/apps/tesm/src/tesm/blueprints/settings.py b/apps/tesm/src/tesm/blueprints/settings.py index 0794709..3397ef5 100644 --- a/apps/tesm/src/tesm/blueprints/settings.py +++ b/apps/tesm/src/tesm/blueprints/settings.py @@ -105,6 +105,7 @@ def system(): "dashboard_public", "vnc_enabled", "vnc_port", + "vnc_idle_timeout", "vnc_warn_unencrypted_off", "terminal_switches_enabled", "terminal_clients_enabled", @@ -148,6 +149,7 @@ def system_save(): "dashboard_public", "vnc_enabled", "vnc_port", + "vnc_idle_timeout", "vnc_warn_unencrypted_off", "terminal_switches_enabled", "terminal_clients_enabled", diff --git a/apps/tesm/src/tesm/settings_spec.py b/apps/tesm/src/tesm/settings_spec.py index a8bef06..26fd650 100644 --- a/apps/tesm/src/tesm/settings_spec.py +++ b/apps/tesm/src/tesm/settings_spec.py @@ -66,6 +66,18 @@ TESM_SETTINGS: tuple[SettingSpec, ...] = ( minimum=1, maximum=65535, ), + SettingSpec( + "vnc_idle_timeout", + 300, + "int", + "Bildschirmsitzung bei Untätigkeit trennen nach (Sekunden)", + hint="Gezählt wird, wenn in **keine** Richtung mehr etwas fließt: der Bildschirm " + "ändert sich nicht und es kommt keine Eingabe. Eine Sitzung, die jemand offen " + "vergisst, hält sonst die Verbindung zum Gerät. 0 schaltet die Trennung ab.", + section="monitoring", + minimum=0, + maximum=86400, + ), SettingSpec( "vnc_warn_unencrypted_off", False, diff --git a/apps/tesm/src/tesm/templates/tesm/settings_system.html b/apps/tesm/src/tesm/templates/tesm/settings_system.html index d994575..a52d128 100644 --- a/apps/tesm/src/tesm/templates/tesm/settings_system.html +++ b/apps/tesm/src/tesm/templates/tesm/settings_system.html @@ -46,17 +46,6 @@ {{ checkbox('dashboard_public', 'Statusübersicht ohne Anmeldung erreichbar', checked=values.dashboard_public, hint_text=specs['dashboard_public'].hint) }} - {{ checkbox('vnc_enabled', 'Bildschirmzugriff (VNC) anbieten', - checked=values.vnc_enabled, - hint_text=specs['vnc_enabled'].hint) }} -
- {{ field('vnc_port', 'VNC-Port', value=values.vnc_port, type='number', - hint_text=specs['vnc_port'].hint) }} -
- {{ checkbox('vnc_warn_unencrypted_off', - 'Warnung des Viewers vor unverschlüsselter Verbindung unterdrücken', - checked=values.vnc_warn_unencrypted_off, - hint_text=specs['vnc_warn_unencrypted_off'].hint) }} @@ -117,6 +106,39 @@ +
+
+
+
{{ icon('monitor', size=17) }} Bildschirm (VNC)
+
+ Dieselben Regeln wie bei der Konsole: eigenes Recht, frische + Passwortbestätigung, getrennte Zwischenablage. Der Knopf erscheint nur, wo + ein VNC-Server antwortet – das prüft der Überwachungsdurchlauf mit. +
+
+
+
+ +
+ {{ checkbox('vnc_enabled', 'Bildschirmzugriff (VNC) anbieten', + checked=values.vnc_enabled, + hint_text=specs['vnc_enabled'].hint) }} +
+ {{ field('vnc_port', 'VNC-Port', value=values.vnc_port, type='number', + hint_text=specs['vnc_port'].hint) }} + {{ field('vnc_idle_timeout', 'Trennen bei Untätigkeit (Sekunden)', + value=values.vnc_idle_timeout, type='number', + hint_text=specs['vnc_idle_timeout'].hint) }} +
+ {{ checkbox('vnc_warn_unencrypted_off', + 'Warnung des Viewers vor unverschlüsselter Verbindung unterdrücken', + checked=values.vnc_warn_unencrypted_off, + hint_text=specs['vnc_warn_unencrypted_off'].hint) }} +
+ +
+
+
diff --git a/packages/tesm-core/pyproject.toml b/packages/tesm-core/pyproject.toml index e39e87d..65affe2 100644 --- a/packages/tesm-core/pyproject.toml +++ b/packages/tesm-core/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm-core" -version = "2.8.9" +version = "2.8.10" description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver" requires-python = ">=3.11" dependencies = [ diff --git a/packages/tesm-core/src/tesm_core/static/tesm_core/js/screen.js b/packages/tesm-core/src/tesm_core/static/tesm_core/js/screen.js index bbe7bdd..f42519e 100644 --- a/packages/tesm-core/src/tesm_core/static/tesm_core/js/screen.js +++ b/packages/tesm-core/src/tesm_core/static/tesm_core/js/screen.js @@ -198,7 +198,10 @@ function starten(wurzel) { knopfTrennen.hidden = true; knopfStrg.hidden = true; if (ereignis.detail && ereignis.detail.clean) { - melden("Sitzung beendet."); + // Beim sauberen Schluss steht der Grund im Schliessrahmen -- etwa, + // dass TESM wegen Untätigkeit getrennt hat. Ohne ihn stuende da + // "beendet", und niemand wuesste, warum das Bild weg ist. + melden(letzterGrund ? `Sitzung beendet – ${letzterGrund}` : "Sitzung beendet."); } else if (!stand && naechsterVersuch()) { return; } else {