Im Kommentar stand „die Pruefungen reichen bis 5409, Luft fuer 245 weitere Dateien". Noch am selben Tag kamen zwei Pruefdateien aus einer anderen Sitzung dazu, und daraus wurde 5413/243. Das ist kein Fehler, sondern der Beweis, dass die Umstellung von gestern richtig war: Zwei fremde Dateien haben alle Nummern dahinter verschoben, und es musste niemand etwas nachtragen. Nachgemessen danach: 460 Nummern, alle verschieden, 0 Kollisionen, pruef-ports 10/0 und pruef-portnummern 41/0. Aber eine Zahl im Kommentar, die sich am Tag ihrer Messung schon bewegt hat, ist genau die Sorte, der jemand spaeter glaubt. Sie steht jetzt als STAND da, mit dem Hinweis, wo die heutige zu holen ist: pruef-portnummern sagt sie bei jedem Lauf. Co-Authored-By: Claude Opus 5 <[email protected]>
337 lines
16 KiB
JavaScript
337 lines
16 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;
|
|
|
|
/** Ab hier die MESSDATEIEN (01.10.2026).
|
|
*
|
|
* Sie hatten ihre Nummern von Hand. Nachgemessen lagen ACHT davon
|
|
* IM abgeleiteten Bereich der Pruefungen: Der ist bei 204 Dateien
|
|
* dicht von 5000 bis 5409 belegt, und 5387/5397/5399/5401/5403/5405
|
|
* liegen mittendrin. Zwei Laeufe konnten also denselben Port
|
|
* bekommen, ohne dass es jemand merkt. Untereinander war fuenfmal
|
|
* dieselbe Nummer doppelt vergeben (5397, 5399, 5461, 5483, 5491).
|
|
*
|
|
* Aufgefallen ist es, weil mess-reaktion und mess-buehne beide auf
|
|
* 5483 lagen und ein haengengebliebener Lauf einen ganzen Messlauf
|
|
* gekostet hat.
|
|
*
|
|
* 5900 UND NICHT 5500: Zwischen dem Ende der Pruefungen (5409) und
|
|
* hier bleibt Platz fuer 245 weitere Pruefdateien -- bei 5500 waeren
|
|
* es nur 45, und die waeren in einem Jahr aufgebraucht. Nach oben:
|
|
* 5900 + 24 Dateien * 2 = 5948; plus AUSWEICHEN (4000) sind das
|
|
* 9948, also unter 10080, der naechsten gesperrten Nummer. Beides
|
|
* nachgerechnet, nicht geschaetzt -- dieselbe Ueberlegung wie bei
|
|
* BASIS, wo eine geschaetzte 4500 danebengelegen haette.
|
|
*
|
|
* DIE ZAHLEN HIER SIND EIN STAND, KEINE WAHRHEIT. Noch am selben
|
|
* Tag kamen zwei Pruefdateien aus einer anderen Sitzung dazu, und
|
|
* aus 5409/245 wurde 5413/243. Genau dafuer ist die Ableitung da --
|
|
* es musste niemand etwas nachtragen. Wer die heutige Zahl braucht,
|
|
* liest sie nicht hier, sondern laesst pruef-portnummern laufen:
|
|
* „die Pruefungen enden bei X, die Messungen fangen bei Y an".
|
|
*
|
|
* AUSGELEITET aus demselben Grund wie AUSWEICHEN und GESPERRT:
|
|
* pruef-portnummern.mjs weist nach, dass die beiden Bereiche sich
|
|
* nicht beruehren -- und muss dafuer DIESE Zahl ansehen, nicht eine
|
|
* Abschrift, die beim naechsten Verschieben stehen bleibt. */
|
|
export const MESS_BASIS = 5900;
|
|
/** 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. */
|
|
/* AUSGELEITET, damit pruef-ports.mjs mit DERSELBEN Zahl rechnet.
|
|
Eine 4000 dort waere die zweite Fassung -- und beim naechsten
|
|
Mal stuende in einer der beiden eine andere. */
|
|
export 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.
|
|
===================================================================== */
|
|
/* AUSGELEITET aus demselben Grund wie AUSWEICHEN: pruef-ports.mjs
|
|
weist nach, dass keine abgeleitete Nummer hier drinsteht -- und
|
|
muss dafuer die echte Liste ansehen, nicht eine Abschrift. */
|
|
export 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, basis = BASIS) {
|
|
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);
|
|
/* ZWEI SORTEN, ZWEI BEREICHE (01.10.2026). Eine Messdatei zaehlt
|
|
unter ihresgleichen, eine Pruefdatei unter ihresgleichen -- sonst
|
|
verschoebe eine neue Pruefung die Nummern aller Messungen und
|
|
umgekehrt. Welche Sorte es ist, sagt der Dateiname. */
|
|
const istMess = mein.startsWith("mess-");
|
|
const vorsilbe = istMess ? "mess-" : "pruef-";
|
|
const alle = readdirSync(ordner)
|
|
.filter((f) => f.startsWith(vorsilbe) && f.endsWith(".mjs"))
|
|
.sort();
|
|
const stelle = alle.indexOf(mein);
|
|
if (stelle < 0) {
|
|
throw new Error(
|
|
`[Port] ${mein} steht nicht in der Liste der ${vorsilbe}Dateien 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}.`);
|
|
}
|
|
const port = istMess
|
|
? portNummer(stelle * PRO_DATEI + nr, MESS_BASIS)
|
|
: portNummer(stelle * PRO_DATEI + nr);
|
|
|
|
/* DIE WACHE GEGEN DAS ZUSAMMENWACHSEN. Kommen so viele
|
|
Pruefdateien dazu, dass ihr Bereich den der Messungen erreicht,
|
|
bekaemen zwei Laeufe still dieselbe Nummer -- genau der Fehler,
|
|
den diese Ableitung beseitigt. Dann lieber ein lauter Abbruch
|
|
mit Anleitung. */
|
|
if (!istMess && port >= MESS_BASIS) {
|
|
throw new Error(
|
|
`[Port] Der Bereich der Pruefungen ist bei ${port} angekommen und\n`
|
|
+ ` stoesst an den der Messdateien (ab ${MESS_BASIS}).\n`
|
|
+ " MESS_BASIS in helfer-port.mjs hochsetzen -- und dabei nachsehen,\n"
|
|
+ " dass MESS_BASIS + 4000 (AUSWEICHEN) unter 10080 bleibt.");
|
|
}
|
|
return portMussFreiSein(port, wer || mein.replace(/\.mjs$/, ""));
|
|
}
|