feat: der Monitor fragt das Panel, wenn eine Abfrage nicht ausreicht
gamedig FRAGT einen Server. Ein startendes Modpack antwortet nicht, und
nach zwei Fehlversuchen stand im Alarm-Kanal eine Ausfallmeldung, obwohl
niemand etwas kaputtgemacht hat -- am ATM10 des Betreibers gemessen jedes
Mal 306 Sekunden Fehlalarm.
Das d4rk_gameserver-Panel SIEHT den Container, statt ihn zu fragen. Neu
ist deshalb `src/panel.js`: Zustand, Fertig-Merkmal, RAM und CPU kommen
als Zusatzauskunft dazu, einmal je Durchlauf abgerufen und 20 Sekunden
zwischengespeichert.
Was das im Alarm-Kanal aendert:
startet gerade kein Alarm, aber nur innerhalb einer Gnadenfrist von
15 Minuten. Ohne diese Grenze fraesse die Anbindung
genau den Alarm, fuer den es sie gibt -- `bereit`
wird nie von allein wahr, ein haengender Start bliebe
sonst fuer immer stumm.
im Panel gestoppt kein Alarm (Code 0/143/137)
abgestuerzt ALARM, mit Code im Embed
OOM-Kill ALARM, und beim Namen genannt statt als Absturz
getarnt -- bei Modpacks die haeufigste Ursache
Container weg ALARM
Der Zaehler wird beim Unterdruecken NICHT zurueckgesetzt: ein bereits
gemeldeter Ausfall bleibt gemeldet, sonst verschluckt ein Stopp im Panel
die spaetere "wieder online"-Entwarnung und im Kanal bliebe ein Alarm
ohne Aufloesung stehen.
ES IST DURCHGEHEND OPTIONAL. Adresse und Zeichen stehen im Webinterface
unter "Server" und NICHT in der .env; ohne Eintrag -- und ebenso, wenn
das Panel nicht antwortet -- verhaelt sich der Monitor exakt wie vorher.
Das Zeichen darf nur lesen, damit auch ein verlorenes niemandem einen
Server stoppen kann.
`tools/panel-pruefen.mjs` prueft die Logik ohne Discord und ohne
Datenbank; deshalb laedt panel.js die Einstellungen erst beim Aufruf.
This commit is contained in:
@@ -8,6 +8,7 @@ import { brandFooter, botStatusText } from '../runtime-settings.js';
|
||||
import { brandEmbed } from '../embeds.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
import { tuning, tuningMs, everyTuned } from '../tuning.js';
|
||||
import { panelStatus, panelEintrag, zustandText, lastText, startetNoch, gewolltAus } from '../panel.js';
|
||||
|
||||
const GREEN = 0x23a55a;
|
||||
const RED = 0xf23f43;
|
||||
@@ -257,8 +258,14 @@ export function buildServerEmbed(r) {
|
||||
// nicht verlässlich, und dann stünde das Steuerzeichen wörtlich da.
|
||||
kopf.push(`*${r.spielname.trim().slice(0, 200)}*`);
|
||||
}
|
||||
// Wenn GameDig nichts erreicht hat, das Panel aber sagt, dass der
|
||||
// Container laeuft und noch startet: DAS ist die richtige Auskunft. Ein
|
||||
// Modpack braucht Minuten bis zum ersten Spieler -- am ATM10 des
|
||||
// Betreibers 306 Sekunden -- und ein rotes "Offline" waere in dieser Zeit
|
||||
// schlicht falsch.
|
||||
const ausPanel = r.online ? null : zustandText(r.panel);
|
||||
kopf.push([
|
||||
r.online ? '🟢 **Online**' : '🔴 **Offline**',
|
||||
r.online ? '🟢 **Online**' : (ausPanel ?? '🔴 **Offline**'),
|
||||
zugang.grund ? `🔒 ${zugang.grund}` : null,
|
||||
r.online && r.ping != null ? `${r.ping} ms` : null,
|
||||
].filter(Boolean).join(' · '));
|
||||
@@ -276,6 +283,12 @@ export function buildServerEmbed(r) {
|
||||
r.mods != null ? { name: 'Mods', value: String(r.mods), inline: true } : null,
|
||||
]));
|
||||
|
||||
// Was nur das Panel weiss: was der Server gerade verbraucht.
|
||||
{
|
||||
const last = lastText(r.panel);
|
||||
if (last) embed.addFields({ name: 'Auf dem Wirt', value: last });
|
||||
}
|
||||
|
||||
if (r.online && r.playerNames.length > 0) {
|
||||
embed.addFields({
|
||||
name: `Wer drauf ist (${r.playerNames.length})`,
|
||||
@@ -318,6 +331,31 @@ async function handleAlerts(client, r) {
|
||||
alertState.set(r.id, { fails: 0, down: false, since: null });
|
||||
return;
|
||||
}
|
||||
|
||||
// KEIN ALARM, WENN DAS PANEL DEN ZUSTAND ERKLÄREN KANN.
|
||||
//
|
||||
// Das ist der eigentliche Gewinn der Anbindung. GameDig *fragt* einen
|
||||
// Server; ein startender antwortet nicht, und nach zwei Fehlversuchen
|
||||
// stünde hier eine Ausfallmeldung. Bei einem Modpack sind das jedes Mal
|
||||
// Minuten Fehlalarm — am ATM10 des Betreibers 306 Sekunden bis zum ersten
|
||||
// Spieler. Das Panel *sieht* den Container und weiß, dass er läuft und
|
||||
// nur das Fertig-Merkmal noch nicht durch ist.
|
||||
//
|
||||
// Zwei Fälle, eine Behandlung: nichts tun. Der Zähler bleibt dabei
|
||||
// ausdrücklich STEHEN statt zurückgesetzt zu werden, aus zwei Gründen:
|
||||
//
|
||||
// - Läuft die Gnadenfrist ab, ohne dass der Server fertig wird, greift
|
||||
// `startetNoch` nicht mehr und der Alarm kommt sofort statt erst nach
|
||||
// zwei weiteren Durchläufen.
|
||||
// - Ein bereits gemeldeter Ausfall bleibt gemeldet. Sonst verschluckt
|
||||
// ein Stopp im Panel die spätere „✅ wieder online"-Entwarnung, und im
|
||||
// Alarm-Kanal bliebe ein 🚨 ohne Auflösung stehen.
|
||||
//
|
||||
// Und was NICHT unterdrückt wird: ein Absturz, ein OOM-Kill und ein
|
||||
// fehlender Container. Das sind die Fälle, für die es den Kanal gibt —
|
||||
// hier hilft das Panel dem Alarm, statt ihn zu ersetzen.
|
||||
if (startetNoch(r.panel) || gewolltAus(r.panel)) return;
|
||||
|
||||
const fails = s.fails + 1;
|
||||
if (!s.down && fails >= tuning('monitor_fails')) {
|
||||
const channel = await client.channels.fetch(channelId).catch(() => null);
|
||||
@@ -343,6 +381,12 @@ export async function monitorTick(client) {
|
||||
|
||||
const results = await Promise.all(servers.map(queryServer));
|
||||
|
||||
// Die Zusatzauskunft vom Panel: EINMAL je Durchlauf, nicht je Server.
|
||||
// Sie ist optional -- ist nichts eingetragen oder antwortet das Panel
|
||||
// nicht, bleibt `panel` schlicht null und alles läuft wie vorher.
|
||||
const panelItems = await panelStatus();
|
||||
for (const r of results) r.panel = panelEintrag(panelItems, r);
|
||||
|
||||
// Verlauf für die Web-Seite festhalten + Cache fürs API
|
||||
for (const r of results) {
|
||||
lastResults.set(r.id, { ...r, checkedAt: Date.now() });
|
||||
|
||||
@@ -243,6 +243,13 @@ export const MODULES = [
|
||||
fields: [
|
||||
{ setting: 'status_channel_id', label: 'Status-Kanal', art: 'kanal', hint: 'hier steht das Live-Embed' },
|
||||
{ setting: 'server_alert_channel_id', label: 'Alarm-Kanal', art: 'kanal', hint: '🚨 down / ✅ wieder online' },
|
||||
// Optional. GameDig FRAGT einen Server -- ein startender antwortet
|
||||
// nicht, und das sieht aus wie ein Ausfall. Das Panel SIEHT den
|
||||
// Container und kann "startet gerade" von "aus" unterscheiden.
|
||||
{ setting: 'panel_url', label: 'Panel-Adresse', art: 'text',
|
||||
hint: 'optional, z. B. https://panel.d4rkst3r.de — unterscheidet „startet gerade" von „offline"' },
|
||||
{ setting: 'panel_zeichen', label: 'Panel-Zeichen', art: 'text',
|
||||
hint: 'im Panel unter „Zugang für Programme" anlegen; darf nur lesen' },
|
||||
],
|
||||
tool: { tab: 'server', label: 'Game-Server verwalten' },
|
||||
},
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
// Auskunft vom d4rk_gameserver-Panel.
|
||||
//
|
||||
// WARUM ÜBERHAUPT, wo der Monitor doch schon GameDig hat: weil GameDig einen
|
||||
// Server FRAGT, und ein Server, der noch startet, antwortet nicht. Für den
|
||||
// Monitor sieht das aus wie „offline" — und genau dann steht im Alarm-Kanal
|
||||
// eine Ausfallmeldung, obwohl niemand etwas kaputtgemacht hat.
|
||||
//
|
||||
// Ein Modpack braucht bis zum ersten Spieler Minuten; am ATM10 des Betreibers
|
||||
// gemessen sind es 306 Sekunden. Fünf Minuten Fehlalarm bei jedem Neustart.
|
||||
//
|
||||
// Das Panel weiß es besser, weil es den Container sieht statt ihn zu fragen:
|
||||
//
|
||||
// zustand was Docker sagt (running, exited, weg)
|
||||
// bereit ob das Fertig-Merkmal im Protokoll stand
|
||||
//
|
||||
// Damit lässt sich „läuft noch gar nicht" von „startet gerade" und von
|
||||
// „hängt" unterscheiden.
|
||||
//
|
||||
// ES IST ABSICHTLICH OPTIONAL. Ohne Adresse und Zeichen verhält sich der
|
||||
// Monitor exakt wie vorher, und wenn das Panel nicht antwortet, ebenfalls.
|
||||
// Eine Zusatzauskunft darf nie dazu führen, dass die Hauptauskunft ausfällt.
|
||||
|
||||
// Kurz zwischenspeichern: der Monitor fragt je Server einmal, und bei acht
|
||||
// Servern wären das acht Abrufe für dieselbe Antwort.
|
||||
let zwischen = { zeit: 0, items: null };
|
||||
const FRISCH_MS = 20_000;
|
||||
|
||||
/** Der ganze Panel-Status, oder null. Wirft nie. */
|
||||
export async function panelStatus() {
|
||||
// ERST HIER GELADEN und nicht oben im Modul: `runtime-settings` zieht die
|
||||
// Datenbank mit herein, und dann laesst sich keine der reinen Funktionen
|
||||
// unten mehr ohne better-sqlite3 pruefen. Eine Probe, die eine Datenbank
|
||||
// braucht, um eine Zeichenkette zu vergleichen, schreibt niemand.
|
||||
const { panelUrl, panelZeichen } = await import('./runtime-settings.js');
|
||||
const basis = panelUrl();
|
||||
const zeichen = panelZeichen();
|
||||
if (!basis || !zeichen) return null;
|
||||
|
||||
if (zwischen.items && Date.now() - zwischen.zeit < FRISCH_MS) return zwischen.items;
|
||||
|
||||
try {
|
||||
const steuer = AbortSignal.timeout(5000);
|
||||
const antwort = await fetch(`${basis.replace(/\/$/, '')}/api/oeffentlich/status`, {
|
||||
headers: { authorization: `Bearer ${zeichen}` },
|
||||
signal: steuer,
|
||||
});
|
||||
if (!antwort.ok) return null;
|
||||
const daten = await antwort.json();
|
||||
zwischen = { zeit: Date.now(), items: Array.isArray(daten.items) ? daten.items : [] };
|
||||
return zwischen.items;
|
||||
} catch {
|
||||
// Panel weg, Zeichen zurückgezogen, Netz hakt — der Monitor läuft
|
||||
// dann einfach ohne diese Zusatzauskunft weiter.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Den Panel-Eintrag zu einem Monitor-Server finden.
|
||||
*
|
||||
* ÜBER DEN PORT, nicht über den Namen. Namen sind Geschmackssache und stehen
|
||||
* in zwei Werkzeugen selten gleich; der Port ist eine Zahl, die beide kennen
|
||||
* und die nur einmal vergeben ist.
|
||||
*/
|
||||
export function panelEintrag(items, server) {
|
||||
if (!items?.length) return null;
|
||||
const port = Number(server?.port);
|
||||
if (Number.isFinite(port) && port > 0) {
|
||||
const treffer = items.find((x) => Number(x.port) === port);
|
||||
if (treffer) return treffer;
|
||||
}
|
||||
// Zweite Möglichkeit: gleicher Name, klein geschrieben. Hilft, wenn
|
||||
// jemand den Port im Monitor weggelassen hat.
|
||||
const name = String(server?.name ?? '').trim().toLowerCase();
|
||||
return items.find((x) => String(x.name).trim().toLowerCase() === name) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wie lange ein Start dauern darf, bevor er als hängend gilt.
|
||||
*
|
||||
* Gemessen: ATM10 braucht 306 Sekunden bis zum ersten Spieler. Fünfzehn
|
||||
* Minuten sind knapp das Dreifache — genug Luft für ein größeres Pack auf
|
||||
* kaltem Cache, und kurz genug, dass ein hängender Start noch am selben
|
||||
* Abend auffällt.
|
||||
*
|
||||
* DIESE GRENZE IST NÖTIG, sonst frisst die Anbindung genau den Alarm, für den
|
||||
* es sie gibt: `bereit` wird nie von allein wahr. Ein Server, der beim Start
|
||||
* hängt, bliebe für immer „startet gerade" — und für immer ohne Meldung.
|
||||
*/
|
||||
export const STARTGNADE_MS = 15 * 60_000;
|
||||
|
||||
/** Läuft der Container und ist er noch in der zugestandenen Startzeit? */
|
||||
export function startetNoch(eintrag) {
|
||||
if (eintrag?.zustand !== 'running' || eintrag.bereit) return false;
|
||||
// Ohne Startzeit lässt sich nichts begrenzen. Das passiert nur, wenn
|
||||
// Dockers Zeitstempel unlesbar ist — dieselbe Antwort, die uns gerade
|
||||
// „running" gesagt hat. Ein kaputtes Feld ist kein hängender Server,
|
||||
// deshalb hier Nachsicht.
|
||||
if (!eintrag.seit) return true;
|
||||
return Date.now() - eintrag.seit < STARTGNADE_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* War das Ende gewollt?
|
||||
*
|
||||
* 0 heißt sauber beendet, 143 ist SIGTERM (`docker stop`) und 137 SIGKILL
|
||||
* nach abgelaufener Frist — alle drei entstehen, wenn jemand im Panel auf
|
||||
* Stopp drückt. Jeder andere Code ist ein Absturz, und `oom` heißt: der
|
||||
* Speicher war zu knapp. Genau dafür ist der Alarm-Kanal da.
|
||||
*/
|
||||
export function gewolltAus(eintrag) {
|
||||
if (eintrag?.zustand !== 'exited') return false;
|
||||
if (eintrag.oom) return false;
|
||||
// `code` fehlt bei einem älteren Panel. Dann ist die Frage nicht
|
||||
// beantwortbar, und im Zweifel gilt der Ausfall als echt.
|
||||
return eintrag.code === 0 || eintrag.code === 143 || eintrag.code === 137;
|
||||
}
|
||||
|
||||
/**
|
||||
* Was im Embed stehen soll, wenn GameDig nichts erreicht hat.
|
||||
*
|
||||
* Gibt null zurück, wenn das Panel nichts beizutragen hat -- dann bleibt es
|
||||
* beim schlichten „Offline".
|
||||
*/
|
||||
export function zustandText(eintrag) {
|
||||
if (!eintrag) return null;
|
||||
|
||||
if (eintrag.zustand === 'running' && !eintrag.bereit) {
|
||||
const s = eintrag.seit ? Math.round((Date.now() - eintrag.seit) / 1000) : null;
|
||||
const dauer = s == null ? '' : s < 90 ? ` (seit ${s} s)` : ` (seit ${Math.round(s / 60)} min)`;
|
||||
// Nach der Gnadenfrist ist „startet gerade" keine Auskunft mehr,
|
||||
// sondern eine Beschwichtigung. Dann steht hier, was Sache ist.
|
||||
return startetNoch(eintrag)
|
||||
? `🟡 **Startet gerade**${dauer}`
|
||||
: `🟠 **Startet seit ${dauer.replace(/^ \(seit |\)$/g, '')} und ist nicht fertig**`;
|
||||
}
|
||||
|
||||
if (eintrag.zustand === 'exited') {
|
||||
if (eintrag.oom) return '🔴 **Vom Speicher erschlagen** (out of memory)';
|
||||
if (gewolltAus(eintrag)) return '⚫ **Gestoppt** (im Panel)';
|
||||
// Auch ohne `code` (älteres Panel) landet man hier — dann ohne Zahl.
|
||||
return eintrag.code == null
|
||||
? '🔴 **Beendet**'
|
||||
: `🔴 **Abgestürzt** (Code ${eintrag.code})`;
|
||||
}
|
||||
|
||||
if (eintrag.zustand === 'weg') return '🔴 **Container fehlt**';
|
||||
return null;
|
||||
}
|
||||
|
||||
/** „1,2 GB / 7 GB · 14 %" — nur wenn das Panel Werte hat. */
|
||||
export function lastText(eintrag) {
|
||||
if (!eintrag || eintrag.zustand !== 'running') return null;
|
||||
const teile = [];
|
||||
if (eintrag.ram) {
|
||||
const gb = (b) => `${(b / 1024 ** 3).toFixed(1)} GB`;
|
||||
teile.push(eintrag.ram_grenze ? `${gb(eintrag.ram)} / ${gb(eintrag.ram_grenze)}` : gb(eintrag.ram));
|
||||
}
|
||||
// Das Panel liefert Dockers Kern-Prozent (200 = zwei Kerne voll). Für eine
|
||||
// Statusmeldung ist der Anteil an der Maschine die verständlichere Zahl --
|
||||
// „187 %" liest sich wie ein Fehler.
|
||||
if (eintrag.cpu != null && eintrag.kerne) teile.push(`${(eintrag.cpu / eintrag.kerne).toFixed(1)} % CPU`);
|
||||
return teile.length ? teile.join(' · ') : null;
|
||||
}
|
||||
@@ -325,3 +325,20 @@ export function repoIgnored(repo) {
|
||||
(r) => r.toLowerCase() === repo.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* d4rk_gameserver-Panel — Adresse und Lesezeichen.
|
||||
*
|
||||
* BEIDES NUR HIER UND NICHT IN DER .env: der Betreiber soll es im Webinterface
|
||||
* eintragen können, so wie alles andere auch. Das Zeichen darf ausschließlich
|
||||
* lesen; selbst wenn es abhandenkommt, kann damit niemand einen Server stoppen.
|
||||
*
|
||||
* Leer = die Zusatzauskunft ist aus, und der Monitor verhält sich wie vorher.
|
||||
*/
|
||||
export function panelUrl() {
|
||||
return (getSetting('panel_url') || '').replace(/\/$/, '');
|
||||
}
|
||||
|
||||
export function panelZeichen() {
|
||||
return getSetting('panel_zeichen') || '';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user