fix: support 4-part Windows version in version comparison
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 сейчас оверхед — но запомнить на будущее
|
||||||
Reference in New Issue
Block a user