From d34baa67be0dfdf6943d7601c6c09a1fe428847a Mon Sep 17 00:00:00 2001 From: Tim Eertmoed Date: Sun, 13 Sep 2026 13:23:39 +0200 Subject: [PATCH] Version 2.8.18: Versionspruefung im Update, zweiter Anlauf beim Bildschirm --- apps/tesm/pyproject.toml | 2 +- deploy/install.sh | 54 +++++++++++++ deploy/update.sh | 28 +++++++ docs/BETRIEB.md | 8 ++ packages/tesm-core/pyproject.toml | 2 +- .../tesm_core/static/tesm_core/js/screen.js | 81 ++++++++++++++++--- .../static/tesm_core/vendor/novnc/HERKUNFT.md | 27 +++++-- 7 files changed, 180 insertions(+), 22 deletions(-) diff --git a/apps/tesm/pyproject.toml b/apps/tesm/pyproject.toml index e7acff3..fff5db0 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.17" +version = "2.8.18" description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen" requires-python = ">=3.11" dependencies = [ diff --git a/deploy/install.sh b/deploy/install.sh index fda0af5..3a50bec 100644 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -39,6 +39,7 @@ HTTPS=0 HTTPS_PORT="" DOMAIN="" ACME_EMAIL="" +ERZWINGEN=0 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" @@ -49,6 +50,39 @@ warn() { printf '%s !!%s %s\n' "$COLOR_WARN" "$COLOR_OFF" "$*"; } fail() { printf '%s !!%s %s\n' "$COLOR_ERR" "$COLOR_OFF" "$*" >&2; exit 1; } info() { printf ' %s\n' "$*"; } +# -- Welche Fassung liegt hier, und welche laeuft schon? --------------------- +# +# Ein Installationslauf auf dieselbe Fassung macht nichts besser: er baut die +# Python-Umgebung neu, kopiert dieselben Dateien, laedt nginx neu und startet +# den Dienst -- mit der einzigen Wirkung, dass die Anwendung ein paar Sekunden +# weg ist. Wer "update.sh" in einen Zeitplan haengt, bekaeme das taeglich. +# +# Verglichen wird die Anwendung **und** der Kern: die beiden Nummern laufen +# absichtlich auseinander (siehe pyproject.toml), und eine Auslieferung kann +# denselben Anwendungsstand mit neuem Kern enthalten. + +#: Die Nummer aus einer pyproject.toml. Die erste Zeile "version = ..." gilt -- +#: weitere Vorkommen stehen in Abhaengigkeiten und gehen uns nichts an. +paket_version() { + sed -n 's/^version = "\([^"]*\)"/\1/p' "$1" 2>/dev/null | head -1 +} + +#: Was in dieser Installation laeuft: " ", sonst leer. +laufende_version() { + local wurzel="$1" modul="$2" + [[ -x "$wurzel/venv/bin/python" ]] || return 0 + "$wurzel/venv/bin/python" -c \ + "import $modul, tesm_core; print($modul.__version__, tesm_core.__version__)" \ + 2>/dev/null || true +} + +#: Ist das, was hier liegt, schon installiert? Eine eigene Funktion, weil genau +#: diese Frage sich pruefen lassen muss -- ohne einen Server aufzusetzen. +gleiche_version() { + local laeuft="$1" liegt="$2" + [[ -n "$laeuft" && "$laeuft" == "$liegt" ]] +} + #: Merker fuer die EXIT-Falle. Zurueckgerollt wird nur, was auch gesichert ist. SICHERUNG_STEHT=0 INSTALL_FERTIG=0 @@ -129,6 +163,7 @@ Verwendung: install.sh --app [Optionen] --acme-email Kontaktadresse fuer Let's Encrypt. --skip-packages Keine Systempakete installieren --yes Rueckfragen ueberspringen + --force Auch dann einrichten, wenn dieselbe Fassung schon laeuft USAGE } @@ -144,6 +179,7 @@ while [[ $# -gt 0 ]]; do --acme-email) ACME_EMAIL="${2:?}"; shift 2 ;; --skip-packages) SKIP_PACKAGES=1; shift ;; --yes|-y) ASSUME_YES=1; shift ;; + --force) ERZWINGEN=1; shift ;; -h|--help) usage; exit 0 ;; *) fail "unbekannte Option: $1" ;; esac @@ -261,6 +297,23 @@ FAILED_DIR="/srv/$NAME-failed-$TIMESTAMP" IS_UPDATE=0 [[ -f "$ROOT/data/app.db" ]] && IS_UPDATE=1 +# Laeuft hier schon genau das, was hier liegt? Dann ist Schluss, bevor +# irgendetwas angefasst wird -- und zwar mit Ausstiegscode 0: es ist nichts +# schiefgegangen, es war nur nichts zu tun. +MODUL="tesm" +[[ "$APP" == "tesm-license" ]] && MODUL="tesm_license" +LIEGT="$(paket_version "$REPO_DIR/apps/$APP/pyproject.toml") $( + paket_version "$REPO_DIR/packages/tesm-core/pyproject.toml")" +LAEUFT="$(laufende_version "$ROOT" "$MODUL")" +if [[ $IS_UPDATE -eq 1 && $ERZWINGEN -eq 0 ]] && gleiche_version "$LAEUFT" "$LIEGT"; then + step "Nichts zu tun" + info "$APP ${LIEGT%% *} (tesm-core ${LIEGT##* }) laeuft hier bereits." + info "Ein zweiter Durchlauf wuerde nur die Umgebung neu bauen und den" + info "Dienst neu starten -- ohne dass sich etwas aendert." + info "Trotzdem neu einrichten: --force" + exit 0 +fi + step "TESM-Installation" info "Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}" info "Ziel : $ROOT" @@ -278,6 +331,7 @@ else info "nginx : Port $HTTP_PORT (nur HTTP)" fi info "Modus : $([[ $IS_UPDATE -eq 1 ]] && echo Aktualisierung || echo Neuinstallation)" +info "Fassung : ${LIEGT%% *} (tesm-core ${LIEGT##* })${LAEUFT:+, bisher ${LAEUFT%% *}}" if [[ $ASSUME_YES -eq 0 ]]; then read -r -p "Fortfahren? [j/N] " answer diff --git a/deploy/update.sh b/deploy/update.sh index cec3c7d..eaf8a27 100644 --- a/deploy/update.sh +++ b/deploy/update.sh @@ -33,6 +33,8 @@ EXTRA=("$@") echo "Verwendung: update.sh [tag] [install.sh-Argumente ...]" >&2 echo "Beispiel : update.sh tesm latest --instance test" >&2 echo "Ohne Rueckfrage: --yes anhaengen (ohne Terminal zwingend)." >&2 + echo "Laeuft die Fassung schon, endet der Aufruf ohne Arbeit; --force setzt" >&2 + echo "sie trotzdem neu auf." >&2 exit 2 } @@ -173,6 +175,32 @@ release_holen() { release_holen +# Schon installiert? Dann gar nicht erst fragen. +# +# Dieselbe Pruefung steht in install.sh und gilt dort endgueltig (sie +# vergleicht auch den Kern). Hier steht sie noch einmal, weil ein Update, das +# ohne Wirkung bleibt, gar nicht erst nach einer Bestaetigung fragen soll -- +# und weil "es lief durch" sonst wie eine Aktualisierung aussieht. +ERZWINGEN=0 +for ((i = 0; i < ${#EXTRA[@]}; i++)); do + [[ "${EXTRA[i]}" == "--force" ]] && ERZWINGEN=1 +done +INSTALLATION="$APP" +for ((i = 0; i < ${#EXTRA[@]}; i++)); do + [[ "${EXTRA[i]}" == "--instance" ]] && INSTALLATION="$APP-${EXTRA[i + 1]:-}" +done +MODUL="tesm" +[[ "$APP" == "tesm-license" ]] && MODUL="tesm_license" +if [[ $ERZWINGEN -eq 0 && -x "/srv/$INSTALLATION/venv/bin/python" ]]; then + LAEUFT="$("/srv/$INSTALLATION/venv/bin/python" -c \ + "import $MODUL; print($MODUL.__version__)" 2>/dev/null || true)" + if [[ -n "$LAEUFT" && "v$LAEUFT" == "$TAG" ]]; then + echo "==> $APP $LAEUFT laeuft hier bereits -- nichts zu tun." + echo " Trotzdem neu einrichten: --force" + exit 0 + fi +fi + # --yes darf auch hier stehen, nicht nur bei install.sh und upgrade.sh. Es # bleibt in EXTRA: install.sh bekommt es ohnehin, doppelt schadet nicht. OHNE_RUECKFRAGE=0 diff --git a/docs/BETRIEB.md b/docs/BETRIEB.md index 1593ccf..8cdcd22 100644 --- a/docs/BETRIEB.md +++ b/docs/BETRIEB.md @@ -120,6 +120,14 @@ sudo bash /srv/tesm/deploy/update.sh tesm sudo bash /srv/tesm-license/deploy/update.sh tesm-license ``` +**Läuft die Fassung schon, passiert nichts.** `update.sh` vergleicht den +aufgelösten Tag mit dem, was installiert ist, und endet mit `0`, bevor es eine +Rückfrage stellt; `install.sh` prüft es noch einmal genauer (Anwendung **und** +Kern) und bricht vor dem ersten Eingriff ab. Ein zweiter Lauf auf dieselbe +Fassung baut sonst die Python-Umgebung neu, kopiert dieselben Dateien und +startet den Dienst neu -- ein paar Sekunden Ausfall ohne jede Wirkung. Wer das +trotzdem will (eine beschädigte Installation etwa), hängt `--force` an. + Alles nach dem Tag geht unverändert an `install.sh` weiter -- das ist der Weg, eine **benannte Instanz** zu aktualisieren: diff --git a/packages/tesm-core/pyproject.toml b/packages/tesm-core/pyproject.toml index 3f4366b..e39e87d 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.8" +version = "2.8.9" 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 bbff37c..bbe7bdd 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 @@ -11,18 +11,33 @@ import RFB from "../vendor/novnc/core/rfb.js"; -// Zwei Eingriffe in private Methoden der mitgelieferten Bibliothek -- beide -// nur, um zu *erfahren*, was schiefging; keiner greift in die Verhandlung ein. -// Bewusst hier und nicht in der Bibliothek selbst: eine neue Fassung von noVNC -// laesst sich dann austauschen, ohne eine Aenderung nachzupflegen -- faellt -// eine Methode weg, entfaellt nur die Auskunft, nicht die Sitzung. +// Drei Eingriffe in private Methoden der mitgelieferten Bibliothek. Bewusst +// hier und nicht in der Bibliothek selbst: eine neue Fassung von noVNC laesst +// sich dann austauschen, ohne eine Aenderung nachzupflegen -- faellt eine +// Methode weg, entfaellt nur die Auskunft, nicht die Sitzung. // -// Es stand hier einmal ein dritter, der die Wahl des Sicherheitsverfahrens -// umbog: ohne hinterlegte Zugangsdaten sollte ein Verfahren ohne Anmeldung -// gewinnen, weil der Server RA2ne zuerst nennt. Gut gemeint und falsch -- -// genau dieser Griff hat eine Sitzung, die lief, zum Abbruch gebracht: der -// RealVNC Server bietet "ohne Authentifizierung" zwar an, legt bei dieser Wahl -// aber auf. Die Reihenfolge des Servers ist seine Entscheidung, nicht unsere. +// Zwei davon holen nur Auskunft (_fail, _socketClose). Der dritte greift in +// die Wahl des Sicherheitsverfahrens ein -- aber erst, **nachdem** eines +// nachweislich nicht getragen hat. +// +// Die Vorgeschichte gehoert dazu, damit niemand den Schritt zurueckgeht: hier +// stand einmal eine Vorauswahl, die ohne hinterlegte Zugangsdaten ein +// Verfahren ohne Anmeldung erzwang, weil RealVNC RA2ne zuerst nennt. Das war +// geraten und falsch. Was jetzt hier steht, ist das Gegenteil einer +// Vermutung: der Server nennt seine Reihenfolge, die gilt -- und wenn seine +// erste Wahl die Verbindung abreissen laesst, ist das ein Befund, auf den man +// antworten darf. Gemessen am Geraet: RealVNC bietet 6 (RA2ne) an, waehlt man +// es, legt es auf; daneben liegt 1 (ohne Authentifizierung), und darueber +// laeuft auch der Viewer. +const istUnterstuetzt = RFB.prototype._isSupportedSecurityType; + +//: Verfahren, die in dieser Sitzung schon abgerissen sind. +const gescheitert = new Set(); + +//: Hoechstens ein zweiter Anlauf. Jeder Versuch ist eine Verbindung, und ein +//: RealVNC Server zaehlt abgebrochene Versuche mit -- wer dreimal anklopft, +//: sperrt sich aus. +const HOECHSTENS = 2; // Den *Grund* eines Fehlschlags kennt nur ``_fail`` -- das Ereignis // "disconnect" traegt ihn nicht, und der Weg ueber die Protokollausgabe // funktioniert nicht: die Bibliothek bindet ``console.error`` beim Laden, also @@ -72,6 +87,9 @@ function starten(wurzel) { ); let rfb = null; + let versuche = 0; + let stand = false; + let letzterZugang = {}; function melden(text, art = "") { zeile.textContent = text; @@ -86,9 +104,35 @@ function starten(wurzel) { if (rfb && rfb._rfbAuthScheme > 0) { teile.push(`gewähltes Verfahren: ${rfb._rfbAuthScheme}`); } + if (gescheitert.size) { + teile.push(`erfolglos: ${[...gescheitert].join(", ")}`); + } return teile.join(" · "); } + // Hat das gewählte Verfahren die Verbindung abreissen lassen? Dann ist das + // ein Befund und keine Vermutung -- der naechste Anlauf laesst es aus. + // + // Nur solange nie ein Bild stand: bricht eine **laufende** Sitzung ab, liegt + // es nicht am Anmeldeverfahren, und ein stiller Neuaufbau wuerde die Ursache + // verdecken. + function naechsterVersuch() { + const verfahren = rfb && rfb._rfbAuthScheme > 0 ? rfb._rfbAuthScheme : 0; + if (!verfahren || versuche >= HOECHSTENS || gescheitert.has(verfahren)) { + return false; + } + gescheitert.add(verfahren); + melden( + `Verfahren ${verfahren} hat die Verbindung nicht getragen – nächster Anlauf ` + + "mit dem nächsten, das der Server anbietet …", + "warnung", + ); + const zugang = letzterZugang; + rfb = null; + window.setTimeout(() => verbinden(zugang), 400); + return true; + } + // Steht die WebSocket-Strecke zu TESM überhaupt? Zwei sehr verschiedene // Ursachen sehen von aussen gleich aus: der VNC-Server mag nicht, oder die // Verbindung kommt nie zustande, weil ein Proxy zwischen Browser und TESM @@ -115,9 +159,19 @@ function starten(wurzel) { } function verbinden(zugang) { - melden("Verbindung wird aufgebaut …"); + melden(versuche ? "Nächster Versuch …" : "Verbindung wird aufgebaut …"); letzterGrund = ""; letzterCode = 0; + letzterZugang = zugang; + versuche += 1; + + // Was abgerissen ist, wird nicht noch einmal gewaehlt. Sonst faende noVNC + // dasselbe Verfahren wieder zuerst -- und der zweite Anlauf waere eine + // Wiederholung des ersten. + RFB.prototype._isSupportedSecurityType = function (typ) { + if (gescheitert.has(typ)) return false; + return istUnterstuetzt.call(this, typ); + }; rfb = new RFB(leinwand, adresse, { credentials: zugang, @@ -131,6 +185,7 @@ function starten(wurzel) { rfb.focusOnClick = true; rfb.addEventListener("connect", () => { + stand = true; melden(`Verbunden mit ${wurzel.dataset.ziel}`, "ok"); abfrage.hidden = true; leinwand.hidden = false; @@ -144,6 +199,8 @@ function starten(wurzel) { knopfStrg.hidden = true; if (ereignis.detail && ereignis.detail.clean) { melden("Sitzung beendet."); + } else if (!stand && naechsterVersuch()) { + return; } else { const erklaerung = grund(); melden( diff --git a/packages/tesm-core/src/tesm_core/static/tesm_core/vendor/novnc/HERKUNFT.md b/packages/tesm-core/src/tesm_core/static/tesm_core/vendor/novnc/HERKUNFT.md index 1e8193d..e6c40e1 100644 --- a/packages/tesm-core/src/tesm_core/static/tesm_core/vendor/novnc/HERKUNFT.md +++ b/packages/tesm-core/src/tesm_core/static/tesm_core/vendor/novnc/HERKUNFT.md @@ -21,14 +21,25 @@ Datei -- mit Grund und Fundstelle, damit ein Wechsel auf eine neuere Fassung nicht stillschweigend darueberlaeuft. **Was TESM von aussen umbiegt** (also *nicht* in diesen Dateien steht, sondern -in `js/screen.js`): `RFB.prototype._isSupportedSecurityType` wird ueberschrieben, -solange keine Zugangsdaten vorliegen und der Server ein Verfahren ohne -Anmeldung anbietet. Grund: noVNC nimmt "das erste, das ich kann, in der -Reihenfolge des Servers"; ein RealVNC Server nennt RA2ne vor "ohne -Authentifizierung", und der Browser liefe in eine Passwortabfrage, obwohl -daneben der Weg offensteht, den auch der RealVNC Viewer nimmt. Faellt die -Methode in einer neueren Fassung weg, greift die Vorauswahl nicht mehr -- die -Sitzung laeuft dann wie ohne sie, es bricht nichts. +in `js/screen.js`): + +* `RFB.prototype._fail` und `_socketClose` -- nur, um den Grund eines + Abbruchs zu erfahren. noVNC meldet ihn sonst ausschliesslich in die + Entwicklerkonsole, und `console.error` mitzulesen traegt nicht: die + Bibliothek bindet es beim Laden. +* `RFB.prototype._isSupportedSecurityType` -- **erst nachdem** ein Verfahren + die Verbindung nachweislich hat abreissen lassen. Dann wird es fuer den + zweiten Anlauf ausgelassen, damit noVNC nicht wieder dasselbe zuerst findet. + Hoechstens zwei Anlaeufe, denn jeder ist eine Verbindung, und ein RealVNC + Server zaehlt abgebrochene mit. + + Hier stand einmal das Gegenteil: eine **Vorauswahl**, die ohne hinterlegte + Zugangsdaten ein Verfahren ohne Anmeldung erzwang. Das war geraten und hat + eine laufende Sitzung gebrochen. Der Unterschied ist nicht der Code, sondern + der Anlass: eine Vermutung vorher gegen einen Befund hinterher. + +Faellt eine dieser Methoden in einer neueren Fassung weg, entfaellt nur die +Auskunft bzw. der zweite Anlauf -- die Sitzung laeuft wie ohne sie. **Was noVNC an Sicherheitsverfahren spricht** (`core/rfb.js`, `_isSupportedSecurityType`): `1` None, `2` VNC-Passwort, `6` RA2ne (RSA-AES