Upload via GUI (39 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-02 17:01:18 +02:00
parent a803f297ea
commit eff6403dd5
26 changed files with 3536 additions and 134 deletions

180
src/crashreports.js Normal file
View File

@@ -0,0 +1,180 @@
'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, verlauf: [] }
*/
function auswerten(text) {
const zeilen = String(text || '').split(/\r?\n/);
const befund = { beschreibung: '', fehler: '', verdaechtigeMods: '', 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();
}
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 };