3 Commits
1.0.4 ... 1.0.6

Author SHA1 Message Date
ec4992c5c7 README.md aktualisiert 2026-08-05 07:03:48 +00:00
3e28b50f1b README.md aktualisiert 2026-08-03 19:10:09 +00:00
Git Manager GUI
54d248133c Upload via GUI (39 Dateien) 2026-08-03 21:00:24 +02:00
8 changed files with 292 additions and 54 deletions

103
README.md
View File

@@ -6,7 +6,7 @@
Ein Minecraft-Launcher für Windows mit getrennten Instanzen, Mod-Unterstützung und Microsoft-Anmeldung. Ein Minecraft-Launcher für Windows mit getrennten Instanzen, Mod-Unterstützung und Microsoft-Anmeldung.
Aktueller Stand: Version 1.0.4 Aktueller Stand: Version 1.0.6
</div> </div>
@@ -21,23 +21,30 @@ Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen
- Beliebig viele getrennte Minecraft-Instanzen - Beliebig viele getrennte Minecraft-Instanzen
- Unterstützung für Vanilla, Fabric, Quilt, Forge und NeoForge - Unterstützung für Vanilla, Fabric, Quilt, Forge und NeoForge
- Versionsliste mit Filter: Releases, Snapshots oder alle - Versionsliste mit Filter: Releases, Snapshots oder alle
- Modrinth-Integration für **Mods, Ressourcenpakete und Shader** (inkl. Pflicht-Abhängigkeiten und Update-Prüfung) - Modrinth- **und CurseForge**-Integration für **Mods, Ressourcenpakete und Shader** (Suche wahlweise über beide Quellen, Installation inkl. rekursiver Pflicht-Abhängigkeiten, Update-Prüfung samt Sammel-Update)
- Modpacks direkt von Modrinth **oder von der Technic-Plattform** durchsuchen und mit einem Klick als neue Instanz installieren ohne vorherigen manuellen Download
- Microsoft-Login mit mehreren Konten - Microsoft-Login mit mehreren Konten
- Gemeinsamer Import für AeroMC-ZIP, Modrinth-Modpacks (`.mrpack`), Minecraft Launcher, CurseForge sowie MultiMC/Prism - Gemeinsamer Import für AeroMC-ZIP, Modrinth-Modpacks (`.mrpack`), Minecraft Launcher, CurseForge, MultiMC/Prism sowie Technic
- Reimport überschreibt vorhandene Instanzen mit gleichem Namen - Reimport überschreibt vorhandene Instanzen mit gleichem Namen
- Export, Kopieren, Gruppieren, Löschen und Desktop-Verknüpfung pro Instanz - Export, Kopieren, Gruppieren, Löschen und Desktop-Verknüpfung pro Instanz
- Mehrfachauswahl mit Strg-Klick für Sammellöschen - Mehrfachauswahl mit Strg-Klick für Sammellöschen, Tastenkürzel für die wichtigsten Aktionen (Strg+F, Strg+N, Entf)
- Globales Backup inkl. automatischer Zeitplan-Sicherungen - Mods/Modpacks/Instanzen auch per Drag & Drop aufs Fenster ziehen
- Welten, Ressourcenpakete, Screenshots, Serverliste und Logdateien verwalten - Mods zusätzlich als eigene Datei hinzufügen, wenn sie nicht über Modrinth/CurseForge zu finden sind
- Warnt beim Entfernen eines Mods, falls ein anderer installierter Mod ihn noch als Pflicht-Abhängigkeit braucht
- Globales Backup inkl. automatischer Zeitplan-Sicherungen, dazu Schnellsicherung einzelner Welten
- Welten, Ressourcenpakete, Datenpakete, Screenshots, Serverliste und Logdateien verwalten
- Serverliste mit Live-Status (Spieleranzahl, MOTD, Version) und Übernahme einzelner Einträge in andere Instanzen
- Online- und Offline-Start - Online- und Offline-Start
- Eingebaute Konsole/Log-Ansicht - Eingebaute Konsole/Log-Ansicht inkl. Absturzberichte pro Instanz
- Skin-Hintergrund des aktiven Kontos (mehrere Posen, einstellbare Deckkraft) - Windows-Benachrichtigung, wenn ein großer Download oder ein Backup im Hintergrund fertig wird
- Eigene Icons (inkl. eigenes Bild) und Notizen pro Instanz - Eigenen Minecraft-Skin direkt im Launcher hochladen, mit Live-Vorschau anpassen (klassisch/schlank) und zurücksetzen
- Skin-Hintergrund des aktiven Kontos (mehrere Posen, einstellbare Deckkraft) wechselt bei mehreren angemeldeten Konten optional automatisch durch
- Eigene Icons pro Instanz vorgefertigte Symbole, direkt aus Minecraft extrahierte Block-/Item-Icons oder ein eigenes Bild plus Notizen
- Java pro Instanz oder global, inkl. automatischem Download fehlender Versionen (Eclipse Temurin) - Java pro Instanz oder global, inkl. automatischem Download fehlender Versionen (Eclipse Temurin)
- RAM-, JVM-, Pre-Launch- und Post-Exit-Einstellungen (global und pro Instanz) - RAM-, JVM-, Pre-Launch- und Post-Exit-Einstellungen (global und pro Instanz), inkl. Ein-Klick-Knopf für empfohlene GC-Flags
- Optionaler Wrapper-Befehl und HTTP-Proxy für Downloads - Optionaler Wrapper-Befehl und HTTP-Proxy für Downloads
- Bedrock-Bereich für vorhandene Bedrock-Installation unter Windows - Bedrock-Bereich für vorhandene Bedrock-Installation unter Windows, inkl. Installation zusätzlicher Add-ons
- Update-Funktion für neue Launcher-Versionen - Update-Suche läuft automatisch beim Start, dazu manuelle Prüfung in den Einstellungen
- Design Hell/Dunkel und Sortierung der Instanzen - Design Hell/Dunkel und Sortierung der Instanzen
## Installation ## Installation
@@ -71,18 +78,18 @@ Jede Instanz hat:
- Namen und optionale Gruppe - Namen und optionale Gruppe
- Minecraft-Version und optionalen Mod-Loader - Minecraft-Version und optionalen Mod-Loader
- optionales Icon (vorgegeben oder eigenes Bild) und Notizen - optionales Icon (vorgegeben, aus Minecraft extrahiert oder eigenes Bild) und Notizen
- eigene Mods, Ressourcenpakete, Shader, Welten, Screenshots und Serverliste - eigene Mods, Ressourcenpakete, Shader, Welten, Datenpakete, Screenshots und Serverliste
- eigene Java-, RAM-, JVM-, Pre-Launch- und Post-Exit-Einstellungen (leer = globale Vorgabe) - eigene Java-, RAM-, JVM-, Pre-Launch- und Post-Exit-Einstellungen (leer = globale Vorgabe), inkl. Knopf für empfohlene GC-Flags bei den JVM-Argumenten
Über die Seitenleiste einer ausgewählten Instanz erreichst du u. a.: Über die Seitenleiste einer ausgewählten Instanz erreichst du u. a.:
- Starten / Offline starten - Starten / Offline starten
- Instanz und Notizen bearbeiten - Instanz und Notizen bearbeiten
- Mods, Ressourcenpakete und Shader - Mods, Ressourcenpakete und Shader
- Welten und Bildschirmfotos - Welten (inkl. Schnellsicherung einzelner Welten), Datenpakete und Bildschirmfotos
- Server verwalten - Server verwalten (Live-Status, Übernahme eines Eintrags in andere Instanzen)
- Logdateien ansehen - Logdateien und Absturzberichte ansehen
- Minecraft-, Konfigurations- und Instanz-Ordner öffnen - Minecraft-, Konfigurations- und Instanz-Ordner öffnen
- Desktop-Verknüpfung erstellen - Desktop-Verknüpfung erstellen
- Exportieren, Kopieren, Gruppe ändern, Löschen - Exportieren, Kopieren, Gruppe ändern, Löschen
@@ -90,6 +97,17 @@ Jede Instanz hat:
In der Instanzliste: Suche, Sortierung und **Strg + Klick** für Mehrfachauswahl. In der Instanzliste: Suche, Sortierung und **Strg + Klick** für Mehrfachauswahl.
### Tastenkürzel
| Taste | Wirkung |
|---|---|
| Strg+F | Suche fokussieren |
| Strg+N | Neue Instanz anlegen |
| Entf | Markierte Instanz(en) löschen |
| Esc | Aktuellen Dialog schließen |
Greifen nicht, während ein Textfeld fokussiert ist oder ein Dialog mit eigener Suche offen ist.
## Import ## Import
Über **Importieren**: Über **Importieren**:
@@ -97,26 +115,43 @@ In der Instanzliste: Suche, Sortierung und **Strg + Klick** für Mehrfachauswahl
| Quelle | Typische Nutzung | | Quelle | Typische Nutzung |
|---|---| |---|---|
| ZIP-Archiv | AeroMC-Export (`instance.json`) sowie einzelne MultiMC/Prism-Instanzen (auch gezippt) | | ZIP-Archiv | AeroMC-Export (`instance.json`) sowie einzelne MultiMC/Prism-Instanzen (auch gezippt) |
| Modpack | Modrinth `.mrpack` | | Modpack | Modrinth-Katalog direkt durchsuchen und installieren, oder Datei importieren: `.mrpack` sowie CurseForge-ZIP (`manifest.json`) |
| Minecraft Launcher | Profile aus dem offiziellen Launcher | | Minecraft Launcher | Profile aus dem offiziellen Launcher |
| CurseForge | Instanzen aus dem CurseForge-Ordner | | CurseForge | Instanzen aus dem CurseForge-Ordner |
| MultiMC / Prism | Vorhandene MultiMC- oder Prism-Instanzen | | MultiMC / Prism | Vorhandene MultiMC- oder Prism-Instanzen |
| Technic | Technic-Katalog direkt durchsuchen und installieren, oder eine vorhandene Technic-Installation importieren (`%APPDATA%\.technic\modpacks`) |
Existiert bereits eine Instanz mit demselben Namen, wird sie ersetzt. Existiert bereits eine Instanz mit demselben Namen, wird sie ersetzt.
Beim offiziellen Minecraft Launcher werden nur benutzerdefinierte Profile angeboten (keine „Latest Release“/„Latest Snapshot“). Bei gemeinsamen Standard-Spielordnern übernimmt AeroMC spielrelevante Daten (Mods, Welten, Konfiguration), nicht den kompletten Cache. Beim offiziellen Minecraft Launcher werden nur benutzerdefinierte Profile angeboten (keine „Latest Release“/„Latest Snapshot“). Bei gemeinsamen Standard-Spielordnern übernimmt AeroMC spielrelevante Daten (Mods, Welten, Konfiguration), nicht den kompletten Cache.
Im Reiter **Modpack** desselben Dialogs lässt sich zusätzlich direkt im Modrinth-Katalog suchen und ein Modpack mit einem Klick installieren ganz ohne vorherigen manuellen Download der `.mrpack`-Datei. Im Reiter **Technic** gibt es dasselbe für den Technic-Katalog.
Alternativ: `.jar`-, `.mrpack`- oder `.zip`-Dateien direkt aufs Launcher-Fenster ziehen. AeroMC erkennt anhand des Inhalts, ob es sich um einen Mod (landet in der gerade offenen/ausgewählten Instanz), ein Modpack oder einen Instanz-Export handelt.
## Mods, Ressourcenpakete und Shader ## Mods, Ressourcenpakete und Shader
Im Inhalts-Dialog einer Instanz: Im Inhalts-Dialog einer Instanz:
- Katalog durchsuchen (Sortierung, Kategorien) - Katalog durchsuchen (Quelle Modrinth oder CurseForge, Sortierung, Kategorien)
- Installation inkl. Abhängigkeiten - Installation inkl. **automatischer** Installation aller Pflicht-Abhängigkeiten (rekursiv, auch quellübergreifend)
- Installierte Einträge aktivieren, deaktivieren, entfernen - Eigene Datei hinzufügen (`.jar`/`.zip`) für Mods, die nicht über Modrinth/CurseForge zu finden sind, oder per Drag & Drop aufs Fenster ziehen
- Installierte Einträge aktivieren, deaktivieren, entfernen beim Entfernen warnt AeroMC, falls ein anderer installierter Mod das noch als Pflicht-Abhängigkeit braucht
- Nach Updates suchen und alle aktualisieren - Nach Updates suchen und alle aktualisieren
- Ordner der jeweiligen Inhaltsart öffnen - Ordner der jeweiligen Inhaltsart öffnen
Ressourcenpakete und Shader funktionieren auch in Vanilla-Instanzen. Mods benötigen einen Mod-Loader. Ressourcenpakete und Shader funktionieren auch in Vanilla-Instanzen. Mods benötigen einen Mod-Loader. Für CurseForge-Inhalte wird ein eigener API-Key oder eine Weiterleitungs-Adresse benötigt (siehe **Einstellungen → Netzwerk**).
## Skin ändern
Über **Konten → Skin ändern…** lässt sich der Skin des aktiven Microsoft-Kontos direkt im Launcher ändern:
- PNG-Datei auswählen, sofortige Prüfung auf gültige Maße
- Live-Vorschau am 3D-Modell vor dem Anwenden
- Modell wählen: klassisch oder schlank (slim)
- Auf einen früheren Standard-Skin zurücksetzen
Die Änderung wird direkt bei Mojang für das angemeldete Konto hinterlegt und gilt somit auch außerhalb von AeroMC.
## Backup und Wiederherstellung ## Backup und Wiederherstellung
@@ -138,10 +173,11 @@ Speicherort: `Dokumente\AeroMC Launcher\Backups`.
- Instanzen sortieren (nach letztem Start / nach Name) - Instanzen sortieren (nach letztem Start / nach Name)
- Instanz-Verzeichnis - Instanz-Verzeichnis
- Bedrock-Bereich anzeigen - Bedrock-Bereich anzeigen
- Windows-Benachrichtigung bei fertigem Hintergrund-Download/-Backup (nur wenn das Fenster nicht im Vordergrund ist)
### Darstellung ### Darstellung
- Block-Symbole räumlich (3D) - Block-Symbole räumlich (3D)
- Skin-Hintergrund: Quelle, Pose, Deckkraft - Skin-Hintergrund: Quelle, Pose, Deckkraft, optional automatischer Wechsel zwischen mehreren angemeldeten Konten (mit einstellbarem Zeitabstand)
### Spiel ### Spiel
- Standard-RAM (min./max.) - Standard-RAM (min./max.)
@@ -167,11 +203,12 @@ Ohne eigenen Key und ohne Weiterleitung funktionieren Modrinth-Inhalte weiterhin
- Manuell, Zeitplan und Wiederherstellung - Manuell, Zeitplan und Wiederherstellung
### Info ### Info
- Version und manuelle Update-Prüfung - Version, Autor und Links
- Update-Suche läuft automatisch kurz nach dem Start (Status erscheint grün, wenn alles aktuell ist) sowie jederzeit manuell per Knopf
## Bedrock ## Bedrock
Der Bedrock-Bereich startet die vorhandene Windows-Store-Version und erleichtert den Zugriff auf Welten, Pakete, Add-ons und Ordner. AeroMC installiert Bedrock nicht selbst. Fehlt die Installation, wird der Bereich automatisch ausgeblendet. Der Bedrock-Bereich startet die vorhandene Windows-Store-Version und erleichtert den Zugriff auf Welten, Pakete, Add-ons und Ordner. Zusätzliche Add-on-Dateien lassen sich direkt über AeroMC installieren. AeroMC installiert Bedrock selbst (die App aus dem Microsoft Store) nicht. Fehlt die Installation, wird der Bereich automatisch ausgeblendet.
## Konsole, Logs und Crash-Reports ## Konsole, Logs und Crash-Reports
@@ -190,9 +227,17 @@ Nutzerdaten liegen lokal unter `%APPDATA%\aeromc`.
## Rechtliches ## Rechtliches
AeroMC ist kein offizielles Produkt von Mojang oder Microsoft. AeroMC ist kein offizielles Produkt von Mojang oder Microsoft.
Minecraft ist eine Marke von Mojang AB. Minecraft ist eine Marke von Mojang AB.
## Lizenz ### Urheberrecht & Nutzungsbedingungen
MIT AeroMC ist urheberrechtlich geschützt. © M_Viper Alle Rechte vorbehalten.
- ❌ Die Weitergabe dieses Launchers an Dritte ist strengstens untersagt
- ❌ Das Verändern, Dekompilieren oder Bearbeiten des Codes ist nicht erlaubt
- ❌ Eine Veröffentlichung auch in veränderter Form ist verboten
- ❌ Der Verkauf oder die kommerzielle Nutzung ist nicht gestattet
- ✅ Die Nutzung ist ausschließlich für den privaten, nicht-kommerziellen Gebrauch erlaubt
Wer gegen diese Bedingungen verstößt, macht sich strafbar und kann rechtlich belangt werden.

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);