fix: support 4-part Windows version in version comparison

This commit is contained in:
SashegDev
2026-07-30 11:12:08 +00:00
parent 424cf9bc25
commit 15532bf341
5 changed files with 277 additions and 15 deletions
@@ -13,11 +13,14 @@ import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import javax.swing.*; import javax.swing.*;
import javax.swing.plaf.basic.BasicProgressBarUI; import javax.swing.plaf.basic.BasicProgressBarUI;
import java.awt.*; import java.awt.*;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
@@ -159,15 +162,38 @@ public class Bootstrap {
ProcessBuilder pb = new ProcessBuilder(cmd); ProcessBuilder pb = new ProcessBuilder(cmd);
pb.directory(baseDir.toFile()); pb.directory(baseDir.toFile());
pb.inheritIO(); pb.redirectErrorStream(false);
log("Starting process: " + String.join(" ", cmd)); log("Starting process: " + String.join(" ", cmd));
Process p = pb.start(); Process p = pb.start();
// Read stderr in background thread for crash reporting
StringBuilder errorOutput = new StringBuilder();
Thread errorReader = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(p.getErrorStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
errorOutput.append(line).append("\n");
log("[JFX] " + line);
}
} catch (Exception ignored) {}
});
errorReader.setDaemon(true);
errorReader.start();
int code = p.waitFor(); int code = p.waitFor();
errorReader.join(2000);
log("JFX process exited with code: " + code); log("JFX process exited with code: " + code);
System.exit(code);
if (code != 0 && !GraphicsEnvironment.isHeadless()) {
String stderr = errorOutput.toString();
SwingUtilities.invokeLater(() -> showCrashDialog(code, stderr));
} else {
System.exit(code);
}
} }
private static Path findJava(boolean preferConsole) { private static Path findJava(boolean preferConsole) {
@@ -563,6 +589,203 @@ public class Bootstrap {
} }
} }
// ====================== CRASH HANDLER ======================
private static String readFileSafe(Path path, int maxLines) {
if (!Files.exists(path)) return "(file not found)";
try {
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
if (lines.size() <= maxLines) {
return String.join("\n", lines);
}
List<String> tail = lines.subList(lines.size() - maxLines, lines.size());
return "... (" + (lines.size() - maxLines) + " lines omitted)\n" + String.join("\n", tail);
} catch (Exception e) {
return "(error reading: " + e.getMessage() + ")";
}
}
private static Map<String, Object> collectCrashInfo(int exitCode, String stderr) {
Map<String, Object> info = new HashMap<>();
// Version
info.put("launcher_version", readCurrentVersion());
// Exit
info.put("exit_code", exitCode);
info.put("stderr", stderr != null ? stderr : "");
info.put("launcher_log", readFileSafe(logDir.resolve("launcher.log"), 200));
// System
Map<String, Object> system = new HashMap<>();
system.put("os_name", System.getProperty("os.name", "unknown"));
system.put("os_version", System.getProperty("os.version", "unknown"));
system.put("os_arch", System.getProperty("os.arch", "unknown"));
system.put("java_version", System.getProperty("java.version", "unknown"));
system.put("java_vendor", System.getProperty("java.vendor", "unknown"));
system.put("available_processors", Runtime.getRuntime().availableProcessors());
system.put("total_memory_mb", Runtime.getRuntime().maxMemory() / (1024 * 1024));
info.put("system", system);
// User (from auth.json, tokens excluded)
Map<String, Object> user = new HashMap<>();
try {
Path authFile = Path.of(System.getProperty("user.home"), ".zernmc", "auth.json");
if (Files.exists(authFile)) {
String authContent = Files.readString(authFile, StandardCharsets.UTF_8);
JsonObject authJson = JsonParser.parseString(authContent).getAsJsonObject();
if (authJson.has("username")) user.put("username", authJson.get("username").getAsString());
if (authJson.has("uuid")) user.put("uuid", authJson.get("uuid").getAsString());
if (authJson.has("role")) user.put("role", authJson.get("role").getAsInt());
}
} catch (Exception ignored) {}
info.put("user", user);
return info;
}
private static void showCrashDialog(int exitCode, String stderr) {
Map<String, Object> crashInfo = collectCrashInfo(exitCode, stderr);
Gson gson = new GsonBuilder().setPrettyPrinting().create();
String fullReport = gson.toJson(crashInfo);
JFrame frame = new JFrame("ZernMC Launcher - Crash Report");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(700, 520);
frame.setLocationRelativeTo(null);
frame.setResizable(true);
Color bgColor = new Color(0x0c, 0x0c, 0x12);
Color surfaceColor = new Color(0x16, 0x16, 0x1f);
Color accentColor = new Color(0xe9, 0x45, 0x60);
Color textColor = new Color(0xee, 0xee, 0xf0);
Color mutedColor = new Color(0x88, 0x88, 0x9a);
JPanel root = new JPanel(new BorderLayout());
root.setBackground(bgColor);
root.setBorder(BorderFactory.createEmptyBorder(16, 20, 16, 20));
// Title
JLabel titleLabel = new JLabel("ZernMC Launcher Crashed (code: " + exitCode + ")");
titleLabel.setFont(new Font("Segoe UI", Font.BOLD, 16));
titleLabel.setForeground(accentColor);
titleLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 12, 0));
root.add(titleLabel, BorderLayout.NORTH);
// Log area
JTextArea logArea = new JTextArea(fullReport);
logArea.setEditable(false);
logArea.setFont(new Font("Consolas", Font.PLAIN, 12));
logArea.setBackground(surfaceColor);
logArea.setForeground(textColor);
logArea.setCaretColor(textColor);
logArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
JScrollPane scrollPane = new JScrollPane(logArea);
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(0x2a, 0x2a, 0x3a)));
scrollPane.getViewport().setBackground(surfaceColor);
root.add(scrollPane, BorderLayout.CENTER);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 12));
buttonPanel.setOpaque(false);
JLabel statusLabel = new JLabel(" ");
statusLabel.setFont(new Font("Segoe UI", Font.PLAIN, 12));
statusLabel.setForeground(mutedColor);
buttonPanel.add(statusLabel);
JButton openLogsBtn = new JButton("Open Logs Folder");
styleCrashButton(openLogsBtn, mutedColor, bgColor);
openLogsBtn.addActionListener(e -> {
try {
Desktop.getDesktop().open(logDir.toFile());
} catch (Exception ignored) {}
});
buttonPanel.add(openLogsBtn);
JButton sendBtn = new JButton("Send to Server");
styleCrashButton(sendBtn, new Color(0x45, 0xe9, 0x60), bgColor);
sendBtn.addActionListener(e -> {
sendBtn.setEnabled(false);
sendBtn.setText("Sending...");
new Thread(() -> {
boolean ok = sendCrashReport(crashInfo);
SwingUtilities.invokeLater(() -> {
if (ok) {
statusLabel.setForeground(new Color(0x45, 0xe9, 0x60));
statusLabel.setText("Crash report sent. Thank you!");
sendBtn.setText("Sent");
} else {
statusLabel.setForeground(accentColor);
statusLabel.setText("Failed to send. Check logs folder.");
sendBtn.setEnabled(true);
sendBtn.setText("Send to Server");
}
});
}).start();
});
buttonPanel.add(sendBtn);
JButton closeBtn = new JButton("Close");
styleCrashButton(closeBtn, new Color(0x45, 0x60, 0xe9), bgColor);
closeBtn.addActionListener(e -> {
frame.dispose();
System.exit(exitCode);
});
buttonPanel.add(closeBtn);
root.add(buttonPanel, BorderLayout.SOUTH);
frame.setContentPane(root);
frame.setVisible(true);
frame.toFront();
}
private static void styleCrashButton(JButton btn, Color fg, Color bg) {
btn.setFont(new Font("Segoe UI", Font.PLAIN, 13));
btn.setForeground(fg);
btn.setBackground(bg);
btn.setBorderPainted(false);
btn.setFocusPainted(false);
btn.setContentAreaFilled(false);
btn.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
btn.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseEntered(java.awt.event.MouseEvent e) { btn.setForeground(Color.WHITE); }
public void mouseExited(java.awt.event.MouseEvent e) { btn.setForeground(fg); }
});
}
private static boolean sendCrashReport(Map<String, Object> data) {
try {
Gson gson = new Gson();
String jsonBody = gson.toJson(data);
byte[] bodyBytes = jsonBody.getBytes(StandardCharsets.UTF_8);
URL url = new URL(BASE_URL + "/launcher/crash-report");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("Content-Length", String.valueOf(bodyBytes.length));
conn.setDoOutput(true);
conn.setConnectTimeout(10000);
conn.setReadTimeout(10000);
try (java.io.OutputStream os = conn.getOutputStream()) {
os.write(bodyBytes);
os.flush();
}
int status = conn.getResponseCode();
conn.disconnect();
return status == 200;
} catch (Exception e) {
log("Crash report send failed: " + e.getMessage());
return false;
}
}
// ====================== SWING UI ====================== // ====================== SWING UI ======================
private static class BootstrapUI { private static class BootstrapUI {
@@ -88,13 +88,14 @@ public class Bootstrap {
try { try {
String[] sa = server.split("\\."); String[] sa = server.split("\\.");
String[] ca = current.split("\\."); String[] ca = current.split("\\.");
for (int i = 0; i < Math.min(sa.length, ca.length); i++) { int max = Math.max(sa.length, ca.length);
int sv = Integer.parseInt(sa[i]); for (int i = 0; i < max; i++) {
int cv = Integer.parseInt(ca[i]); int sv = i < sa.length ? Integer.parseInt(sa[i]) : 0;
int cv = i < ca.length ? Integer.parseInt(ca[i]) : 0;
if (sv > cv) return true; if (sv > cv) return true;
if (sv < cv) return false; if (sv < cv) return false;
} }
return sa.length > ca.length; return false;
} catch (Exception ignored) {} } catch (Exception ignored) {}
return false; return false;
} }
@@ -144,18 +144,15 @@ public class LauncherAPI {
private static int compareVersions(String a, String b) { private static int compareVersions(String a, String b) {
String[] partsA = a.split("\\."); String[] partsA = a.split("\\.");
String[] partsB = b.split("\\."); String[] partsB = b.split("\\.");
int len = Math.min(partsA.length, partsB.length); int max = Math.max(partsA.length, partsB.length);
for (int i = 0; i < len; i++) { for (int i = 0; i < max; i++) {
try { try {
int numA = Integer.parseInt(partsA[i]); int numA = i < partsA.length ? Integer.parseInt(partsA[i]) : 0;
int numB = Integer.parseInt(partsB[i]); int numB = i < partsB.length ? Integer.parseInt(partsB[i]) : 0;
if (numA != numB) return Integer.compare(numB, numA); if (numA != numB) return Integer.compare(numB, numA);
} catch (NumberFormatException e) { } catch (NumberFormatException ignored) {}
int cmp = partsA[i].compareTo(partsB[i]);
if (cmp != 0) return cmp;
}
} }
return Integer.compare(partsB.length, partsA.length); return 0;
} }
private boolean isNeoForgeCompatible(String version, String mcVersion) { private boolean isNeoForgeCompatible(String version, String mcVersion) {
+32
View File
@@ -33,6 +33,7 @@ from playtime import router as playtime_router, init_playtime_db
import asyncio import asyncio
import hashlib import hashlib
import uuid
import aiofiles import aiofiles
import mimetypes import mimetypes
@@ -67,6 +68,9 @@ MANUAL_BLOCKED_IPS = set(os.environ.get("BLOCKED_IPS", "").split(",")) - {""} #
# Cache file for blocklist (load once) # Cache file for blocklist (load once)
BLOCKLIST_CACHE_FILE = Path("data/blocklist_cache.txt") BLOCKLIST_CACHE_FILE = Path("data/blocklist_cache.txt")
# Crash reports directory
CRASH_REPORTS_DIR = Path("data/crash_reports")
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
@@ -143,6 +147,7 @@ async def lifespan(app: FastAPI):
BUILDS_DIR.mkdir(exist_ok=True) BUILDS_DIR.mkdir(exist_ok=True)
PACKS_DIR.mkdir(exist_ok=True) PACKS_DIR.mkdir(exist_ok=True)
DATA_DIR.mkdir(exist_ok=True) DATA_DIR.mkdir(exist_ok=True)
CRASH_REPORTS_DIR.mkdir(exist_ok=True)
init_db() init_db()
init_friends_db() init_friends_db()
@@ -1894,6 +1899,33 @@ async def get_launcher_full_info():
return info return info
@app.post("/launcher/crash-report")
async def receive_crash_report(request: Request):
"""Receive and store launcher crash reports"""
try:
body = await request.json()
except Exception:
raise HTTPException(status_code=400, detail="Invalid JSON")
ip = request.client.host if request.client else "unknown"
body["source_ip"] = ip
body["received_at"] = datetime.utcnow().isoformat()
report_id = datetime.utcnow().strftime("%Y%m%d_%H%M%S") + "_" + uuid.uuid4().hex[:8]
report_path = CRASH_REPORTS_DIR / f"{report_id}.json"
try:
CRASH_REPORTS_DIR.mkdir(parents=True, exist_ok=True)
async with aiofiles.open(report_path, "w", encoding="utf-8") as f:
await f.write(json.dumps(body, indent=2, ensure_ascii=False))
logger.info(f"Crash report saved: {report_path.name} from {ip}")
except Exception as e:
logger.error(f"Failed to save crash report: {e}")
raise HTTPException(status_code=500, detail="Failed to save report")
return {"status": "ok", "id": report_id}
# ====================== НОВОСТИ ====================== # ====================== НОВОСТИ ======================
NEWS_DIR = Path(__file__).parent / "news" NEWS_DIR = Path(__file__).parent / "news"
+9
View File
@@ -0,0 +1,9 @@
TODO / ideas backlog
====================
Java Agent + IPC (low priority, overkill now)
- Заменить прямой ProcessBuilder.start() на прокси-класс (-javaagent или -cp прокси)
- Прокси класс: подключается к localhost RPC, ждёт "launch", потом reflection вызывает реальный main
- Даёт: двустороннюю связь лаунчер-игра, контроль жизненного цикла, crash-репорты из процесса игры,
передачу настроек/токенов на лету, патчинг классов через Instrumentation API
- Для ZernMC сейчас оверхед — но запомнить на будущее