From c81e862992d682a2c0565e212c0b850ab5e27946 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Sun, 16 Aug 2026 12:01:24 +0200 Subject: [PATCH] Wo der Bot nicht hinkommt, fragt das Panel -- Typ `panel` Bisher entschied `x.gamedig` darueber, ob aus einem Panel-Server ein Monitor-Eintrag wird: "kennt unsere Bibliothek dieses Spiel?" Das hat einen Fall ausgesperrt, den es wirklich gibt -- TeamSpeak. UND ZWAR NICHT, WEIL GAMEDIG ES NICHT KOENNTE. Am 16.08.2026 nachgeschlagen, nachdem die Annahme "gamedig kennt TeamSpeak nicht" sich als falsch erwies: gamedig 5.3.3 hat `teamspeak3`, DiscordGSM fuehrt es ebenfalls. Der Grund ist ein anderer und ein besserer: node_modules/gamedig/protocols/teamspeak3.js queryPort = options.teamspeakQueryPort || 10011 'use port=' -> 'serverinfo' -> 'clientlist' Es braucht den SERVERQUERY-Port. Beim Betreiber liegt der hier: 9987/udp -> 0.0.0.0:9987 Sprache, oeffentlich 10011/tcp -> 127.0.0.1:9988 ServerQuery, NUR localhost 30033/tcp -> 0.0.0.0:9989 Dateien, oeffentlich Der Bot laeuft im Container und erreicht 127.0.0.1 des Hosts nie. Zwei Gegenproben vom Host aus, beide gescheitert (die zweite auch deshalb, weil gamedig ohne `teamspeakQueryPort` stur 10011 nimmt -- den es dort gar nicht gibt). Eine Kennung einzutragen brauechte also einen OFFENEN Verwaltungsport, und das ist genau das, was die Regel "kein Port geht ohne Eintrag ins Netz" verhindert. Das Panel dagegen laeuft auf dem Host, darf hin und hat die Zugangsdaten -- und schickt seine eigene Messung seit heute im Statusbericht mit. Also: - `taugt` fragt `zeigbar` statt `gamedig`: "kann UEBERHAUPT JEMAND diesen Server abfragen -- wir oder das Panel fuer uns?" Mit Rueckfall auf `gamedig` fuer ein aelteres Panel; ohne den verschwaenden nach diesem Update alle Eintraege, und zwar lautlos. - Neuer eigener Typ `panel` neben `fivem` und `http`. Er ersetzt nichts: wo gamedig kann, bleibt es bei gamedig -- dessen Antwort bringt Karte, Fassung und Ping mit, die das Panel nicht liefert. - `spieler === null` heisst KEINE ANTWORT, nicht null Spieler. Das Panel sichert dazu einen Grund zu; wir werfen damit und landen im selben Zweig wie ein gescheiterter gamedig-Query, statt eine 0 zu erfinden. Und kein Ping: die Zahl ist eine gespeicherte Messung des Panels, keine Antwortzeit von hier. EINE FALLE BEIM ZWEITEN PARAMETER: `servers.map(queryServer)` haette `panelItems` den INDEX untergeschoben -- der erste Server saehe null, alle weiteren eine Zahl, an der `panelEintrag` still scheitert. Also `map((s) => queryServer(s, panelItems))`. Probe erweitert und im neu gebauten Abbild gelaufen, alles gruen -- einschliesslich "null Spieler sind eine Antwort, kein Ausfall". --- src/bot/server-monitor.js | 53 ++++++++++++++++++++++-- src/panel-abgleich.js | 28 ++++++++++++- tools/panel-abgleich-pruefen.mjs | 69 ++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 6 deletions(-) diff --git a/src/bot/server-monitor.js b/src/bot/server-monitor.js index 728da0c..14bc6ba 100644 --- a/src/bot/server-monitor.js +++ b/src/bot/server-monitor.js @@ -18,7 +18,7 @@ const RED = 0xf23f43; // standen mal ausgedachte wie "arkse" oder "terraria", die es dort gar nicht // gibt. Beim Ändern gegen `games` aus gamedig prüfen, nicht gegen das Gefühl. export const GAME_ICONS = { - fivem: '🚗', http: '🌐', minecraft: '⛏️', mbe: '⛏️', + fivem: '🚗', http: '🌐', minecraft: '⛏️', mbe: '⛏️', panel: '🛰️', teamfortress2: '🎩', counterstrike2: '🔫', csgo: '🔫', garrysmod: '🔧', rust: '🪓', valheim: '🛡️', palworld: '🐑', ase: '🦖', asa: '🦖', pixark: '🦖', dayz: '🧟', projectzomboid: '🧟', sdtd: '🧟', @@ -27,9 +27,13 @@ export const GAME_ICONS = { }; // fivem und http sind unsere eigenen Typen — gamedig kennt sie nicht. +// `panel` ist der dritte, und er ist anders als die beiden: da fragt der Bot +// gar nicht, sondern das Panel hat schon gefragt und schickt die Zahl mit. +// Siehe `queryServer`. const EIGENE = { fivem: { name: 'FiveM', port: 30120 }, http: { name: 'HTTP-Check', port: null }, + panel: { name: 'Über das Panel', port: null }, }; // Protokolle, die ohne Zugangs-Code gar nicht antworten. Der Code steht im @@ -73,8 +77,15 @@ const alertState = new Map(); // id → { fails, down, since } // Letztes Query-Ergebnis pro Server — für die öffentliche /server-Seite export const lastResults = new Map(); // id → Query-Ergebnis + checkedAt -/** Einen Server abfragen — fivem/http direkt, alles andere über gamedig */ -export async function queryServer(server) { +/** + * Einen Server abfragen — fivem/http direkt, `panel` gar nicht, alles andere + * über gamedig. + * + * `panelItems` ist der Statusbericht des Panels aus DIESEM Durchlauf. Er ist + * optional: fehlt er, verhält sich alles wie vorher, und ein `panel`-Eintrag + * gilt als offline — mit Grund. + */ +export async function queryServer(server, panelItems = null) { const base = { ...server, online: false, players: null, max: null, map: null, playerNames: [], ping: null, version: null, mods: null, @@ -84,6 +95,34 @@ export async function queryServer(server) { }; const started = Date.now(); try { + // ================== DAS PANEL HAT SCHON GEFRAGT ==================== + // + // Kein Netzverkehr von hier. Für TeamSpeak wäre er sogar unmöglich: + // gamedig braucht dafür den ServerQuery-Port, und der liegt beim + // Betreiber absichtlich auf `127.0.0.1` — der Bot im Container kommt + // dort nie hin. Das Panel läuft auf dem Host, darf hin und hat die + // Zugangsdaten. Also fragt es, und wir lesen ab. + // + // `spieler === null` HEISST „KEINE ANTWORT", NICHT NULL SPIELER. Das + // Panel garantiert dazu einen `spieler_grund` — entweder eine Zahl + // oder ein Grund, nie beides leer. Genau diese Zusage macht das hier + // sauber: wir werfen mit dem Grund und landen im selben Zweig wie ein + // gescheiterter gamedig-Query, statt eine 0 zu erfinden. + if (server.type === 'panel') { + const p = panelEintrag(panelItems, server); + if (!p) throw new Error('Das Panel kennt diesen Server nicht (mehr).'); + if (p.spieler == null) throw new Error(p.spieler_grund || 'Das Panel nennt keine Zahl.'); + return { + ...base, online: true, + players: p.spieler, + max: p.spieler_max ?? null, + spielname: p.spiel ?? null, + // KEIN PING. Die Zahl ist eine gespeicherte Messung des Panels + // (bis zu zehn Minuten alt) und keine Antwortzeit von hier. + // Eine Millisekundenzahl hinzuschreiben wäre erfunden. + ping: null, + }; + } if (server.type === 'fivem') { const res = await fetch(`${server.query_url.replace(/\/$/, '')}/dynamic.json`, { signal: AbortSignal.timeout(tuningMs.seconds('monitor_timeout')), @@ -406,7 +445,13 @@ export async function monitorTick(client) { const servers = listGameservers(); if (servers.length === 0) return; - const results = await Promise.all(servers.map(queryServer)); + // NICHT `servers.map(queryServer)` -- so stand es hier, und mit dem + // zweiten Parameter wäre es eine Falle gewesen: `map` reicht (element, + // index, array) weiter, `panelItems` bekäme also die Zahl 0. Der erste + // Server hätte damit `null` gesehen und alle weiteren eine Zahl, an der + // `panelEintrag` still scheitert -- ein Fehler, der nur beim zweiten + // Eintrag auftritt und nach Zufall aussieht. + const results = await Promise.all(servers.map((s) => queryServer(s, panelItems))); // Die Zusatzauskunft vom Panel: EINMAL je Durchlauf, nicht je Server. // Sie ist optional -- ist nichts eingetragen oder antwortet das Panel diff --git a/src/panel-abgleich.js b/src/panel-abgleich.js index 8d9979e..4287ce2 100644 --- a/src/panel-abgleich.js +++ b/src/panel-abgleich.js @@ -33,9 +33,28 @@ const uebernehmenStmt = db.prepare('UPDATE gameservers SET panel_name = ? WHERE * sie noch einmal, weil ein Bericht von einem älteren Panel kommen kann und * ein halber Eintrag schlimmer ist als keiner: er stünde dauerhaft auf * offline und schlüge Alarm. + * + * ================== WARUM NICHT MEHR `x.gamedig` ========================== + * + * Hier stand `x.gamedig`, also: „kennt unsere Bibliothek dieses Spiel?" Das + * hat einen Fall ausgesperrt, den es wirklich gibt — TeamSpeak. Nicht weil + * gamedig es nicht könnte (es hat `teamspeak3`), sondern weil der + * ServerQuery-Port des Betreibers absichtlich auf `127.0.0.1` liegt: der Bot + * läuft im Container und kommt dort nie hin. Am 16.08.2026 gegengeprüft, + * beide Versuche vom Host aus gescheitert. + * + * Das Panel dagegen sitzt auf dem Host, darf dorthin und hat die Zugangsdaten + * — es schickt seine eigene Messung seither im Statusbericht mit. Gefragt + * wird deshalb `zeigbar`: „kann ÜBERHAUPT JEMAND diesen Server abfragen — + * wir über gamedig oder das Panel für uns?" + * + * `?? Boolean(x.gamedig)` ist der Rückfall für ein ÄLTERES Panel, das + * `zeigbar` noch nicht kennt. Ohne ihn verschwänden nach einem Bot-Update + * schlagartig alle Einträge, und zwar lautlos. */ export function taugt(x) { - return Boolean(x?.discord && x.gamedig && x.adresse && x.port); + const zeigbar = x?.zeigbar ?? Boolean(x?.gamedig); + return Boolean(x?.discord && zeigbar && x.adresse && x.port); } /** @@ -86,7 +105,12 @@ export function abgleichen(items) { createGameserver({ name: x.name, - type: x.gamedig, + // OHNE gamedig-KENNUNG FRAGEN WIR DAS PANEL. Der Typ `panel` ist + // unser dritter eigener neben `fivem` und `http` — siehe + // `queryServer`. Wo gamedig kann, bleibt es bei gamedig: dessen + // Antwort bringt Karte, Fassung und Ping mit, die das Panel nicht + // liefert. Der neue Typ schliesst eine Luecke, er ersetzt nichts. + type: x.gamedig || 'panel', host: x.adresse, port: x.port, address: `${x.adresse}:${x.port}`, diff --git a/tools/panel-abgleich-pruefen.mjs b/tools/panel-abgleich-pruefen.mjs index c4c4707..2a99a9d 100644 --- a/tools/panel-abgleich-pruefen.mjs +++ b/tools/panel-abgleich-pruefen.mjs @@ -49,6 +49,42 @@ zeig(taugt(panel({ adresse: '' })) === false, 'ohne Adresse für Spieler -> nein zeig(taugt(panel({ port: null })) === false, 'ohne Port -> nein'); zeig(taugt(undefined) === false, 'gar nichts -> nein'); +console.log(''); +console.log('=== Was das Panel selbst abfragen kann (seit 16.08.2026)'); +// +// DER FALL, UM DEN ES GEHT: TeamSpeak. gamedig KANN es -- das Protokoll +// `teamspeak3` gibt es, DiscordGSM fuehrt es ebenfalls. Es braucht dafuer +// aber den ServerQuery-Port, und der liegt beim Betreiber absichtlich auf +// 127.0.0.1 (gemessen: 10011/tcp -> 127.0.0.1:9988). Der Bot im Container +// kommt dort nie hin, das Panel auf dem Host schon -- und es hat die +// Zugangsdaten. Also sagt das Panel `zeigbar: true` OHNE gamedig-Kennung, +// und wir lesen seine Zahl ab, statt einen Verwaltungsport zu oeffnen. +zeig(taugt(panel({ gamedig: null, zeigbar: true })) === true, 'ohne gamedig, aber das Panel kann fragen -> ja'); +zeig(taugt(panel({ gamedig: 'minecraft', zeigbar: false })) === false, 'Panel sagt nein -> nein, auch mit gamedig-Kennung'); +// DER RUECKFALL FUER EIN AELTERES PANEL. Ohne ihn verschwaenden nach einem +// Bot-Update schlagartig alle Eintraege, und zwar lautlos. +zeig(taugt(panel({ gamedig: 'minecraft' })) === true, 'aelteres Panel ohne `zeigbar` -> weiterhin ja'); +zeig(taugt(panel({ gamedig: null })) === false, 'aelteres Panel, kein gamedig -> weiterhin nein'); + +console.log(''); +console.log('=== Und welcher Typ dabei entsteht'); +{ + const vorher = db.listGameservers().length; + const b = abgleichen([panel({ name: 'ts3', gamedig: null, zeigbar: true, port: 9987 })]); + zeig(b.angelegt.includes('ts3'), 'ein Server ohne gamedig-Kennung wird angelegt', b.angelegt.join()); + const t = db.listGameservers().find((s) => s.name === 'ts3'); + zeig(t?.type === 'panel', 'und zwar mit dem Typ `panel` -- da fragt der Bot nicht selbst', t?.type); + // Wo gamedig kann, bleibt es bei gamedig: dessen Antwort bringt Karte, + // Fassung und Ping mit, die das Panel nicht liefert. + abgleichen([panel({ name: 'ts3', gamedig: null, zeigbar: true, port: 9987 }), panel()]); + zeig(db.listGameservers().find((s) => s.name === 'p1')?.type === 'minecraft', 'mit Kennung bleibt es bei gamedig -- der neue Typ ersetzt nichts'); + // Aufraeumen, damit die folgenden Abschnitte auf dem Stand von vorher + // rechnen. Eine Probe, die den Stand veraendert, macht die naechste Zeile + // unlesbar. + abgleichen([]); + zeig(db.listGameservers().length === vorher, 'und danach ist wieder aufgeraeumt'); +} + console.log(''); console.log('=== Anlegen'); let a = abgleichen([panel()]); @@ -111,6 +147,39 @@ zeig(danach.image_url === 'https://neu', 'und die Änderung ist drin'); abgleichen([panel()]); zeig(db.getGameserver(p1.id).image_url === 'https://neu', 'der nächste Durchlauf plättet sie nicht'); +console.log(''); +console.log('=== Der Typ `panel`: was der Bot daraus macht'); +{ + const { queryServer } = await import('../src/bot/server-monitor.js'); + const eintrag = { id: 99, name: 'ts3', type: 'panel', host: '1.2.3.4', port: 9987 }; + const bericht = (o) => [{ + name: 'ts3', port: 9987, spiel: 'TeamSpeak 3', zeigbar: true, gamedig: null, + spieler: 3, spieler_max: 32, spieler_grund: null, ...o, + }]; + + const r = await queryServer(eintrag, bericht()); + zeig(r.online === true && r.players === 3 && r.max === 32, 'die Zahl des Panels wird uebernommen', `${r.players}/${r.max}`); + zeig(r.spielname === 'TeamSpeak 3', 'samt Spielname aus dem Bericht', r.spielname); + // KEIN ERFUNDENER PING. Die Zahl ist eine gespeicherte Messung des Panels + // und keine Antwortzeit von hier. + zeig(r.ping === null, 'aber KEIN Ping -- den haben wir nicht gemessen', String(r.ping)); + + // null HEISST KEINE ANTWORT, NICHT NULL SPIELER. Das ist der ganze Punkt: + // ein leerer Server und ein unerreichbarer duerfen nicht gleich aussehen. + const ohne = await queryServer(eintrag, bericht({ spieler: null, spieler_grund: 'Der Server läuft nicht.' })); + zeig(ohne.online === false, 'ohne Zahl: offline, statt eine 0 zu erfinden'); + zeig(ohne.players === null, 'und die Spielerzahl bleibt null', String(ohne.players)); + + // NULL SPIELER SIND EINE ANTWORT. Der haeufigste Zustand ueberhaupt -- + // wer ihn als "offline" liest, faerbt einen laufenden Server rot. + const leer = await queryServer(eintrag, bericht({ spieler: 0 })); + zeig(leer.online === true && leer.players === 0, 'null Spieler sind eine Antwort, kein Ausfall'); + + // Und die Faelle, in denen gar nichts da ist. + zeig((await queryServer(eintrag, null)).online === false, 'ohne Panel-Bericht: offline statt Absturz'); + zeig((await queryServer(eintrag, [])).online === false, 'leerer Bericht: offline statt Absturz'); +} + console.log(''); console.log('=== Kaputte Berichte'); zeig(abgleichen(null).angelegt.length === 0, 'null -> nichts passiert');