204 lines
7.1 KiB
JavaScript
204 lines
7.1 KiB
JavaScript
'use strict';
|
||
|
||
/*
|
||
* crashreports.js – Absturzberichte von Minecraft auswerten
|
||
* ----------------------------------------------------------
|
||
* Minecraft legt bei einem Absturz eine Datei unter
|
||
* <spielordner>/crash-reports/crash-<datum>-client.txt an. Die enthält
|
||
* hunderte Zeilen Systemangaben, aber nur wenige, die weiterhelfen.
|
||
*
|
||
* Dieses Modul sucht den neuesten Bericht und zieht daraus heraus:
|
||
* Beschreibung, Fehlerart, verdächtige Mods und die ersten Stellen im
|
||
* Aufrufverlauf.
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const zlib = require('zlib');
|
||
|
||
// größere Berichte gibt es kaum; die Grenze schützt vor Ausreißern
|
||
const MAX_BYTES = 2 * 1024 * 1024;
|
||
|
||
function berichtsOrdner(gameDir) {
|
||
return path.join(gameDir, 'crash-reports');
|
||
}
|
||
|
||
/*
|
||
* Neuester Bericht im Ordner. seit: nur Berichte ab diesem Zeitpunkt
|
||
* (Millisekunden) – sonst zeigt man nach einem sauberen Start den Absturz
|
||
* von vorgestern an.
|
||
*/
|
||
function neuesterBericht(gameDir, seit) {
|
||
const dir = berichtsOrdner(gameDir);
|
||
let eintraege = [];
|
||
try { eintraege = fs.readdirSync(dir, { withFileTypes: true }); } catch { return null; }
|
||
|
||
let bester = null;
|
||
for (const e of eintraege) {
|
||
if (!e.isFile() || !/^crash-.*\.txt$/i.test(e.name)) continue;
|
||
const voll = path.join(dir, e.name);
|
||
let st;
|
||
try { st = fs.statSync(voll); } catch { continue; }
|
||
if (seit && st.mtimeMs < seit) continue;
|
||
if (!bester || st.mtimeMs > bester.mtimeMs) bester = { pfad: voll, name: e.name, mtimeMs: st.mtimeMs, groesse: st.size };
|
||
}
|
||
return bester;
|
||
}
|
||
|
||
/*
|
||
* Zieht die aussagekräftigen Zeilen aus einem Bericht.
|
||
* -> { beschreibung, fehler, verdaechtigeMods, diagnose, empfehlung, verlauf: [] }
|
||
*/
|
||
function auswerten(text) {
|
||
const zeilen = String(text || '').split(/\r?\n/);
|
||
const befund = {
|
||
beschreibung: '',
|
||
fehler: '',
|
||
verdaechtigeMods: '',
|
||
diagnose: '',
|
||
empfehlung: '',
|
||
verlauf: [],
|
||
};
|
||
|
||
for (let i = 0; i < zeilen.length; i++) {
|
||
const z = zeilen[i];
|
||
const roh = z.trim();
|
||
|
||
if (!befund.beschreibung && /^Description:/i.test(roh)) {
|
||
befund.beschreibung = roh.replace(/^Description:\s*/i, '').trim();
|
||
// Die Fehlerzeile steht direkt darunter, nach einer Leerzeile.
|
||
for (let j = i + 1; j < Math.min(i + 6, zeilen.length); j++) {
|
||
const k = zeilen[j].trim();
|
||
if (!k) continue;
|
||
befund.fehler = k;
|
||
break;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Forge/NeoForge nennen Verdächtige ausdrücklich
|
||
if (!befund.verdaechtigeMods && /^Suspected Mods?:/i.test(roh)) {
|
||
befund.verdaechtigeMods = roh.replace(/^Suspected Mods?:\s*/i, '').trim();
|
||
continue;
|
||
}
|
||
|
||
// die obersten Stellen im Aufrufverlauf – dort steht meist der Verursacher
|
||
if (befund.verlauf.length < 8 && /^at\s+\S/.test(roh)) {
|
||
befund.verlauf.push(roh);
|
||
}
|
||
}
|
||
|
||
// Manche Berichte nennen keine Description, dafür eine Ausnahme weiter oben
|
||
if (!befund.fehler) {
|
||
const treffer = zeilen.find((z) => /^[\w.$]+(Exception|Error)(:|\s|$)/.test(z.trim()));
|
||
if (treffer) befund.fehler = treffer.trim();
|
||
}
|
||
|
||
const volltext = zeilen.join('\n');
|
||
const shutdownWatchdog = /Description:\s*Client shutdown from post-main/i.test(volltext)
|
||
&& /ClientShutdownWatchdog/i.test(volltext);
|
||
const webMapJetty = /journeymap_webmap:\s*JourneyMap WebMap\b/i.test(volltext)
|
||
&& /JettyServerThreadPool/i.test(volltext)
|
||
&& /ServerConnector.*\{0\.0\.0\.0:\d+\}/i.test(volltext);
|
||
if (shutdownWatchdog && webMapJetty) {
|
||
const version = volltext.match(/journeymap_webmap:\s*JourneyMap WebMap\s+([^\s\r\n]+)/i);
|
||
const name = `JourneyMap WebMap${version ? ` ${version[1]}` : ''}`;
|
||
befund.verdaechtigeMods = name;
|
||
befund.diagnose = `${name} hat seinen eingebetteten Jetty-Webserver beim Beenden nicht gestoppt. `
|
||
+ 'Dessen nicht beendete Server-Threads haben den Minecraft-Shutdown-Watchdog ausgelöst.';
|
||
befund.empfehlung = 'AeroMC beendet einen nach dem regulären Client-Shutdown verbliebenen Java-Prozess nun '
|
||
+ 'nach einer kurzen Schonfrist automatisch. JourneyMap WebMap kann installiert und aktiviert bleiben.';
|
||
}
|
||
return befund;
|
||
}
|
||
|
||
/*
|
||
* Sucht und liest den neuesten Bericht.
|
||
* -> { ok: false, grund } oder { ok: true, datei, zeitpunkt, ...auswertung, text }
|
||
*/
|
||
function lies(gameDir, seit) {
|
||
const bericht = neuesterBericht(gameDir, seit);
|
||
if (!bericht) return { ok: false, grund: 'kein-bericht' };
|
||
if (bericht.groesse > MAX_BYTES) return { ok: false, grund: 'zu-gross', datei: bericht.name };
|
||
|
||
let text = '';
|
||
try { text = fs.readFileSync(bericht.pfad, 'utf8'); }
|
||
catch (err) { return { ok: false, grund: 'nicht-lesbar', message: err.message }; }
|
||
|
||
return Object.assign({
|
||
ok: true,
|
||
datei: bericht.name,
|
||
pfad: bericht.pfad,
|
||
zeitpunkt: new Date(bericht.mtimeMs).toISOString(),
|
||
text,
|
||
}, auswerten(text));
|
||
}
|
||
|
||
/*
|
||
* Logdateien der Instanz auflisten. Minecraft schreibt latest.log und legt
|
||
* ältere Läufe gepackt als <datum>-<nr>.log.gz daneben.
|
||
* -> [{ name, groesse, geaendert, gepackt }] – neueste zuerst
|
||
*/
|
||
function listeLogs(gameDir) {
|
||
const dir = path.join(gameDir, 'logs');
|
||
let eintraege = [];
|
||
try { eintraege = fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; }
|
||
|
||
const logs = [];
|
||
for (const e of eintraege) {
|
||
if (!e.isFile()) continue;
|
||
const gepackt = /\.log\.gz$/i.test(e.name);
|
||
if (!gepackt && !/\.log$/i.test(e.name)) continue;
|
||
try {
|
||
const st = fs.statSync(path.join(dir, e.name));
|
||
logs.push({ name: e.name, groesse: st.size, geaendert: st.mtimeMs, gepackt });
|
||
} catch { /* überspringen */ }
|
||
}
|
||
// latest.log immer zuoberst, danach nach Datum
|
||
logs.sort((a, b) => {
|
||
if (a.name.toLowerCase() === 'latest.log') return -1;
|
||
if (b.name.toLowerCase() === 'latest.log') return 1;
|
||
return b.geaendert - a.geaendert;
|
||
});
|
||
return logs;
|
||
}
|
||
|
||
/*
|
||
* Inhalt einer Logdatei. Gepackte werden entpackt.
|
||
* maxZeilen begrenzt die Rückgabe – ein Log kann zehntausende Zeilen haben,
|
||
* und die Oberfläche soll dabei nicht einfrieren.
|
||
*/
|
||
function liesLog(gameDir, name, maxZeilen = 5000) {
|
||
const dir = path.join(gameDir, 'logs');
|
||
const voll = path.join(dir, path.basename(String(name || '')));
|
||
if (path.dirname(voll) !== dir) return { ok: false, grund: 'bad-name' };
|
||
if (!fs.existsSync(voll)) return { ok: false, grund: 'nicht-gefunden' };
|
||
|
||
let text = '';
|
||
try {
|
||
if (/\.gz$/i.test(voll)) text = zlib.gunzipSync(fs.readFileSync(voll)).toString('utf8');
|
||
else text = fs.readFileSync(voll, 'utf8');
|
||
} catch (err) {
|
||
return { ok: false, grund: 'nicht-lesbar', message: err.message };
|
||
}
|
||
|
||
const zeilen = text.split(/\r?\n/);
|
||
const gekuerzt = zeilen.length > maxZeilen;
|
||
// bei langen Logs das Ende zeigen – dort steht, was zuletzt passierte
|
||
const sichtbar = gekuerzt ? zeilen.slice(zeilen.length - maxZeilen) : zeilen;
|
||
|
||
return {
|
||
ok: true,
|
||
name: path.basename(voll),
|
||
zeilenGesamt: zeilen.length,
|
||
gekuerzt,
|
||
text: sichtbar.join('\n'),
|
||
};
|
||
}
|
||
|
||
function logOrdner(gameDir) {
|
||
return path.join(gameDir, 'logs');
|
||
}
|
||
|
||
module.exports = { lies, auswerten, neuesterBericht, berichtsOrdner, listeLogs, liesLog, logOrdner };
|