'use strict'; /* * crashreports.js – Absturzberichte von Minecraft auswerten * ---------------------------------------------------------- * Minecraft legt bei einem Absturz eine Datei unter * /crash-reports/crash--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 -.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 };