Files
AeroMc-Launcher/src/crashreports.js
2026-08-03 21:00:24 +02:00

204 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 };