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.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import javax.swing.*;
|
||||
import javax.swing.plaf.basic.BasicProgressBarUI;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
@@ -159,16 +162,39 @@ public class Bootstrap {
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(cmd);
|
||||
pb.directory(baseDir.toFile());
|
||||
pb.inheritIO();
|
||||
pb.redirectErrorStream(false);
|
||||
|
||||
log("Starting process: " + String.join(" ", cmd));
|
||||
|
||||
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();
|
||||
errorReader.join(2000);
|
||||
|
||||
log("JFX process exited with code: " + 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) {
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
@@ -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 ======================
|
||||
|
||||
private static class BootstrapUI {
|
||||
|
||||
@@ -88,13 +88,14 @@ public class Bootstrap {
|
||||
try {
|
||||
String[] sa = server.split("\\.");
|
||||
String[] ca = current.split("\\.");
|
||||
for (int i = 0; i < Math.min(sa.length, ca.length); i++) {
|
||||
int sv = Integer.parseInt(sa[i]);
|
||||
int cv = Integer.parseInt(ca[i]);
|
||||
int max = Math.max(sa.length, ca.length);
|
||||
for (int i = 0; i < max; 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 false;
|
||||
}
|
||||
return sa.length > ca.length;
|
||||
return false;
|
||||
} catch (Exception ignored) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -144,18 +144,15 @@ public class LauncherAPI {
|
||||
private static int compareVersions(String a, String b) {
|
||||
String[] partsA = a.split("\\.");
|
||||
String[] partsB = b.split("\\.");
|
||||
int len = Math.min(partsA.length, partsB.length);
|
||||
for (int i = 0; i < len; i++) {
|
||||
int max = Math.max(partsA.length, partsB.length);
|
||||
for (int i = 0; i < max; i++) {
|
||||
try {
|
||||
int numA = Integer.parseInt(partsA[i]);
|
||||
int numB = Integer.parseInt(partsB[i]);
|
||||
int numA = i < partsA.length ? Integer.parseInt(partsA[i]) : 0;
|
||||
int numB = i < partsB.length ? Integer.parseInt(partsB[i]) : 0;
|
||||
if (numA != numB) return Integer.compare(numB, numA);
|
||||
} catch (NumberFormatException e) {
|
||||
int cmp = partsA[i].compareTo(partsB[i]);
|
||||
if (cmp != 0) return cmp;
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
return Integer.compare(partsB.length, partsA.length);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private boolean isNeoForgeCompatible(String version, String mcVersion) {
|
||||
|
||||
@@ -33,6 +33,7 @@ from playtime import router as playtime_router, init_playtime_db
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import uuid
|
||||
import aiofiles
|
||||
import mimetypes
|
||||
|
||||
@@ -67,6 +68,9 @@ MANUAL_BLOCKED_IPS = set(os.environ.get("BLOCKED_IPS", "").split(",")) - {""} #
|
||||
# Cache file for blocklist (load once)
|
||||
BLOCKLIST_CACHE_FILE = Path("data/blocklist_cache.txt")
|
||||
|
||||
# Crash reports directory
|
||||
CRASH_REPORTS_DIR = Path("data/crash_reports")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
@@ -143,6 +147,7 @@ async def lifespan(app: FastAPI):
|
||||
BUILDS_DIR.mkdir(exist_ok=True)
|
||||
PACKS_DIR.mkdir(exist_ok=True)
|
||||
DATA_DIR.mkdir(exist_ok=True)
|
||||
CRASH_REPORTS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
init_db()
|
||||
init_friends_db()
|
||||
@@ -1894,6 +1899,33 @@ async def get_launcher_full_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"
|
||||
|
||||
@@ -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