Files
dogfather-universe/server/helfer-port.mjs
T
DogFatherGitandClaude Opus 5 f6225d7341 Ports: auch die Messwerkzeuge leiten ihre Nummer ab
GEMESSEN, BEVOR ICH ETWAS ANGEFASST HABE:

  204 pruef-Dateien, abgeleitete Ports 5000-5409 (dicht belegt)
  24 mess-Dateien, Nummern von Hand: 4471 bis 5493
  davon IM Pruefbereich: 5387, 5397, 5397, 5399, 5399, 5401, 5403, 5405
  untereinander doppelt: 5397, 5399, 5461, 5483, 5491

Aufgefallen ist es, weil mess-buehne und mess-reaktion beide auf 5483
lagen und ein haengengebliebener Lauf gestern einen ganzen Messlauf
gekostet hat.

Beim Umbau kam das Groessere heraus: EINUNDZWANZIG der 24 Messdateien
hatten nicht nur eine Nummer von Hand, sondern ueberhaupt keinen
Waechter -- schlicht `const PORT = 5397;`. Liegt dort schon ein
Server, startet der eigene still nicht, und gemessen wird ab da ein
fremder Stand. Genau der Fehler, gegen den helfer-port.mjs gebaut
wurde; die Messdateien standen die ganze Zeit ausserhalb.

WAS JETZT GILT

Zwei Sorten, zwei Bereiche, beide abgeleitet aus der Stelle im
Alphabet -- jede Sorte unter ihresgleichen, sonst verschoebe eine
neue Pruefung die Nummern aller Messungen. Pruefungen ab 5000,
Messungen ab MESS_BASIS = 5900.

5900 und nicht 5500: dazwischen bleibt Platz fuer 245 weitere
Pruefdateien (bei 5500 waeren es 45). Nach oben 5948 + AUSWEICHEN
4000 = 9948, also unter 10080, der naechsten gesperrten Nummer.
Nachgerechnet, nicht geschaetzt -- eine geschaetzte 4500 hatte bei
BASIS schon einmal danebengelegen.

Eine Wache dazu: Waechst der Pruefbereich bis an MESS_BASIS heran,
bricht die Ableitung ab und sagt, was zu tun ist. Eine stille
Ueberschneidung waere genau der Fehler, den das hier beseitigt.

Die zweite Nummer kommt ueber nr=1, nie ueber `PORT + 1`:
portNummer ueberspringt gesperrte Nummern, deshalb kann die naechste
Zahl die Nummer der naechsten DATEI sein, sobald einmal eine Sperre
dazwischenliegt. Heute liegt dort keine -- das ist Glueck, kein
Entwurf.

NEBENBEI GEFUNDEN UND MIT REPARIERT

Sechs bild-*.mjs riefen den Waechter und warfen seine Antwort weg:

    await portMussFreiSein(4315, "das Bildwerkzeug");
    process.env.PORT = "4315";
    const BASIS = "http://127.0.0.1:4315";

Er lief, meldete nichts und wirkte nicht. Gibt das System den Port
dauerhaft nicht her, weicht er auf Port + 4000 aus und GIBT DIE NEUE
NUMMER ZURUECK -- diese Werkzeuge hoerten danach trotzdem auf der
alten und stuerzten mit `listen EACCES` ab, also mit genau dem
Fehler, gegen den er gebaut wurde. Dazu stand die Zahl dreimal je
Datei. Jetzt einmal, und die Antwort wird benutzt.

tiktok-videos.mjs hatte den Waechter ABGESCHRIEBEN -- eine kurze
eigene Fassung ohne den dritten Ausgang: Bei EACCES meldete sie
"belegt" und brach ab, statt auszuweichen. Auf diesem Rechner ist
genau das am 23.09. eingetreten (Port 5040, Windows-Dienst).
mess-fokus und mess-notizblock hatten dieselbe Abschrift. Eine
abgeschriebene Sicherung ist dieselbe Falle wie eine abgeschriebene
Liste.

GEPRUEFT

  pruef-portnummern  15 -> 41 Pruefungen, 0 Fehler
  pruef-ports         8 -> 10 Pruefungen, 456 statt 408 Ports geprobt
  node --check auf allen 33 geaenderten Dateien

Gegenproben, die wirklich rot werden:
  - eine Messdatei auf eine feste Nummer zurueckgesetzt -> 2 FEHL,
    danach wieder 41/0
  - die Wache: in einem Wegwerf-Ordner mit 452 pruef-Dateien bricht
    eigenerPort ab statt still zu ueberlappen; eine Datei knapp
    darunter bekommt weiter ihre Nummer (5846)
  - die Erkennungen fuer feste Nummern, PORT + 1 und weggeworfene
    Waechterantworten je gegen einen gebauten Rueckschritt

Am echten Verhalten gemessen:
  - mess-chat-liste und mess-alle-einzelsicht (beide vorher 5397)
    GLEICHZEITIG gestartet: 5906 und 5900, beide exit=0. Vorher war
    das unmoeglich.
  - die 48 neuen Nummern 5900-5947 auf diesem Rechner durchprobiert:
    keine belegt, keine vom System gesperrt
  - bild-chat.mjs durchgelaufen, drei Bilder, exit=0

Kein Eingriff am laufenden Dienst: helfer-port.mjs wird von index.js
und workspace.js nicht geladen (nachgesehen), nur von Pruef- und
Messwerkzeugen. Beide Haeuser unberuehrt -- es wird keine Zeile
angefasst, die eine Seite ausliefert.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-01 01:19:41 +02:00

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