Terminal: das Fenster sagt jetzt selbst, ob die Bruecke laeuft
Der Betreiber oeffnete ein Terminal und las "Keine Verbindung — laeuft der
Terminal-Dienst, und steht der Tunnel?". Das war keine Diagnose, das war eine
Rueckfrage an den, der gerade unterwegs ist und nicht nachsehen kann.
Der Grund ist eine Eigenart des Protokolls: scheitert ein WebSocket schon am
Handschlag, bekommt die Seite laut Norm KEINEN HTTP-Status — `event.code` ist
1006, sonst nichts. Das ist Absicht (sonst waere ein Socket ein Portscanner) und
macht ausgerechnet die Unterscheidung unmoeglich, auf die es hier ankommt: laeuft
die Bruecke nicht, oder ist die Leitung weg? Beides sah gleich aus.
Eine gewoehnliche Anfrage an dieselbe Stelle darf den Status sehr wohl sehen.
Scheitert der Socket, ohne dass je ein Byte kam, fragt das Fenster deshalb einmal
nach und liest die Antwort:
502/503/504 nginx erreicht die Bruecke nicht -> "Der Terminal-Dienst laeuft
nicht", mit dem Befehl, der ihn zurueckholt, und dem Hinweis auf
den geteilten Netz-Namensraum
404 oeffentlicher Hostname -> "Auf diesem Namen gibt es kein Terminal"
sonst die Bruecke lebt, die Sitzung ist an etwas anderem gescheitert;
"Keine Verbindung" bleibt stehen
gar nichts die Anfrage kam nicht einmal los -> die Leitung ist wirklich weg
Alle drei Faelle nachgemessen, nicht angenommen: Bruecke laeuft -> 426,
oeffentlicher Name -> 404, `docker compose stop terminal` -> 502. Und danach mit
gestoppter Bruecke im Browser angesehen, im selben Zustand, in dem der Betreiber
gerade steht.
Dazu ein Test, der jeden Schluessel abdeckt, den terminal.js an showStage()
uebergeben kann — ein fehlender schriebe "undefined" ins Fenster, und zwar
ausgerechnet in dem Moment, in dem etwas kaputt ist.
2507 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main
parent
624ad0bfff
commit
92ebd5d545
|
|
@ -167,6 +167,16 @@ return [
|
|||
'title' => 'Keine Sitzung auf :host',
|
||||
'note' => 'Die Brücke kam nicht bis zur Kommandozeile: die Maschine antwortet nicht, ihr Fingerabdruck weicht vom hinterlegten ab, oder sie hat den Flottenschlüssel abgewiesen — Letzteres ist das Wahrscheinlichste, wenn der Host neu aufgesetzt wurde. Was davon, steht im Protokoll des Terminal-Dienstes; dieses Fenster erfährt es absichtlich nicht.',
|
||||
],
|
||||
// Die beiden hier stehen für Fälle, die der Browser von sich aus gar
|
||||
// nicht auseinanderhalten kann — siehe resources/js/terminal.js.
|
||||
'service_down' => [
|
||||
'title' => 'Der Terminal-Dienst läuft nicht',
|
||||
'note' => 'Die Konsole steht, die Brücke dahinter nicht: nginx meldet für diese Stelle 502. Auf dem Server hilft „docker compose up -d terminal". Sie teilt sich den Netz-Namensraum des Provisioning-Containers und muss nach jedem Neustart des Tunnels selbst mit neu gestartet werden — sonst lauscht sie in einem Namensraum, den es nicht mehr gibt, und meldet dabei weiter „healthy".',
|
||||
],
|
||||
'not_here' => [
|
||||
'title' => 'Auf diesem Namen gibt es kein Terminal',
|
||||
'note' => 'Die Brücke ist Werkzeug der Konsole und antwortet auf öffentlichen Hostnamen mit 404 — dieselbe Regel wie für /admin. Öffne die Konsole unter ihrem eigenen Namen.',
|
||||
],
|
||||
'retry' => 'Neu verbinden',
|
||||
],
|
||||
|
||||
|
|
|
|||
|
|
@ -167,6 +167,16 @@ return [
|
|||
'title' => 'No session on :host',
|
||||
'note' => 'The bridge never reached a command line: the machine is not answering, its fingerprint differs from the one on file, or it refused the fleet key — the last is the likeliest once a host has been rebuilt. Which of them is in the terminal service log; this window is deliberately not told.',
|
||||
],
|
||||
// These two cover cases the browser cannot tell apart on its own —
|
||||
// see resources/js/terminal.js.
|
||||
'service_down' => [
|
||||
'title' => 'The terminal service is not running',
|
||||
'note' => 'The console is up, the bridge behind it is not: nginx answers 502 for this path. On the server, "docker compose up -d terminal" fixes it. The bridge shares the provisioning container\'s network namespace and has to be restarted along with the tunnel — otherwise it listens inside a namespace that no longer exists while still reporting "healthy".',
|
||||
],
|
||||
'not_here' => [
|
||||
'title' => 'No terminal on this hostname',
|
||||
'note' => 'The bridge is a console tool and answers 404 on public hostnames — the same rule as /admin. Open the console under its own name.',
|
||||
],
|
||||
'retry' => 'Reconnect',
|
||||
],
|
||||
|
||||
|
|
|
|||
|
|
@ -112,8 +112,33 @@ if (root && root.dataset.ticket) {
|
|||
socket.onclose = (event) => {
|
||||
if (event.code === 4401) return showStage('rejected', true)
|
||||
if (event.code === 4502) return showStage('unreachable', true)
|
||||
if (received) return showStage('ended', true)
|
||||
|
||||
showStage(received ? 'ended' : 'lost', true)
|
||||
// Nie eine Sitzung gehabt — und genau hier wusste das Fenster bisher
|
||||
// nichts und riet.
|
||||
//
|
||||
// Scheitert ein WebSocket schon am Handschlag, bekommt die Seite laut
|
||||
// Norm KEINEN HTTP-Status: `event.code` ist 1006, sonst nichts. Das ist
|
||||
// Absicht (sonst wäre ein Socket ein Portscanner), macht aber genau die
|
||||
// Auskunft unmöglich, die ein Betreiber hier braucht — läuft die Brücke
|
||||
// nicht, oder ist die Leitung weg? Beides sah gleich aus.
|
||||
//
|
||||
// Eine gewöhnliche Anfrage an dieselbe Stelle darf den Status sehen.
|
||||
// nginx antwortet dort mit 502, solange die Brücke nicht erreichbar ist
|
||||
// (`resolver` + Variable, docker/nginx/default.conf), mit 404 auf einem
|
||||
// öffentlichen Namen, und sonst mit dem, was die Brücke selbst sagt —
|
||||
// ein Upgrade-Fehler, aber eben von ihr. Das reicht zum Unterscheiden.
|
||||
showStage('lost', true)
|
||||
|
||||
fetch('/terminal/ws', { cache: 'no-store' })
|
||||
.then((probe) => {
|
||||
if ([502, 503, 504].includes(probe.status)) showStage('service_down', true)
|
||||
else if (probe.status === 404) showStage('not_here', false)
|
||||
})
|
||||
.catch(() => {
|
||||
// Die Anfrage kam nicht einmal los: dann ist wirklich die
|
||||
// Leitung weg, und „Keine Verbindung" von oben stimmt.
|
||||
})
|
||||
}
|
||||
|
||||
// Kein `onerror`: ein Fehler zieht immer ein `close` nach sich, und dort oben
|
||||
|
|
|
|||
|
|
@ -43,6 +43,14 @@
|
|||
'title' => __('hosts.terminal.stage.unreachable.title', ['host' => $host->name]),
|
||||
'note' => __('hosts.terminal.stage.unreachable.note', ['host' => $host->name]),
|
||||
],
|
||||
'service_down' => [
|
||||
'title' => __('hosts.terminal.stage.service_down.title'),
|
||||
'note' => __('hosts.terminal.stage.service_down.note'),
|
||||
],
|
||||
'not_here' => [
|
||||
'title' => __('hosts.terminal.stage.not_here.title'),
|
||||
'note' => __('hosts.terminal.stage.not_here.note'),
|
||||
],
|
||||
];
|
||||
@endphp
|
||||
<div class="relative flex h-screen w-screen flex-col overflow-hidden bg-ink"
|
||||
|
|
|
|||
|
|
@ -203,6 +203,13 @@ it('setzt in jede Meldung, die der Browser später zeigt, den Hostnamen wirklich
|
|||
expect($messages['unreachable']['title'])->toContain($host->name)
|
||||
->and($messages['ended']['note'])->toContain($host->name)
|
||||
->and($messages['lost']['note'])->toContain($host->name);
|
||||
|
||||
// Jeder Schlüssel, den terminal.js an `showStage()` übergeben kann, muss
|
||||
// hier liegen — sonst stünde im Fenster „undefined", und zwar ausgerechnet
|
||||
// in dem Moment, in dem etwas kaputt ist.
|
||||
expect(array_keys($messages))->toEqualCanonicalizing([
|
||||
'ended', 'lost', 'rejected', 'unreachable', 'service_down', 'not_here',
|
||||
]);
|
||||
});
|
||||
|
||||
it('bietet den Terminal-Knopf nur dem, der ihn drücken darf', function () {
|
||||
|
|
|
|||
Loading…
Reference in New Issue