/* 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$/, "")); }