Files
dogfather-universe/server/helfer-port.mjs
T
DogFatherGitandClaude Opus 5 189d0b3363 B1: Das Anschlagbrett haelt 24 Stunden -- und sieht aus wie ein Anschlagbrett
"die sachen sollen in der seite vom anschlagbrett immer automatisch
nach 24h von da verschwinden. weil alles hat seine kategorie und was
nicht ist auch nicht um ewig auf der seite zu bleiben fertig."

SEINE BEGRUENDUNG IST DIE REGEL. Ein Anschlagbrett sagt, was GERADE
gilt. Was laenger gilt, hat seinen eigenen Bereich -- Regeln stehen bei
den Regeln, Termine bei "Was ansteht". Eine Ansage vom letzten
Dienstag, die noch haengt, ist keine Ansage mehr, sondern Papier an der
Wand.

DIE DREI ENTSCHEIDUNGEN, DIE ICH TREFFEN MUSSTE -- und woran ich sie
festgemacht habe:

1. AB WANN LAUFEN DIE 24 STUNDEN? Ab `erstellt`, nicht ab `datum`.
   `datum` ist ein vom Team gesetztes Feld und kann in der Zukunft
   liegen (es traegt die Termine). Eine Ansage, die morgen gilt, soll
   morgen verschwinden und nicht uebermorgen.

2. VERSCHWINDET SIE GANZ? Nein. Er sagt "von DA verschwinden" -- von
   der Seite. Geloescht wird nichts: Wer eine Ansage geschrieben hat,
   soll sie wiederfinden. Sie kommt als `abgehaengt` mit und steht
   zugeklappt unter einer leisen Zeile "Abgehaengt (3)". Auf dem Brett
   selbst steht sie damit nicht mehr.

3. WER ENTSCHEIDET? Der Server. Die Stunden im Browser nachzurechnen
   waere eine zweite Fassung derselben Regel, und eine davon waere
   irgendwann aelter. Die Frist geht als Zahl mit der Antwort.

DAS AUSSEHEN ("viel besser gestaltet, übersichtlicher, moderner,
hochwertiger, professioneller"): Was haengt, sieht jetzt aus wie etwas,
das haengt -- kraeftiger Rand links in der Farbe seiner ART (Ansage
babyblau, Neue Regel gold, Hinweis lila, Danke gruen), ein Schein von
links, mehr Luft. Die Art stand bisher nur im Text; jetzt sieht man
sie. Abgehaengtes ist grau statt durchsichtig -- durchsichtig hiesse,
das Buehnenbild scheint durch, also Text auf einem Foto.

pruef-anschlagbrett.mjs (NEU, 12/0) misst beide Seiten der Grenze
(23 h haengt, 25 h nicht), rechnet relativ zur jetzigen Uhrzeit statt
mit einem festen Datum, und hat den Eintrag, an dem sich alles
entscheidet: alt aufgehaengt, Datum von heute. Wer nach `datum`
filtert, laesst ihn haengen. Dazu zwei Gegenproben: auf jedem anderen
Brett gibt es das Merkmal gar nicht, und wo die Grenze WIRKLICH liegt,
wird aus den Eintraegen nachgerechnet statt aus der Zahl im Modul.

NEBENBEFUND, den erst dieser Commit ausgeloest hat
---------------------------------------------------
pruef-bereiche-lesend stuerzte ab: "listen EACCES 127.0.0.1:5040".

Die Portnummern werden aus der Stelle im Alphabet abgeleitet -- heute
sind fuenf neue Pruefdateien dazugekommen, und dadurch ist diese
Pruefung auf die 5040 gerutscht. Die haelt auf diesem Rechner ein
Windows-Dienst (svchost, PID 7136).

`portMussFreiSein` kannte nur ZWEI Antworten: EADDRINUSE oder frei.
EACCES galt als frei -- die Pruefung lief weiter, bis der echte Server
auf demselben Port startete und mit einem unbehandelten Fehler
abstuerzte. Ein Stapelauszug aus node:net, der wie ein Fehler im Code
aussieht.

Jetzt drei Antworten. Und ein belegter Port wird anders behandelt als
ein gesperrter: Belegt geht vorbei (der andere Lauf hoert auf),
gesperrt nie. Abzubrechen hiesse, die Pruefung waere auf diesem Rechner
dauerhaft nicht ausfuehrbar -- die Sorte Warnung, die immer kommt und
deshalb weggeklickt wird. Ausgewichen wird um einen festen Betrag
(+4000), damit die Nummer abgeleitet und eindeutig bleibt.

Mitgelaufen und gruen: pruef-bereiche-lesend, pruef-css-klassen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 23:47:52 +02:00

275 lines
13 KiB
JavaScript

/* IST DER PORT WIRKLICH MEINER?
Gemeinsamer Baustein für alle Prüfungen, die einen eigenen Server
starten.
DER FEHLER, DEN DAS VERHINDERT — und der in diesem Haus schon zweimal
zugeschlagen hat:
Läuft auf dem gewünschten Port schon ein anderer Prozess, dann startet
der eigene Server nicht. `app.listen()` meldet das an einen
Fehlerbehandler, den niemand gesetzt hat -- die Prüfung läuft
weiter und misst ab da gegen den FREMDEN Server. Sie bekommt Antworten,
sie bekommt sogar plausible Antworten, und sie meldet Fehler, die es
nicht gibt (oder schlimmer: sie meldet "in Ordnung" über einen Stand,
den sie nie angesehen hat).
Das erste Mal stand es schon in der Projektnotiz vom 27.08.2026:
*"Ein alter Testserver hielt den Port besetzt (EADDRINUSE), der neue
startete still nicht — ich habe zwischenzeitlich gegen den alten Stand
gemessen."*
Das zweite Mal am 05.09.2026: Ein Prüflauf lief von Hand, während die
Gesamtprüfung denselben Lauf ebenfalls aufrief. Beide wollten Port
4289. Ergebnis war ein WebKit-Fehler, den es gar nicht gab -- die
Anmeldung scheiterte, weil der Code aus der einen Datenbank gegen den
Server der anderen geprüft wurde. Zwei Fehlersuchen an der falschen
Stelle (HSTS, dann CSP) waren die Folge.
DESHALB: Vor dem Start wird nachgesehen, ob der Port frei ist. Ist er
belegt, bricht die Prüfung SOFORT ab und sagt, warum. Ein belegter
Port ist kein Messergebnis -- er ist der dritte Ausgang: nicht in
Ordnung ist etwas anderes als nicht nachgesehen.
=================================================================== */
import { createServer } from "node:net";
/** Was ist mit diesem Port los?
*
* DREI ANTWORTEN, NICHT ZWEI (22.09.2026) -- und die dritte hat
* heute gefehlt:
*
* null frei
* EADDRINUSE jemand hoert darauf (meist ein zweiter Prueflauf)
* EACCES das System gibt ihn nicht her -- dauerhaft
*
* Vorher wurde NUR auf EADDRINUSE geprueft; jeder andere Fehler galt
* als "frei". Auf diesem Rechner haelt ein Windows-Dienst (svchost,
* PID 7136) den Port 5040. `createServer().listen` scheiterte dort mit
* EACCES, die Antwort war trotzdem "frei", und die Pruefung lief
* weiter -- bis der ECHTE Server auf demselben Port startete und mit
* einem unbehandelten Fehler abstuerzte. Eine Meldung, die "belegt"
* heissen muesste, kam nie; stattdessen ein Stapelauszug aus
* node:net, der wie ein Fehler im Code aussieht.
*
* Aufgefallen ist es, weil die Nummern sich verschoben haben: Sie
* werden aus der Stelle im Alphabet abgeleitet, und heute sind fuenf
* neue Pruefdateien dazugekommen. Der Fall lag also seit Langem da
* und wartete nur darauf, dass jemand auf die 5040 rutscht. */
async function portLage(port) {
return new Promise((fertig) => {
const s = createServer();
s.once("error", (f) => fertig(f.code || "FEHLER"));
s.once("listening", () => s.close(() => fertig(null)));
s.listen(port, "127.0.0.1");
});
}
/** Wirft, wenn der Port belegt ist. Sonst gibt er ihn frei zurück. */
export async function portMussFreiSein(port, wer = "diese Pruefung") {
const lage = await portLage(port);
/* DAUERHAFT GESPERRT -> AUSWEICHEN, NICHT ABBRECHEN.
------------------------------------------------------------------
Ein belegter Port ist ein Zustand, der vorbeigeht: Der andere Lauf
hoert auf, und danach geht es. Ein vom System reservierter Port
geht NIE. Abzubrechen hiesse, diese Pruefung waere auf diesem
Rechner dauerhaft nicht ausfuehrbar -- und das waere die Sorte
Warnung, die immer kommt und deshalb weggeklickt wird.
Ausgewichen wird um einen festen Betrag, nicht auf "den naechsten
freien": Damit bleibt die Nummer ABGELEITET und eindeutig. Zwei
Pruefungen, die beide ausweichen, landen auf zwei verschiedenen
Nummern, weil ihre Ausgangsnummern verschieden waren. */
if (lage === "EACCES") {
const ersatz = port + AUSWEICHEN;
const lage2 = await portLage(ersatz);
if (lage2 === null) {
console.log(`[Port] ${port} ist vom System belegt (EACCES) --`
+ ` ${wer} nimmt ${ersatz}.`);
return ersatz;
}
console.error(`\nABBRUCH: Port ${port} ist vom System gesperrt, und der`);
console.error(` Ausweichport ${ersatz} ist ebenfalls nicht frei (${lage2}).\n`);
console.error(` ${wer} kann hier nicht messen. Das ist KEIN Befund am Code --`);
console.error(` es heisst nur, dass gerade niemand nachsehen konnte.\n`);
console.error(` Nachsehen, wer die Ports haelt:`);
console.error(` netstat -ano | findstr :${port}`);
console.error(` netstat -ano | findstr :${ersatz}\n`);
process.exit(3);
}
if (lage !== null) {
console.error(`\nABBRUCH: Port ${port} ist belegt.\n`);
console.error(` ${wer} startet einen eigenen Server auf diesem Port. Laeuft dort`);
console.error(` schon etwas, wuerde gegen einen FREMDEN Stand gemessen -- mit`);
console.error(` Ergebnissen, die nichts ueber den eigenen Code aussagen.\n`);
console.error(` Meist laeuft parallel noch ein zweiter Prueflauf (etwa`);
console.error(` tools/alles-pruefen.mjs). Erst den beenden, dann hier neu starten.\n`);
console.error(` Nachsehen, wer den Port haelt:`);
console.error(` netstat -ano | findstr :${port}\n`);
process.exit(3);
}
return port;
}
/* =====================================================================
WELCHER PORT GEHOERT MIR? (20.09.2026)
---------------------------------------------------------------------
WARUM ES DIESE FUNKTION GIBT
Gemessen am 20.09.2026: 164 Pruefdateien, 129 verschiedene Ports --
NEUNZEHN Nummern waren doppelt vergeben, eine sogar dreifach
(4186, 4188, 4196, 4198 ...). Niemand hatte das absichtlich getan;
jede neue Pruefung wurde von einer vorhandenen abgeschrieben, und
die Nummer kam mit.
Der Waechter darueber faengt den Schaden ab -- er bricht ab, statt
gegen einen fremden Server zu messen. Aber er kann nur melden, was
schon passiert ist: Zwei Pruefungen mit derselben Nummer koennen nie
gleichzeitig laufen, und ein liegengebliebener Prozess der einen
laesst die andere mit einer Meldung abbrechen, die wie ein Befund
aussieht. Genau das ist mir heute zweimal passiert.
EINE GEPFLEGTE NUMMERNLISTE IST DIE FALSCHE LOESUNG. Dieses Haus ist
an abgeschriebenen Listen schon dreimal gescheitert (Spaltenliste
beim Datenumbau, Namensliste in pruef-anruf, Namensliste in
pruef-entwicklung). Eine Liste, die niemand pflegt, kann nicht
veralten -- also wird die Nummer ABGELEITET.
---------------------------------------------------------------------
WIE ABGELEITET WIRD, UND WARUM NICHT MIT EINER PRUEFSUMME
Der naheliegende Weg waere ein Hash des Dateinamens. Nachgerechnet
ist er der schlechtere: Bei 164 Namen in 4900 moeglichen Ports sind
nach dem Geburtstagsproblem rund DREI Zusammenstoesse zu erwarten --
der Hash tauscht eine sichtbare Doppelung gegen eine unsichtbare.
Stattdessen: die Stelle im Alphabet. Alle `pruef-*.mjs` im selben
Ordner werden sortiert, und wer an Stelle n steht, bekommt Port
BASIS + n*2. Das ist NICHT ZUFAELLIG EINDEUTIG, sondern eindeutig
von der Bauart her -- zwei Dateien koennen nicht dieselbe Stelle im
Alphabet haben.
Jede Datei bekommt ZWEI Nummern (nr = 0 und 1). Fuenf Pruefungen
starten heute einen zweiten Server; der braucht dann keinen eigenen
Eintrag irgendwo.
Nebenwirkung, bewusst in Kauf genommen: Kommt eine neue Pruefung
dazu, verschieben sich die Nummern dahinter. Das ist harmlos -- es
sind Testports, kein Mensch merkt sie sich -- und der Waechter faengt
den einen Fall ab, in dem es zaehlt (ein Prozess von vorhin liegt
noch auf der Nummer).
===================================================================== */
import { readdirSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { basename, dirname } from "node:path";
/** Ab hier aufwaerts, zwei je Datei.
*
* NICHT 4100: Der echte Dienst hoert dort, und eine Pruefung, die
* versehentlich dorthin zielt, waere der eine Fehler, den der Waechter
* NICHT erkennen kann -- sie bekaeme brav Antworten.
*
* UND NICHT 4500, wie hier zuerst stand. Nachgemessen reichen die
* bisher von Hand vergebenen Nummern von 4176 bis 4504 -- der Bereich
* 4500-4827 haette sich also mit dem obersten Rest ueberschnitten.
* Das ist der Grund, warum solche Zahlen gemessen und nicht geschaetzt
* werden: "irgendwo ueber 4400" haette plausibel geklungen. */
const BASIS = 5000;
const PRO_DATEI = 2;
/** Um wie viel ausgewichen wird, wenn das System einen Port nicht
* hergibt. 4000 liegt sicher oberhalb des ganzen abgeleiteten
* Bereichs (BASIS bis BASIS+5000 in Schritten von 2, bei derzeit
* rund 180 Dateien also bis etwa 5400) -- zwei Pruefungen koennen
* sich dadurch nicht ins Gehege kommen, auch wenn beide ausweichen. */
const AUSWEICHEN = 4000;
/** Der Port dieser Datei -- abgeleitet, nicht vergeben.
*
* `meta` ist `import.meta` der aufrufenden Datei. Prueft danach
* gleich, ob er frei ist, und bricht sonst ab (siehe oben).
*
* DRITTER AUSGANG: Findet sich die eigene Datei nicht in der Liste --
* etwa weil jemand die Funktion aus einem anderen Ordner aufruft --,
* wird NICHT geraten. Eine geratene Nummer waere genau die Doppelung,
* die hier abgeschafft wird. */
/* =====================================================================
PORTS, DIE `fetch` NICHT ANFASST (20.09.2026, teuer gelernt)
Die Fetch-Spezifikation fuehrt eine Liste "bad ports" -- Nummern, auf
die ein Browser KEINE Verbindung aufbaut, weil sich ueber sie fremde
Dienste missbrauchen liessen (SMTP, NNTP, SIP, IRC und andere). Node
hat diese Liste uebernommen: `fetch` wirft dort `TypeError: fetch
failed` mit der Ursache `bad port`, noch bevor ein Paket fliegt.
WIE ES AUFGEFALLEN IST: Nach der Umstellung auf abgeleitete Nummern
bekam pruef-chat-kanaele die 5060 -- und stuerzte ab, obwohl der
Server nachweislich lief ("Server laeuft auf http://127.0.0.1:5060")
und die Anmeldung ueber `http.request` sogar geklappt hatte. Nur die
`fetch`-Aufrufe scheiterten. 5060 ist SIP und steht auf der Liste.
WARUM DAS SO SCHWER ZU FINDEN WAR: Die Meldung lautet "fetch failed"
-- dieselbe Meldung wie bei einem Server, der nicht laeuft. Wer sie
liest, sucht am Server. Der Unterschied steht erst in `.cause`:
`bad port` statt `ECONNREFUSED`. Gegengeprueft mit einem echten
Zuhoerer auf 5060: Port 5060 -> "bad port", Port 5062 -> sauberes
"ECONNREFUSED". Die Sperre ist also real und trifft genau diese
Nummern.
NEBENBEFUND: Auch im alten, von Hand vergebenen Bereich lag eine --
die 4190 (ManageSieve). Sie war nie vergeben, rein zufaellig.
===================================================================== */
const GESPERRT = new Set([
1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79,
87, 95, 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137,
138, 139, 143, 161, 179, 389, 427, 465, 512, 513, 514, 515, 526, 530, 531,
532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993, 995, 1719, 1720,
1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668,
6669, 6679, 6697, 10080,
]);
/** Die n-te brauchbare Nummer ab BASIS -- gesperrte werden uebersprungen.
*
* Damit bleibt die Ableitung eindeutig (die Reihenfolge steht fest),
* ohne dass eine gesperrte Nummer je herauskommt. Eine Liste von
* Ausnahmen musste dafuer niemand pflegen: Uebersprungen wird nach
* der Regel, nicht nach einer Tabelle von Sonderfaellen.
*/
export function portNummer(nte) {
let p = BASIS;
let uebrig = nte;
while (true) {
if (!GESPERRT.has(p)) {
if (uebrig === 0) return p;
uebrig -= 1;
}
p += 1;
if (p > BASIS + 5000) {
throw new Error("[Port] Kein freier Nummernbereich mehr oberhalb von " + BASIS + ".");
}
}
}
export async function eigenerPort(meta, wer = null, nr = 0) {
const pfad = fileURLToPath(meta.url);
const mein = basename(pfad);
const ordner = dirname(pfad);
const alle = readdirSync(ordner)
.filter((f) => f.startsWith("pruef-") && f.endsWith(".mjs"))
.sort();
const stelle = alle.indexOf(mein);
if (stelle < 0) {
throw new Error(
`[Port] ${mein} steht nicht in der Liste der Pruefdateien in ${ordner}.\n`
+ " Die Nummer wird aus der Stelle im Alphabet abgeleitet -- ohne\n"
+ " Stelle gibt es keine Nummer, und geraten wird nicht.");
}
if (nr < 0 || nr >= PRO_DATEI) {
throw new Error(`[Port] Es gibt ${PRO_DATEI} Nummern je Datei (0 und 1), nicht ${nr}.`);
}
return portMussFreiSein(portNummer(stelle * PRO_DATEI + nr), wer || mein.replace(/\.mjs$/, ""));
}