Instanzmanager Stufe 2: Kestrel auf dem Socket, Instanz kopflos (#528)

Eine Instanz ist jetzt ohne Port ansprechbar. InstanceEndpoint liefert die
Adresse: unter Linux <Instanz>/manager.sock, unter Windows eine Named Pipe,
deren Name aus einem SHA-256-Fingerabdruck des Instanzpfades entsteht. Zwei
Instanzen bekommen damit zwangslaeufig zwei Adressen, ohne dass jemand
etwas vergeben muss - genau der Unterschied zur Vorbelegung 7001, um die
sich zwei Instanzen stillschweigend streiten.

Der Socket ist immer da, auch ohne Weboberflaeche. Er gehoert nicht zu
#494, sondern ist die Steuerflaeche der Instanz: webUiEnabled schaltet den
TCP-Zugang, nicht den Socket. WebUiHost heisst deshalb jetzt
InstanceHttpHost - ein Kestrel, zwei Zugaenge.

Ueber TCP gilt Token-Pflicht, ueber den Socket nicht: Dort steht die Grenze
am Dateisystem (0600 unter Linux, CurrentUserOnly bei der Named Pipe,
explizit gesetzt statt auf die Vorgabe vertraut). Wer die Socketdatei
oeffnen darf, sieht ohnehin in das Verzeichnis, in dem alles liegt, was ein
Token schuetzen wuerde.

Woran die beiden Zugaenge auseinandergehalten werden, ist der Punkt, an dem
ein Irrtum teuer waere. Bei Socket-Verbindungen stehen in
HttpContext.Connection keine IP-Adressen - daraus zu schliessen "keine IP,
also Socket" waere eine Vermutung an der Stelle, an der ein Torwaechter
steht, und sie irrt in die falsche Richtung: Im Zweifel liesse sie eine
TCP-Anfrage ohne Token durch. Der Socket-Zuhoerer haengt deshalb jeder
Verbindung selbst eine Marke an (LocalTransport, Verbindungs-
Zwischenschicht ueber ListenOptions.Use); wo sie fehlt, gilt Token-Pflicht.

Ein belegter Port kostet nur noch die Weboberflaeche. Vorher gab TryStart
in dem Fall null zurueck und die ganze HTTP-Flaeche war weg. Seit dem
Socket ist das die falsche Rangfolge: Ihn zu verlieren, weil eine zweite
Instanz laeuft, hiesse die Instanz unerreichbar zu machen - in genau dem
Fall, fuer den es den Manager gibt.

Kopfloser Betrieb ueber --headless --instance <Pfad>. Die Verzweigung sitzt
in Program.Main vor BuildAvaloniaApp, damit auf einem Server ohne
Bildschirmsitzung kein Fenstersystem angefasst wird. Gefragt wird nichts:
Instanz aus dem Argument, Lizenz ueber das bereits vorhandene
NonInteractiveLicensePrompt, Telegram-Anmeldung aus.

Nachgemessen, nicht angenommen: Kopflose Instanz auf Instance-Test
gestartet, kein Fenster, und eine rohe HTTP/1.1-Anfrage aus einem fremden
Prozess (PowerShell, direkt auf die Pipe) bekam 200 OK auf /api/info, ohne
Token. Das ist die Abnahmebedingung fuer Stufe 2.

Korrektur zur eigenen Ankuendigung: webServerPort entfaellt nicht mit
Stufe 2, sondern mit Stufe 4. Solange kein Manager die Weboberflaeche
traegt, hiesse Wegnehmen, sie ersatzlos abzuschalten.

Zwei Luecken bleiben offen und gehoeren auf die Linux-Testmaschine: Die
Socketdatei-Faelle sind unter Windows gegenstandslos und werden dort mit
sichtbarer Begruendung uebersprungen - der Unix-Domain-Socket selbst ist
auf der Entwicklungsmaschine also ungeprueft, obwohl er die Zielplattform
ist. Und das geordnete Ende auf SIGTERM liess sich hier nicht ausloesen:
Windows kennt das Signal fuer einen fensterlosen Prozess nicht.

Build ohne Warnungen, 714 Tests gruen, 8 uebersprungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-08 22:08:23 +02:00
co-authored by Claude Opus 5
parent facd21459c
commit 8686d13999
14 changed files with 903 additions and 273 deletions
+48 -6
View File
@@ -312,7 +312,7 @@ Verbindung und führt sie zusammen, mit der Instanzkennung an jedem Ereignis.
| Stufe | Inhalt | Woran man es misst |
|---|---|---|
| 1 ✔ | Einstellungen und Protokolle je Instanz trennen (Abschnitt 6) | Zwei Instanzen gleichzeitig, keine schreibt der anderen ins Protokoll |
| 2 | Instanzprozess kopflos lauffähig, Kestrel auf dem Socket | Eine Instanz läuft ohne Fenster, der Socket antwortet |
| 2 ✔ | Instanzprozess kopflos lauffähig, Kestrel auf dem Socket | Eine Instanz läuft ohne Fenster, der Socket antwortet |
| 3 | Manager: anlegen, starten, anhalten, Prozessaufsicht, Lizenz, Sub-Token | Eine Instanz abschießen — der Manager merkt es und meldet es |
| 4 | Weboberfläche zieht um, Weiterleitung, ein Ereignisstrom | Eine Adresse, ein Zugang, Wechsel ohne Neustart |
| 5 | Instanzfenster als eigener Prozess, Wechsel holt das Fenster nach vorn | Kein Neustart mehr beim Wechseln |
@@ -336,11 +336,10 @@ einer alten `Settings.json`, meldet `SettingsManager.AbandonedKeys` sie, und der
protokolliert einmal, was nicht mehr gilt und wohin diese Instanz stattdessen schreibt.
Beim nächsten Speichern verschwinden sie aus der Datei.
**`webServerPort` bleibt vorerst stehen.** Es entfällt erst, wenn Kestrel auf dem Socket
hört — also mit Stufe 2. Vorher wegzunehmen hieße, die Weboberfläche ohne Ersatz
abzuschalten. Der Portstreit zweier Instanzen ist bis dahin gedeckelt, aber nicht gelöst:
`WebUiHost.TryStart` gibt bei belegtem Port `null` zurück und protokolliert es, statt den
Start zu verhindern.
**`webServerPort` bleibt vorerst stehen.** Es entfällt erst mit **Stufe 4**, wenn der
Manager den Port übernimmt — nicht schon, sobald Kestrel den Socket hat. Eine frühere
Fassung dieses Absatzes sagte „mit Stufe 2"; das war falsch gerechnet: Solange es keinen
Manager gibt, der die Weboberfläche trägt, hieße Wegnehmen, sie ersatzlos abzuschalten.
---
@@ -357,3 +356,46 @@ Manager läuft immer mit, die Instanzfenster laufen mit, aber eine Instanz *kann
ohne Fenster laufen.
**#218 (SDK 2.6.0 → aktuell)** wird Voraussetzung — siehe Abschnitt 5.
### 9.2 Was Stufe 2 gebracht hat
**Kestrel hört auf dem Socket** — `<Instanz>/manager.sock` unter Linux, unter Windows
eine Named Pipe, deren Name aus einem Fingerabdruck des Instanzpfades entsteht
(`InstanceEndpoint`). Zwei Instanzen bekommen zwangsläufig zwei Adressen, ohne dass
jemand etwas vergeben müsste.
**Der Socket ist immer da, auch ohne Weboberfläche.** Er ist nicht Teil von #494, sondern
die Steuerfläche der Instanz: `webUiEnabled` schaltet den TCP-Zugang, nicht den Socket.
Was aus `WebUiHost` wurde, heißt deshalb jetzt `InstanceHttpHost` — ein Kestrel, zwei
Zugänge.
**Der Torwächter unterscheidet die Zugänge.** Über TCP gilt Token-Pflicht, über den
Socket nicht: Dort steht die Grenze am Dateisystem (0600 beziehungsweise
`CurrentUserOnly`), und wer die Socketdatei öffnen darf, sieht ohnehin in das Verzeichnis,
in dem alles liegt, was ein Token schützen würde. **Woran die beiden auseinandergehalten
werden, ist der Punkt, an dem ein Irrtum teuer wäre:** Bei Socket-Verbindungen stehen in
`HttpContext.Connection` keine IP-Adressen — daraus zu schließen „keine IP, also Socket"
wäre eine Vermutung an der Stelle, an der ein Torwächter steht, und ließe im Zweifel eine
TCP-Anfrage ohne Token durch. Der Socket-Zuhörer hängt deshalb jeder Verbindung selbst
eine Marke an (`LocalTransport`); wo sie fehlt, gilt Token-Pflicht.
**Ein belegter Port kostet nur noch die Weboberfläche.** Vorher gab `TryStart` in dem Fall
`null` zurück und die ganze HTTP-Fläche war weg. Das ist seit dem Socket die falsche
Rangfolge: Ihn zu verlieren, weil eine zweite Instanz schon läuft, hieße die Instanz
unerreichbar zu machen — in genau dem Fall, für den es den Manager gibt.
**Kopfloser Betrieb** über `--headless --instance <Pfad>`. Die Verzweigung sitzt in
`Program.Main` vor `BuildAvaloniaApp`, damit auf einem Server ohne Bildschirmsitzung kein
Fenstersystem angefasst wird. Gefragt wird nichts: Instanz aus dem Argument, Lizenz über
`NonInteractiveLicensePrompt`, Telegram-Anmeldung aus.
Nachgemessen, nicht angenommen: Eine kopflose Instanz auf `Instance-Test` lief ohne
Fenster, und eine rohe HTTP-Anfrage aus einem fremden Prozess (PowerShell, direkt auf die
Pipe) bekam `200 OK` auf `/api/info` — ohne Token.
**Zwei Lücken, die offen bleiben.** Die Socketdatei-Fälle (liegengebliebene Leiche,
Aufräumen beim Beenden) sind unter Windows gegenstandslos und werden dort mit Begründung
übersprungen — der Unix-Domain-Socket selbst ist auf der Entwicklungsmaschine also
ungeprüft, obwohl er die Zielplattform ist. Und das geordnete Ende auf `SIGTERM` ließ sich
hier nicht auslösen: Windows kennt das Signal für einen fensterlosen Prozess nicht.
Beides gehört auf die Linux-Testmaschine.
+15
View File
@@ -78,6 +78,21 @@ Startet sie nicht, steht der Grund im Protokoll (`Logs/`, Modul `Web`). Die
häufigsten: Token fehlt, Token kürzer als 20 Zeichen, Port belegt. Ein
Fehlschlag hält die Anwendung nie auf — sie läuft dann eben ohne Browser.
### Der Socket daneben — nicht deine Baustelle, aber gut zu wissen
Seit #528 (Stufe 2) hört dieselbe Instanz zusätzlich auf einem Socket im
Instanzverzeichnis (`manager.sock`, unter Windows eine Named Pipe). Über ihn
wird der Instanzmanager die Instanz erreichen. Drei Dinge, die dich betreffen
könnten:
- **Er ist immer da**, auch wenn `webUiEnabled` aus ist. Was du in `Logs/Web.log`
als „Instanz hört auf …" siehst, ist er und nicht die Weboberfläche.
- **Er kennt kein Token.** Seine Grenze steht am Dateisystem. Wer über den Socket
hereinkommt, ist an `TokenGate` vorbei — das ist Absicht und in
`LocalTransport` festgehalten.
- **Für dich ändert sich nichts.** Endpunkte, Übertragungstypen und der
Ereignisstrom sind dieselben; es ist nur eine zweite Tür in denselben Raum.
---
## 2 — Die API