Files
dogfather-universe/server/helfer-port.mjs
T
DogFatherGitandClaude Opus 5 445ea88f2b fetch verweigert Port 5060 -- und sagt dasselbe wie ein toter Server
Nach der Umstellung auf abgeleitete Nummern bekam pruef-chat-kanaele
die 5060 und stuerzte ab:

  TypeError: fetch failed

Der Server lief nachweislich ("Server laeuft auf http://127.0.0.1:5060"),
die Anmeldung ueber `http.request` hatte sogar geklappt, und BASIS war
sauber ("http://127.0.0.1:5060", 21 Zeichen, PORT eine Zahl). Nur die
`fetch`-Aufrufe scheiterten.

5060 ist SIP und steht auf der "bad ports"-Liste der
Fetch-Spezifikation. Node hat sie uebernommen: `fetch` baut dorthin
keine Verbindung auf, noch bevor ein Paket fliegt. Die Ursache steht
nur in `.cause` -- "bad port" statt "ECONNREFUSED". Die Meldung
obendrueber ist beide Male dieselbe, und wer sie liest, sucht am
Server.

Gegengeprueft mit einem echten Zuhoerer auf beiden Nummern:
  5060 -> fetch failed (bad port)
  5062 -> fetch failed (ECONNREFUSED)   <- also normal erreichbar

`eigenerPort` ueberspringt die gesperrten Nummern jetzt (portNummer).
Uebersprungen wird nach der REGEL, nicht nach einer Tabelle von
Sonderfaellen: die n-te brauchbare Nummer ab 5000. Eindeutig bleibt es,
weil die Reihenfolge feststeht.

pruef-portnummern (11 statt 8) prueft es doppelt: gegen die Liste UND
am echten Verhalten -- ein Zuhoerer auf 5060 muss fuer `fetch` tot
sein, einer auf einer abgeleiteten Nummer erreichbar. Ohne die zweite
Haelfte waere die Liste nur eine Behauptung ueber Node, und
Behauptungen altern.

Nebenbefund: Auch im alten, von Hand vergebenen Bereich lag eine
gesperrte Nummer -- die 4190. Sie war nie vergeben, rein zufaellig.

pruef-chat-kanaele: 79 Pruefungen, 0 Fehler -- wieder auf der
Grundlinie, die vor der Umstellung gemessen wurde.

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

213 lines
9.6 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";
/** Wirft, wenn der Port belegt ist. Sonst gibt er ihn frei zurück. */
export async function portMussFreiSein(port, wer = "diese Pruefung") {
const belegt = await new Promise((fertig) => {
const s = createServer();
s.once("error", (f) => fertig(f.code === "EADDRINUSE"));
s.once("listening", () => s.close(() => fertig(false)));
s.listen(port, "127.0.0.1");
});
if (belegt) {
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;
/** 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$/, ""));
}