Upload via GUI (39 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-03 21:00:24 +02:00
parent 129ab2d939
commit 54d248133c
7 changed files with 218 additions and 25 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "aeromc", "name": "aeromc",
"version": "1.0.4-beta", "version": "1.0.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "aeromc", "name": "aeromc",
"version": "1.0.4-beta", "version": "1.0.5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"archiver": "^7.0.1", "archiver": "^7.0.1",

View File

@@ -1,7 +1,7 @@
{ {
"name": "aeromc", "name": "aeromc",
"productName": "AeroMC", "productName": "AeroMC",
"version": "1.0.4", "version": "1.0.5",
"description": "AeroMC Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)", "description": "AeroMC Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
"homepage": "https://m-viper.de", "homepage": "https://m-viper.de",
"main": "src/main.js", "main": "src/main.js",

View File

@@ -47,11 +47,18 @@ function neuesterBericht(gameDir, seit) {
/* /*
* Zieht die aussagekräftigen Zeilen aus einem Bericht. * Zieht die aussagekräftigen Zeilen aus einem Bericht.
* -> { beschreibung, fehler, verdaechtigeMods, verlauf: [] } * -> { beschreibung, fehler, verdaechtigeMods, diagnose, empfehlung, verlauf: [] }
*/ */
function auswerten(text) { function auswerten(text) {
const zeilen = String(text || '').split(/\r?\n/); const zeilen = String(text || '').split(/\r?\n/);
const befund = { beschreibung: '', fehler: '', verdaechtigeMods: '', verlauf: [] }; const befund = {
beschreibung: '',
fehler: '',
verdaechtigeMods: '',
diagnose: '',
empfehlung: '',
verlauf: [],
};
for (let i = 0; i < zeilen.length; i++) { for (let i = 0; i < zeilen.length; i++) {
const z = zeilen[i]; const z = zeilen[i];
@@ -86,6 +93,22 @@ function auswerten(text) {
const treffer = zeilen.find((z) => /^[\w.$]+(Exception|Error)(:|\s|$)/.test(z.trim())); const treffer = zeilen.find((z) => /^[\w.$]+(Exception|Error)(:|\s|$)/.test(z.trim()));
if (treffer) befund.fehler = treffer.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; return befund;
} }

View File

@@ -21,6 +21,7 @@ const { spawn, spawnSync } = require('child_process');
const services = require('./services'); const services = require('./services');
const loaders = require('./loaders'); const loaders = require('./loaders');
const forge = require('./forge'); const forge = require('./forge');
const processwatchdog = require('./processwatchdog');
const UA = 'AeroMC-Launcher/0.2'; const UA = 'AeroMC-Launcher/0.2';
const RESOURCES = 'https://resources.download.minecraft.net'; const RESOURCES = 'https://resources.download.minecraft.net';
@@ -253,20 +254,28 @@ async function prepareAssets(vj, sharedDir, onProgress) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function pickJava(vj, preferredPath) { function pickJava(vj, preferredPath) {
const required = (vj.javaVersion && vj.javaVersion.majorVersion) || 8;
let preferred = null;
if (preferredPath && preferredPath.trim()) { if (preferredPath && preferredPath.trim()) {
const info = services.probeJava(preferredPath); const info = services.probeJava(preferredPath);
if (info) return { path: preferredPath, major: info.major, forced: true }; if (info) {
preferred = { path: preferredPath, major: info.major, forced: true };
if (info.major === required) return preferred;
}
} }
const required = (vj.javaVersion && vj.javaVersion.majorVersion) || 8;
const jvms = services.scanJava(); const jvms = services.scanJava();
// exakt passende oder nächsthöhere 64-bit-Version // Mod-Loader und Mods unterstützen neuere Java-Versionen nicht automatisch.
// Deshalb ausschließlich die von Mojang angegebene Major-Version verwenden.
const x64 = jvms.filter((j) => j.arch === 'x64'); const x64 = jvms.filter((j) => j.arch === 'x64');
const exact = x64.find((j) => j.major === required); const exact = x64.find((j) => j.major === required);
if (exact) return { path: exact.path, major: exact.major }; if (exact) return { path: exact.path, major: exact.major };
const higher = x64.filter((j) => j.major >= required).sort((a, b) => a.major - b.major)[0]; const fallback = preferred || x64[0] || jvms[0];
if (higher) return { path: higher.path, major: higher.major }; return fallback ? {
const any = jvms[0]; path: fallback.path,
return any ? { path: any.path, major: any.major, mismatch: required } : null; major: fallback.major,
forced: !!fallback.forced,
mismatch: required,
} : null;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -387,6 +396,14 @@ async function prepareAndLaunch(opts, onProgress) {
} }
} }
if (!java) return { ok: false, message: 'Keine Java-Installation gefunden. Bitte in den Einstellungen Java festlegen.' }; if (!java) return { ok: false, message: 'Keine Java-Installation gefunden. Bitte in den Einstellungen Java festlegen.' };
if (java.mismatch) {
return {
ok: false,
requiredJava: java.mismatch,
message: `Minecraft ${vj.id} benötigt Java ${java.mismatch}, gefunden wurde nur Java ${java.major || 'unbekannt'}. `
+ 'Bitte Auto-Java aktivieren oder eine passende Java-Installation auswählen.',
};
}
// Mod-Loader // Mod-Loader
let effectiveVj = vj; let effectiveVj = vj;
@@ -503,14 +520,33 @@ async function prepareAndLaunch(opts, onProgress) {
spawnArgs = [...w.slice(1), java.path, ...args]; spawnArgs = [...w.slice(1), java.path, ...args];
} }
const child = spawn(cmd, spawnArgs, { cwd: opts.gameDir, detached: false }); const child = spawn(cmd, spawnArgs, { cwd: opts.gameDir, detached: false });
child.stdout.on('data', (d) => emit('log', { line: d.toString() })); const shutdownWatchdog = opts.wrapperCommand && opts.wrapperCommand.trim()
child.stderr.on('data', (d) => emit('log', { line: d.toString() })); ? null
: processwatchdog.createProcessWatchdog({
isRunning: () => child.exitCode === null && child.signalCode === null,
kill: () => child.kill(),
onForce: (graceMs) => emit('log', {
line: `[Launcher] Regulärer Client-Shutdown war nach ${graceMs / 1000} Sekunden noch nicht beendet; `
+ 'verbliebenen Java-Prozess beendet.\n',
}),
onError: () => emit('log', {
line: '[Launcher] Der nach dem regulären Client-Shutdown verbliebene Java-Prozess konnte nicht beendet werden.\n',
}),
});
child.stdout.on('data', (d) => {
emit('log', { line: d.toString() });
if (shutdownWatchdog) shutdownWatchdog.feed(d, 'stdout');
});
child.stderr.on('data', (d) => {
emit('log', { line: d.toString() });
if (shutdownWatchdog) shutdownWatchdog.feed(d, 'stderr');
});
// Ende genau einmal melden 'exit' und 'error' können beide kommen // Ende genau einmal melden 'exit' und 'error' können beide kommen
let beendetGemeldet = false; let beendetGemeldet = false;
const meldeEnde = (code) => { const meldeEnde = (code, cleanShutdown) => {
if (beendetGemeldet) return; if (beendetGemeldet) return;
beendetGemeldet = true; beendetGemeldet = true;
emit('exit', { code }); emit('exit', { code, cleanShutdown: !!cleanShutdown });
// Post-Exit-Befehl // Post-Exit-Befehl
if (opts.postExitCommand && opts.postExitCommand.trim()) { if (opts.postExitCommand && opts.postExitCommand.trim()) {
emit('log', { line: '[Post-Exit] ' + opts.postExitCommand + '\n' }); emit('log', { line: '[Post-Exit] ' + opts.postExitCommand + '\n' });
@@ -521,12 +557,19 @@ async function prepareAndLaunch(opts, onProgress) {
} catch { /* egal */ } } catch { /* egal */ }
} }
}; };
child.on('exit', (code) => meldeEnde(code)); child.on('exit', (code) => {
// vor end() lesen: sawCleanShutdown wird zwar nicht von end()/clear()
// zurückgesetzt, aber so bleibt die Reihenfolge unabhängig von internen Details
const cleanShutdown = !!(shutdownWatchdog && shutdownWatchdog.sawCleanShutdown);
if (shutdownWatchdog) shutdownWatchdog.end();
meldeEnde(shutdownWatchdog && shutdownWatchdog.forced ? 0 : code, cleanShutdown);
});
// ohne diesen Listener würde ein Fehlstart (z. B. falscher Java-Pfad) als // ohne diesen Listener würde ein Fehlstart (z. B. falscher Java-Pfad) als
// unbehandeltes 'error'-Ereignis den Hauptprozess beenden // unbehandeltes 'error'-Ereignis den Hauptprozess beenden
child.on('error', (err) => { child.on('error', (err) => {
if (shutdownWatchdog) shutdownWatchdog.end();
emit('log', { line: '[Fehler] Minecraft konnte nicht gestartet werden: ' + err.message + '\n' }); emit('log', { line: '[Fehler] Minecraft konnte nicht gestartet werden: ' + err.message + '\n' });
meldeEnde(-1); meldeEnde(-1, false);
}); });
return { ok: true, pid: child.pid, java }; return { ok: true, pid: child.pid, java };

View File

@@ -762,7 +762,11 @@ async function importMrpack(buf, zip) {
const herkunft = await modpackHerkunft(buf, index); const herkunft = await modpackHerkunft(buf, index);
store.updateInstance(created.id, { modpack: Object.assign(herkunft, { dateien: eingespielt.dateien }) }); store.updateInstance(created.id, { modpack: Object.assign(herkunft, { dateien: eingespielt.dateien }) });
return { ok: true, instance: store.getInstance(created.id), fileCount: eingespielt.dateien.length }; return {
ok: true,
instance: store.getInstance(created.id),
fileCount: eingespielt.dateien.length,
};
} }
/* /*
@@ -921,7 +925,9 @@ ipcMain.handle('modpack:update', async (_e, id, versionId) => {
if (SPIELERDATEN.some((r) => r.test(alt))) continue; if (SPIELERDATEN.some((r) => r.test(alt))) continue;
const p = path.join(gameDir, String(alt).replace(/\//g, path.sep)); const p = path.join(gameDir, String(alt).replace(/\//g, path.sep));
if (!p.startsWith(gameDir + path.sep)) continue; if (!p.startsWith(gameDir + path.sep)) continue;
try { if (fs.existsSync(p)) { fs.rmSync(p, { force: true }); entfernt.push(alt); } } catch { /* egal */ } try {
if (fs.existsSync(p)) { fs.rmSync(p, { force: true }); entfernt.push(alt); }
} catch { /* egal */ }
} }
// Minecraft-Version und Loader können sich mit der neuen Fassung ändern // Minecraft-Version und Loader können sich mit der neuen Fassung ändern
@@ -1514,7 +1520,10 @@ ipcMain.handle('mmc:import', (_e, entries, copyData) => {
const done = []; const done = [];
const failed = []; const failed = [];
for (const entry of entries || []) { for (const entry of entries || []) {
try { done.push(mmcimport.importOne(store, entry, copyData !== false)); } try {
const imported = mmcimport.importOne(store, entry, copyData !== false);
done.push(imported);
}
catch (err) { failed.push({ name: entry.name, message: err.message }); } catch (err) { failed.push({ name: entry.name, message: err.message }); }
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('mmc:progress', { current: done.length + failed.length, total: entries.length }); mainWindow.webContents.send('mmc:progress', { current: done.length + failed.length, total: entries.length });
@@ -1540,7 +1549,10 @@ ipcMain.handle('cf:import', (_e, entries, copyData) => {
const done = []; const done = [];
const failed = []; const failed = [];
for (const entry of entries || []) { for (const entry of entries || []) {
try { done.push(cfimport.importOne(store, entry, copyData !== false)); } try {
const imported = cfimport.importOne(store, entry, copyData !== false);
done.push(imported);
}
catch (err) { failed.push({ name: entry.name, message: err.message }); } catch (err) { failed.push({ name: entry.name, message: err.message }); }
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('cf:progress', { current: done.length + failed.length, total: entries.length }); mainWindow.webContents.send('cf:progress', { current: done.length + failed.length, total: entries.length });
@@ -1566,7 +1578,10 @@ ipcMain.handle('mc:import', (_e, entries, copyData) => {
const done = []; const done = [];
const failed = []; const failed = [];
for (const entry of entries || []) { for (const entry of entries || []) {
try { done.push(mcimport.importOne(store, entry, copyData !== false)); } try {
const imported = mcimport.importOne(store, entry, copyData !== false);
done.push(imported);
}
catch (err) { failed.push({ name: entry.name, message: err.message }); } catch (err) { failed.push({ name: entry.name, message: err.message }); }
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('mc:progress', { current: done.length + failed.length, total: entries.length }); mainWindow.webContents.send('mc:progress', { current: done.length + failed.length, total: entries.length });

101
src/processwatchdog.js Normal file
View File

@@ -0,0 +1,101 @@
'use strict';
const DEFAULT_GRACE_MS = 15000;
const CLEAN_SHUTDOWN = /\[(?:Render thread|Client thread)\/INFO\](?:\s+\([^)]*\))?:\s*Stopping!\s*$/;
const FATAL_OUTPUT = [
/---- Minecraft Crash Report ----/i,
/\b(?:Encountered an unexpected exception|Unreported exception thrown|Reported exception thrown)\b/i,
/\b(?:This crash report has been saved to|Saving crash report to)\b/i,
/\bA fatal error has been detected by the Java Runtime Environment\b/i,
/Exception in thread "(?:Render thread|Client thread|main)"/i,
/\[(?:Render thread|Client thread|main)\/FATAL\]/i,
];
function createProcessWatchdog(options) {
const graceMs = options.graceMs || DEFAULT_GRACE_MS;
const schedule = options.setTimer || setTimeout;
const cancel = options.clearTimer || clearTimeout;
const buffers = { stdout: '', stderr: '' };
let timer = null;
let armed = false;
let crashed = false;
let ended = false;
let forced = false;
// bleibt true, sobald einmal ein regulärer Shutdown ("Stopping!") gesehen
// wurde im Gegensatz zu `armed` wird das NICHT durch markCrash()
// zurückgesetzt, damit ein später vom internen Watchdog erzwungener Halt
// weiterhin als "war ein regulärer Shutdown" erkennbar bleibt
let sawCleanShutdown = false;
function clear() {
if (timer !== null) cancel(timer);
timer = null;
armed = false;
}
function markCrash() {
crashed = true;
clear();
}
function scheduleKill() {
if (timer !== null) cancel(timer);
timer = schedule(() => {
timer = null;
if (ended || crashed || !armed || !options.isRunning()) return;
try {
forced = options.kill() === true;
} catch {
forced = false;
}
if (forced && options.onForce) options.onForce(graceMs);
else if (!forced && options.isRunning() && options.onError) options.onError();
}, graceMs);
if (timer && typeof timer.unref === 'function') timer.unref();
}
function inspectLine(line) {
const isCleanShutdown = CLEAN_SHUTDOWN.test(line);
if (isCleanShutdown) sawCleanShutdown = true;
if (FATAL_OUTPUT.some((pattern) => pattern.test(line))) {
markCrash();
return;
}
if (ended || crashed || armed || !isCleanShutdown) return;
armed = true;
scheduleKill();
}
function feed(chunk, stream) {
if (ended) return;
const key = stream === 'stderr' ? 'stderr' : 'stdout';
const text = buffers[key] + String(chunk || '');
const lines = text.split(/\r?\n/);
buffers[key] = lines.pop() || '';
for (const line of lines) inspectLine(line);
if (armed && !crashed && !ended && lines.length) scheduleKill();
}
function end() {
ended = true;
clear();
}
return {
feed,
end,
get armed() { return armed; },
get crashed() { return crashed; },
get forced() { return forced; },
get sawCleanShutdown() { return sawCleanShutdown; },
};
}
module.exports = {
DEFAULT_GRACE_MS,
CLEAN_SHUTDOWN,
FATAL_OUTPUT,
createProcessWatchdog,
};

View File

@@ -2227,7 +2227,8 @@ async function zeigeAbsturzbericht(instanzId) {
const zeile = (titel, wert) => wert const zeile = (titel, wert) => wert
? `<div class="crash-zeile"><div class="crash-titel">${titel}</div><div class="crash-wert">${escapeHtml(wert)}</div></div>` ? `<div class="crash-zeile"><div class="crash-titel">${titel}</div><div class="crash-wert">${escapeHtml(wert)}</div></div>`
: ''; : '';
let html = zeile('Beschreibung', res.beschreibung) + zeile('Fehler', res.fehler) let html = zeile('Diagnose', res.diagnose) + zeile('Empfohlene Lösung', res.empfehlung)
+ zeile('Beschreibung', res.beschreibung) + zeile('Fehler', res.fehler)
+ zeile('Verdächtige Mods', res.verdaechtigeMods); + zeile('Verdächtige Mods', res.verdaechtigeMods);
if (res.verlauf && res.verlauf.length) { if (res.verlauf && res.verlauf.length) {
html += `<div class="crash-zeile"><div class="crash-titel">Aufrufverlauf</div>` html += `<div class="crash-zeile"><div class="crash-titel">Aufrufverlauf</div>`
@@ -2246,7 +2247,17 @@ function onLaunchEvt(evt) {
status('Minecraft beendet (Code ' + evt.code + ')'); status('Minecraft beendet (Code ' + evt.code + ')');
el('status-right').textContent = ''; el('status-right').textContent = '';
appendConsole('\n[Prozess beendet, Code ' + evt.code + ']\n'); appendConsole('\n[Prozess beendet, Code ' + evt.code + ']\n');
const crashed = evt.code !== 0 && evt.code !== null; // War vorher "Stopping!" im Log (regulärer Client-Shutdown), zählt ein
// nachträglicher harter Exit-Code nicht als Absturz während des Spiels
// z. B. wenn Minecrafts interner Shutdown-Watchdog wegen hängender
// Mod-Threads (z. B. JourneyMap WebMap) die JVM nach Ablauf einer
// Wartezeit zwangsweise beendet. Der Bericht bleibt trotzdem gespeichert.
const crashed = evt.code !== 0 && evt.code !== null && !evt.cleanShutdown;
if (evt.cleanShutdown && evt.code !== 0 && evt.code !== null) {
appendConsole('[Hinweis] Der Client wurde regulär beendet (Stopping!), aber danach vom '
+ 'internen Shutdown-Watchdog hart terminiert (z. B. durch einen Mod, dessen Threads '
+ 'nicht rechtzeitig beenden etwa JourneyMap WebMap). Kein Absturz während des Spiels.\n');
}
// Absturzbericht suchen, bevor über die Konsole entschieden wird // Absturzbericht suchen, bevor über die Konsole entschieden wird
// ein Bericht sagt mehr als der Java-Auswurf // ein Bericht sagt mehr als der Java-Auswurf
if (crashed) zeigeAbsturzbericht(evt.id); if (crashed) zeigeAbsturzbericht(evt.id);