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]>
213 lines
9.6 KiB
JavaScript
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$/, ""));
|
|
}
|