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]>
This commit is contained in:
2026-10-01 01:19:41 +02:00
co-authored by Claude Opus 5
parent 8e18c7bcf0
commit f6225d7341
34 changed files with 517 additions and 123 deletions
+251 -2
View File
@@ -20,8 +20,11 @@
sie faellt nur niemandem auf, bis zwei Laeufe kollidieren.
===================================================================== */
import { readdirSync, readFileSync } from "node:fs";
import { eigenerPort, portNummer } from "./helfer-port.mjs";
import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { eigenerPort, portNummer, MESS_BASIS, GESPERRT } from "./helfer-port.mjs";
let ok_ = 0, fehl = 0;
const ok = (b, text) => { if (b) { ok_++; console.log(" ok " + text); }
@@ -30,6 +33,11 @@ const ok = (b, text) => { if (b) { ok_++; console.log(" ok " + text); }
const ORDNER = new URL(".", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1");
const dateien = readdirSync(ORDNER)
.filter((f) => f.startsWith("pruef-") && f.endsWith(".mjs")).sort();
/* DIE MESSDATEIEN GEHOEREN DAZU (01.10.2026). Sie standen bis heute
ganz ausserhalb dieser Pruefung -- und genau dort ist der Fehler
auch gewachsen, den sie verhindern soll. */
const messen = readdirSync(ORDNER)
.filter((f) => f.startsWith("mess-") && f.endsWith(".mjs")).sort();
console.log("\n=== 1. Niemand traegt eine Nummer von Hand ein ===");
const vonHand = [];
@@ -186,6 +194,247 @@ try { await eigenerPort(import.meta, "Gegenprobe", 7); }
catch (f) { zuHoheNummer = /Nummern je Datei/.test(f?.message || ""); }
ok(zuHoheNummer, "und eine dritte Nummer je Datei gibt es nicht");
console.log("\n=== 5. Die Messdateien leiten ihre Nummer auch ab (01.10.2026) ===");
/* WAS HIER AM 01.10. GEMESSEN WURDE, BEVOR ES DIESE PRUEFUNG GAB:
24 Messdateien, und EINUNDZWANZIG davon hatten ihre Nummer nicht
nur von Hand, sondern ganz ohne Waechter -- schlicht
`const PORT = 5397;`. Fuenf Nummern waren untereinander doppelt
vergeben (5397, 5399, 5461, 5483, 5491), und acht lagen mitten im
dicht belegten Bereich der Pruefungen (5000-5409).
Gekostet hat das einen ganzen Messlauf: mess-buehne und
mess-reaktion teilten sich die 5483, ein liegengebliebener Prozess
hielt sie, und die zweite Messung lief gegen einen fremden Stand.
Die Messdateien standen bei alldem AUSSERHALB dieser Pruefung --
sie hat 204 Pruefdateien angesehen und die 24 daneben nie. Eine
Sicherung, die nur die halbe Menge kennt, faellt genau in der
anderen Haelfte aus. */
{
const festeNummer = [];
const ohneWaechter = [];
const platteSumme = [];
for (const f of messen) {
const code = readFileSync(ORDNER + "/" + f, "utf8")
.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
if (/const\s+\w*PORT\w*\s*=\s*\d/.test(code)
|| /portMussFreiSein\(\s*\d/.test(code)) festeNummer.push(f);
/* `PORT + 1` IST KEINE ABLEITUNG. `portNummer` ueberspringt
gesperrte Nummern -- liegt einmal eine dazwischen, ist die
naechste Zahl die Nummer der naechsten DATEI. Heute liegt dort
keine; das ist Glueck, kein Entwurf. */
if (/PORT\s*\+\s*1\b/.test(code)) platteSumme.push(f);
const startetServer = /import\(\s*["'`]\.\/index\.js["'`]\s*\)/.test(code)
|| /process\.env\.PORT\s*=/.test(code);
if (startetServer && !/eigenerPort\(\s*import\.meta/.test(code)) ohneWaechter.push(f);
}
ok(messen.length > 10, `${messen.length} Messdateien angesehen`);
ok(festeNummer.length === 0,
`keine traegt eine Nummer von Hand (${festeNummer.slice(0, 4).join(", ") || "—"})`);
ok(ohneWaechter.length === 0,
`jede, die einen Server startet, leitet ihre Nummer ab (${ohneWaechter.slice(0, 4).join(", ") || "—"})`);
ok(platteSumme.length === 0,
`keine rechnet ihre zweite Nummer als PORT + 1 (${platteSumme.slice(0, 4).join(", ") || "—"})`);
/* ---- KANN DIESE ERKENNUNG AUCH NEIN SAGEN? -----------------------
Die drei Zeilen darueber melden „in Ordnung", solange sie NICHTS
finden -- und genau so sieht auch eine Erkennung aus, die man zu
eng gefasst hat. Am 01.10. waren es 21 Treffer; ab morgen sollen
es null sein, und dann sagt die Zahl allein nichts mehr darueber,
ob noch hingesehen wird. */
const alsFest = (c) => /const\s+\w*PORT\w*\s*=\s*\d/.test(c)
|| /portMussFreiSein\(\s*\d/.test(c);
ok(alsFest("const PORT = 5397;"), "Gegenprobe: eine feste Nummer faellt auf");
/* ZUSAMMENGESETZT, NICHT AUSGESCHRIEBEN: Abschnitt 1 dieser Datei
sucht in JEDER Pruefdatei nach genau diesem Wortlaut und
entfernt vorher nur Kommentare, keine Zeichenketten. Stuende
das Beispiel hier am Stueck, zeigte diese Pruefung sich
selbst an -- gemessen, sie tat es beim ersten Lauf. */
ok(alsFest("const HTTPS_PORT = await portMuss" + "FreiSein(5484);"),
" und eine feste Nummer im Waechter ebenfalls");
ok(!alsFest("const PORT = await eigenerPort(import.meta);"),
" wer ableitet, faellt NICHT auf");
ok(/PORT\s*\+\s*1/.test("const HTTPS_PORT = PORT + 1;"),
" und PORT + 1 wird als solches erkannt");
/* ---- Die beiden Bereiche beruehren sich nicht -------------------- */
const pruefPorts = [];
dateien.forEach((f, i) => pruefPorts.push(portNummer(i * 2), portNummer(i * 2 + 1)));
const messPorts = [];
messen.forEach((f, i) => messPorts.push(
portNummer(i * 2, MESS_BASIS), portNummer(i * 2 + 1, MESS_BASIS)));
const hoechstPruef = Math.max(...pruefPorts);
ok(hoechstPruef < MESS_BASIS,
`die Pruefungen enden bei ${hoechstPruef}, die Messungen fangen bei ${MESS_BASIS} an`
+ ` — Luft fuer ${((MESS_BASIS - hoechstPruef) / 2) | 0} weitere Pruefdateien`);
const zusammen = [...pruefPorts, ...messPorts];
ok(new Set(zusammen).size === zusammen.length,
`alle ${zusammen.length} Nummern beider Sorten sind verschieden`);
const boeseMess = messPorts.filter((p) => GESPERRT.has(p));
ok(boeseMess.length === 0,
`keine Messnummer steht auf der Sperrliste (${boeseMess.join(", ") || "—"})`);
/* ---- GEMESSEN, nicht gerechnet: die echte Funktion befragen ------ */
const erste = messen[0];
const echt = await eigenerPort(
{ url: pathToFileURL(ORDNER + "/" + erste).href }, "pruef-portnummern (Selbsttest)");
ok(echt === portNummer(0, MESS_BASIS) && echt >= MESS_BASIS,
`${erste} bekommt von eigenerPort wirklich ${echt}`);
const letzte = messen.at(-1);
const echt2 = await eigenerPort(
{ url: pathToFileURL(ORDNER + "/" + letzte).href }, "pruef-portnummern (Selbsttest)", 1);
ok(echt2 === portNummer((messen.length - 1) * 2 + 1, MESS_BASIS),
`und ${letzte} als zweite Nummer ${echt2} — beide aus dem Messbereich`);
}
console.log("\n=== 6. Gegenprobe: die Wache gegen das Zusammenwachsen ===");
/* Eine Grenze, die nie anschlaegt, ist keine Grenze. Waechst die Zahl
der Pruefdateien so weit, dass ihr Bereich den der Messungen
erreicht, bekaemen zwei Laeufe still dieselbe Nummer -- genau der
Fehler, den diese ganze Ableitung beseitigt.
GEPRUEFT WIRD DIE ECHTE FUNKTION, nicht eine Nachbildung ihrer
Bedingung: In einem Wegwerf-Ordner liegen so viele leere
`pruef-*.mjs`, dass die letzte ueber MESS_BASIS hinauskaeme. Ein
eigener Ordner deshalb, weil echte Dateien hier die Nummern aller
anderen verschieben wuerden. */
{
const wegwerf = mkdtempSync(join(tmpdir(), "portwache-"));
const noetig = Math.ceil((MESS_BASIS - portNummer(0)) / 2) + 2;
for (let i = 0; i < noetig; i++) {
writeFileSync(join(wegwerf, `pruef-${String(i).padStart(5, "0")}.mjs`), "");
}
const letzte = join(wegwerf, `pruef-${String(noetig - 1).padStart(5, "0")}.mjs`);
let sagte = "";
try { await eigenerPort({ url: pathToFileURL(letzte).href }, "Gegenprobe"); }
catch (f) { sagte = f?.message || ""; }
ok(/stoesst an den der Messdateien/.test(sagte),
`bei ${noetig} Pruefdateien bricht die Ableitung ab statt still zu ueberlappen`);
ok(/MESS_BASIS/.test(sagte) && /hochsetzen/.test(sagte),
"und sagt dabei, was zu tun ist");
/* UND DIE ANDERE RICHTUNG: Eine Datei knapp DARUNTER muss noch
durchgehen. Ohne das hiesse „bricht ab" womoeglich nur, dass in
diesem Ordner alles abbricht. */
const knapp = join(wegwerf, `pruef-${String(noetig - 30).padStart(5, "0")}.mjs`);
let ging = 0;
try { ging = await eigenerPort({ url: pathToFileURL(knapp).href }, "Gegenprobe"); }
catch { ging = 0; }
ok(ging > 0 && ging < MESS_BASIS,
`eine Datei knapp darunter bekommt weiter ihre Nummer (${ging})`);
rmSync(wegwerf, { recursive: true, force: true });
}
console.log("\n=== 7. Die uebrigen Werkzeuge, die einen Server starten ===");
/* BEIM UMBAU AM 01.10. AUFGEFALLEN: Es gibt eine dritte Sorte. Zehn
Werkzeuge starten den Workspace ebenfalls -- `bild-*`, `tmp-lokal`,
`tiktok-videos`, `zz-*` -- und alle zehn auf einer Nummer von Hand;
sieben davon ohne jeden Waechter.
SIE LEITEN NICHT AB, UND DAS IST ABSICHT: Es waeren vier weitere
Vorsilben mit ein bis sechs Dateien, also vier Bereiche fuer zehn
Werkzeuge, die nie gleichzeitig laufen. Der saubere Weg waere, sie
`mess-` zu nennen -- ein eigener Schritt.
WAS HIER GEPRUEFT WIRD, IST DESHALB WENIGER UND TROTZDEM DAS
ENTSCHEIDENDE: dass keine dieser Nummern in einen abgeleiteten
Bereich faellt und keine zweimal vergeben ist. Genau daraus ist die
Doppelung bei den Messdateien entstanden -- aus einer Lage, die
einmal in Ordnung war und die danach niemand mehr nachgesehen hat. */
{
const uebrige = readdirSync(ORDNER)
.filter((f) => f.endsWith(".mjs")
&& !f.startsWith("pruef-") && !f.startsWith("mess-") && !f.startsWith("helfer-"))
.filter((f) => /process\.env\.PORT\s*=/.test(readFileSync(ORDNER + "/" + f, "utf8")))
.sort();
ok(uebrige.length > 0, `${uebrige.length} weitere Werkzeuge starten einen Server`);
/* DIE NUMMER MUSS ABLESBAR SEIN. Findet sich keine, wird nicht
uebersprungen -- ein uebersprungener Fall beweist nichts, und
genau hier soll er auffallen. */
const nummern = new Map();
const unlesbar = [];
for (const f of uebrige) {
const code = readFileSync(ORDNER + "/" + f, "utf8")
.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
const t = code.match(/process\.env\.PORT\s*=\s*[`"']?(\d{3,5})/)
|| code.match(/const PORT\s*=\s*(\d{3,5})/)
|| code.match(/portMussFreiSein\(\s*(\d{3,5})/);
if (!t) { unlesbar.push(f); continue; }
nummern.set(f, Number(t[1]));
}
ok(unlesbar.length === 0,
`bei jedem steht die Nummer im Quelltext (${unlesbar.join(", ") || "—"})`);
const werte = [...nummern.values()];
const doppelte = werte.filter((p, i) => werte.indexOf(p) !== i);
ok(werte.length > 0 && doppelte.length === 0,
`keine ihrer ${werte.length} Nummern ist doppelt (${[...new Set(doppelte)].join(", ") || "—"})`);
const pruefPorts2 = [];
dateien.forEach((f, i) => pruefPorts2.push(portNummer(i * 2), portNummer(i * 2 + 1)));
const messPorts2 = [];
messen.forEach((f, i) => messPorts2.push(
portNummer(i * 2, MESS_BASIS), portNummer(i * 2 + 1, MESS_BASIS)));
const abgeleitet = new Set([...pruefPorts2, ...messPorts2]);
const drin = [...nummern].filter(([, p]) => abgeleitet.has(p));
ok(werte.length > 0 && drin.length === 0,
"keine liegt in einem abgeleiteten Bereich"
+ ` (${drin.map(([f, p]) => `${f}:${p}`).join(", ") || "—"})`);
/* GEGENPROBE: Diese drei Zeilen melden „in Ordnung", solange sie
nichts finden. Also einmal zeigen, dass sie etwas finden KOENNEN. */
const probe = [...abgeleitet][0];
ok(abgeleitet.has(probe) && probe >= 5000,
`Gegenprobe: ${probe} waere ein Treffer — die Liste der abgeleiteten`
+ ` Nummern hat ${abgeleitet.size} Eintraege und ist nicht leer`);
/* ---- EIN WAECHTER, DESSEN ANTWORT NIEMAND LIEST, IST KEINER -----
Am 01.10. in allen sechs `bild-*.mjs` gefunden:
await portMussFreiSein(4315, "das Bildwerkzeug");
process.env.PORT = "4315";
const BASIS = "http://127.0.0.1:4315";
Der Waechter 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. Wer eine davon aendert,
hat einen Server auf dem einen Port und ein Werkzeug, das den
anderen abfragt. */
const weggeworfen = [];
const mehrfach = [];
for (const f of uebrige) {
const code = readFileSync(ORDNER + "/" + f, "utf8")
.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
if (/^\s*await\s+(portMussFreiSein|eigenerPort)\(/m.test(code)) weggeworfen.push(f);
const n = nummern.get(f);
if (n !== undefined) {
const wieOft = (code.match(new RegExp(String(n), "g")) || []).length;
if (wieOft > 1) mehrfach.push(`${f}:${wieOft}x`);
}
}
ok(weggeworfen.length === 0,
`jedes benutzt die Antwort des Waechters (${weggeworfen.join(", ") || "—"})`);
ok(mehrfach.length === 0,
`und schreibt seine Nummer nur einmal hin (${mehrfach.join(", ") || "—"})`);
/* GEGENPROBE zu beiden: Erkennen sie den Rueckschritt auch? */
const wirftWeg = (c) => /^\s*await\s+(portMussFreiSein|eigenerPort)\(/m.test(c);
ok(wirftWeg('await portMuss' + 'FreiSein(4315, "x");'),
"Gegenprobe: eine weggeworfene Antwort faellt auf");
ok(!wirftWeg('const PORT = await portMuss' + 'FreiSein(4315, "x");'),
" wer sie in eine Zeile schreibt, faellt NICHT auf");
ok((('p = 4315; q = 4315;').match(/4315/g) || []).length === 2,
" und eine zweimal genannte Nummer wird gezaehlt");
console.log(` ${[...nummern].map(([f, p]) => `${f.replace(/\.mjs$/, "")} ${p}`).join(", ")}`);
}
console.log(`\n${ok_} Pruefungen, ${fehl} Fehler`);
console.log(fehl ? "NICHT IN ORDNUNG\n" : "ALLES IN ORDNUNG\n");
process.exit(fehl ? 1 : 0);