diff --git a/apps/tesm/pyproject.toml b/apps/tesm/pyproject.toml
index f107f40..f94f43a 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.24"
+version = "2.8.25"
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
requires-python = ">=3.11"
dependencies = [
diff --git a/apps/tesm/src/tesm/blueprints/maintenance.py b/apps/tesm/src/tesm/blueprints/maintenance.py
index b6df018..8b0f0ec 100644
--- a/apps/tesm/src/tesm/blueprints/maintenance.py
+++ b/apps/tesm/src/tesm/blueprints/maintenance.py
@@ -174,7 +174,7 @@ def status():
# Geraete zu uebertragen.
"output": (
service.letzte_zeilen(str(job["output"] or ""))
- if job["state"] in {"pending", "running"}
+ if service.eben_fertig(dict(job))
else ""
),
}
diff --git a/apps/tesm/src/tesm/blueprints/screen.py b/apps/tesm/src/tesm/blueprints/screen.py
index ffed11b..533c0d7 100644
--- a/apps/tesm/src/tesm/blueprints/screen.py
+++ b/apps/tesm/src/tesm/blueprints/screen.py
@@ -70,6 +70,30 @@ def _geraet(conn: Any, device_id: int) -> dict[str, Any] | None:
return device
+def _erklaerung(fehler: OSError) -> str:
+ """Die Fehlernummer in einem Satz, der weiterhilft.
+
+ "No route to host" und "Connection refused" sehen im Protokoll gleich
+ harmlos aus und bedeuten das Gegenteil voneinander: einmal kommt man gar
+ nicht hin, einmal kommt man hin und niemand hoert zu. Und beides hat mit
+ einem gelungenen Ping nichts zu tun -- der prueft eine andere Schicht.
+ """
+ import errno as fehlernummern
+
+ nummer = getattr(fehler, "errno", None)
+ if nummer == fehlernummern.EHOSTUNREACH:
+ return (
+ f"{fehler} – kein Weg dorthin. Eine Firewall oder eine Route steht "
+ "dazwischen; ein gelungener Ping sagt darüber nichts, der prüft eine "
+ "andere Schicht"
+ )
+ if nummer == fehlernummern.ECONNREFUSED:
+ return f"{fehler} – das Gerät ist erreichbar, aber auf diesem Port hört niemand"
+ if nummer == fehlernummern.ETIMEDOUT:
+ return f"{fehler} – keine Antwort; Paketfilter verwerfen meist stillschweigend"
+ return str(fehler)
+
+
def _port(conn: Any) -> int:
extension = core()
if not extension.settings.get(conn, "vnc_enabled"):
@@ -314,10 +338,26 @@ def _strecke(ws, device_id: int) -> None: # type: ignore[no-untyped-def]
ws.close(1008, "Für diesen Client ist kein Ziel bekannt.")
return
- try:
- gegenstelle = socket.create_connection((host, port), timeout=VERBINDUNGSLIMIT)
- except OSError as exc:
- ws.close(1011, f"{host}:{port} antwortet nicht ({exc}).")
+ gegenstelle = None
+ ausweich = str(device.get("ip") or "").strip()
+ for versuch, ziel_host in enumerate([host] + ([ausweich] if ausweich != host else [])):
+ try:
+ gegenstelle = socket.create_connection((ziel_host, port), timeout=VERBINDUNGSLIMIT)
+ if versuch:
+ # Der Name fuehrte ins Leere, die Adresse nicht. Das ist eine
+ # Auskunft ueber das Netz, keine Nebensaechlichkeit: irgendwo
+ # zeigt ein Eintrag woandershin.
+ if live_log := extension.registry.get("live_log"):
+ live_log.warning(
+ "vnc",
+ f"{host} war nicht erreichbar – Sitzung läuft über {ziel_host}",
+ )
+ host = ziel_host
+ break
+ except OSError as exc:
+ letzter = exc
+ if gegenstelle is None:
+ ws.close(1011, f"{host}:{port} antwortet nicht ({_erklaerung(letzter)}).")
return
# Das Zeitlimit galt dem *Aufbau* -- als Abbruchgrund taugt es nicht.
#
diff --git a/apps/tesm/src/tesm/services/maintenance.py b/apps/tesm/src/tesm/services/maintenance.py
index 601fc5c..81dcd9e 100644
--- a/apps/tesm/src/tesm/services/maintenance.py
+++ b/apps/tesm/src/tesm/services/maintenance.py
@@ -495,6 +495,11 @@ def run_update(
)
+#: So lange gilt ein Auftrag als "eben fertig geworden" und schickt seine
+#: letzten Zeilen noch mit. Ohne das verschwaende die Ausgabe genau in dem
+#: Augenblick, in dem sie interessant wird: wenn der Befehl durch ist.
+EBEN_FERTIG_SEKUNDEN = 120
+
#: So viele Zeilen gehen ins Mitlesen. Genug, um zu sehen, was gerade
#: passiert; wenig genug, dass eine Abfrage alle zwei Sekunden nichts kostet.
MITLESEN_ZEILEN = 40
@@ -506,6 +511,22 @@ def letzte_zeilen(ausgabe: str, anzahl: int = MITLESEN_ZEILEN) -> str:
return "\n".join(zeilen[-anzahl:])
+def eben_fertig(job: dict[str, Any], sekunden: int = EBEN_FERTIG_SEKUNDEN) -> bool:
+ """Ist dieser Auftrag gerade eben fertig geworden?"""
+ if job.get("state") in {"pending", "running"}:
+ return True
+ stempel = str(job.get("finished_at") or job.get("updated_at") or "")
+ if not stempel:
+ return False
+ try:
+ moment = datetime.fromisoformat(stempel)
+ except ValueError:
+ return False
+ if moment.tzinfo is None:
+ moment = moment.replace(tzinfo=timezone.utc)
+ return (datetime.now(timezone.utc) - moment).total_seconds() < sekunden
+
+
def befehl_pruefen(befehl: str) -> str:
"""Gibt den bereinigten Befehl zurueck -- oder wirft mit Klartext.
diff --git a/apps/tesm/src/tesm/templates/tesm/maintenance.html b/apps/tesm/src/tesm/templates/tesm/maintenance.html
index fbcd71d..51d7d04 100644
--- a/apps/tesm/src/tesm/templates/tesm/maintenance.html
+++ b/apps/tesm/src/tesm/templates/tesm/maintenance.html
@@ -226,13 +226,13 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
Gestartet Gerät Art Zustand Fortschritt Meldung