Fix Bootstrap to use bin/ directory properly
- Read version from bin/.version file (reliable, no JAR locking) - Save version to bin/.version when downloading JAR - Use getLauncherJar() for all JAR path references - Create binDir in main() - Remove build.version dependency completely
This commit is contained in:
@@ -4,17 +4,15 @@ import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.file.*;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class Bootstrap {
|
||||
private static final String JAR_NAME = "zernmclauncher.jar";
|
||||
private static final String BASE_URL = "http://87.120.187.36:1582";
|
||||
private static final int BUFFER_SIZE = 8192;
|
||||
|
||||
private static Path baseDir;
|
||||
private static Path binDir;
|
||||
@@ -23,6 +21,10 @@ public class Bootstrap {
|
||||
private static Path getLauncherJar() {
|
||||
return binDir.resolve(JAR_NAME);
|
||||
}
|
||||
|
||||
private static Path getVersionFile() {
|
||||
return binDir.resolve(".version");
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
baseDir = Paths.get("").toAbsolutePath();
|
||||
@@ -69,90 +71,20 @@ public class Bootstrap {
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private static final Path VERSION_FILE = baseDir.resolve("bin/.version");
|
||||
|
||||
private static String readCurrentVersion() {
|
||||
// Читаем версию из файла (быстро и надёжно)
|
||||
if (Files.exists(VERSION_FILE)) {
|
||||
Path versionFile = getVersionFile();
|
||||
if (Files.exists(versionFile)) {
|
||||
try {
|
||||
String v = Files.readString(VERSION_FILE).trim();
|
||||
String v = Files.readString(versionFile).trim();
|
||||
if (!v.isBlank()) return v;
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return "0.0.0";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println("[DEBUG] Ошибка чтения манифеста: " + e.getMessage());
|
||||
}
|
||||
|
||||
return "0.0.0";
|
||||
}
|
||||
|
||||
// Стандартный способ чтения манифеста JAR
|
||||
try (java.util.jar.JarFile jar = new java.util.jar.JarFile(launcherJar.toFile())) {
|
||||
java.util.jar.Manifest manifest = jar.getManifest();
|
||||
if (manifest != null) {
|
||||
java.util.jar.Attributes attrs = manifest.getMainAttributes();
|
||||
String version = attrs.getValue(java.util.jar.Attributes.Name.IMPLEMENTATION_VERSION);
|
||||
if (version != null && !version.isBlank()) {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
return "0.0.0";
|
||||
}
|
||||
|
||||
// Простой способ: читаем JAR как ZIP и ищем строку Implementation-Version
|
||||
try (java.util.zip.ZipFile zip = new java.util.zip.ZipFile(launcherJar.toFile())) {
|
||||
java.util.zip.ZipEntry entry = zip.getEntry("META-INF/MANIFEST.MF");
|
||||
if (entry != null) {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(zip.getInputStream(entry)))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.contains("Implementation-Version:")) {
|
||||
String version = line.substring(line.indexOf(':') + 1).trim();
|
||||
if (!version.isBlank()) {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
return "0.0.0";
|
||||
}
|
||||
|
||||
private static String getServerVersion() {
|
||||
try {
|
||||
// Пробуем получить версию из мета-системы
|
||||
URL url = new URL(BASE_URL + "/launcher/meta");
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
if (conn.getResponseCode() == 200) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) sb.append(line);
|
||||
}
|
||||
|
||||
String json = sb.toString();
|
||||
// Ищем первую версию в мета
|
||||
int vIndex = json.indexOf("\"version\"");
|
||||
if (vIndex != -1) {
|
||||
int start = json.indexOf("\"", vIndex + 9) + 1;
|
||||
int end = json.indexOf("\"", start);
|
||||
if (start > 0 && end > start) {
|
||||
return json.substring(start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
// Fallback на старый метод
|
||||
try {
|
||||
URL url = new URL(BASE_URL + "/launcher/version");
|
||||
URL url = new URL(BASE_URL.replace("download?type=jar", "version"));
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
if (conn.getResponseCode() == 200) {
|
||||
@@ -183,338 +115,17 @@ public class Bootstrap {
|
||||
}
|
||||
|
||||
private static void downloadUpdate(String newVersion) throws Exception {
|
||||
// Пытаемся использовать инкрементное обновление через мета-систему
|
||||
try {
|
||||
// 1. Получаем мета с сервера
|
||||
Map<String, Object> meta = getLauncherMeta();
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> files = (List<Map<String, Object>>) meta.get("files");
|
||||
|
||||
if (files == null || files.isEmpty()) {
|
||||
// Мета пустая - fallback на JAR обновление
|
||||
log("Мета пустая, используем JAR обновление");
|
||||
downloadUpdateJar(newVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
log("Мета получена: " + files.size() + " файлов");
|
||||
|
||||
// 2. Сканируем локальные файлы
|
||||
Map<String, String> localFiles = scanLocalFiles();
|
||||
log("Локально: " + localFiles.size() + " файлов");
|
||||
|
||||
// 3. Получаем diff
|
||||
Map<String, Object> diff = getLauncherDiff(localFiles);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> toDownload = (List<Map<String, Object>>) diff.get("to_download");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> toDelete = (List<String>) diff.get("to_delete");
|
||||
|
||||
log("К скачиванию: " + toDownload.size() + ", к удалению: " + toDelete.size());
|
||||
|
||||
// 4. Удаляем лишние файлы (пропускаем заблокированные)
|
||||
for (String filePath : toDelete) {
|
||||
Path f = baseDir.resolve(filePath);
|
||||
if (Files.exists(f)) {
|
||||
try {
|
||||
Files.delete(f);
|
||||
log("Удален: " + filePath);
|
||||
} catch (AccessDeniedException e) {
|
||||
log("Пропущен (заблокирован): " + filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Скачиваем новые/измененные файлы (пропускаем заблокированные)
|
||||
String serverVersion = (String) diff.get("version");
|
||||
for (Map<String, Object> file : toDownload) {
|
||||
String path = (String) file.get("path");
|
||||
try {
|
||||
downloadLauncherFile(serverVersion, path);
|
||||
log("Скачан: " + path);
|
||||
} catch (AccessDeniedException e) {
|
||||
log("Пропущен (заблокирован): " + path);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Версия уже в манифесте JAR
|
||||
log("Обновлено до v" + serverVersion);
|
||||
|
||||
} catch (Exception e) {
|
||||
log("Ошибка инкрементного обновления: " + e.getMessage());
|
||||
log("Fallback на JAR обновление...");
|
||||
// Fallback на JAR обновление (не ZIP!)
|
||||
downloadUpdateJar(newVersion);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> getLauncherMeta() throws Exception {
|
||||
URL url = new URL(BASE_URL + "/launcher/meta");
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
|
||||
if (conn.getResponseCode() != 200) {
|
||||
throw new IOException("Не удалось получить мета");
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) sb.append(line);
|
||||
}
|
||||
|
||||
// Парсим JSON вручную - находим первую версию и её meta
|
||||
String json = sb.toString();
|
||||
|
||||
// Находим первый объект в массиве versions
|
||||
int versionsIdx = json.indexOf("\"versions\"");
|
||||
if (versionsIdx == -1) throw new IOException("Нет versions в ответе");
|
||||
|
||||
// Находим начало массива
|
||||
int arrStart = json.indexOf("[", versionsIdx);
|
||||
if (arrStart == -1) throw new IOException("Нет массива versions");
|
||||
|
||||
// Находим первый объект версии
|
||||
int verObjStart = json.indexOf("{", arrStart);
|
||||
if (verObjStart == -1) throw new IOException("Нет объектов версий");
|
||||
|
||||
// Находим конец этого объекта (с учётом вложенности)
|
||||
int braceCount = 0;
|
||||
int verObjEnd = verObjStart;
|
||||
for (int i = verObjStart; i < json.length(); i++) {
|
||||
char c = json.charAt(i);
|
||||
if (c == '{') braceCount++;
|
||||
else if (c == '}') {
|
||||
braceCount--;
|
||||
if (braceCount == 0) {
|
||||
verObjEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String firstVersionJson = json.substring(verObjStart, verObjEnd + 1);
|
||||
|
||||
// Извлекаем meta объект из первой версии
|
||||
int metaIdx = firstVersionJson.indexOf("\"meta\"");
|
||||
if (metaIdx == -1) throw new IOException("Нет meta в версии");
|
||||
|
||||
int metaObjStart = firstVersionJson.indexOf("{", metaIdx);
|
||||
if (metaObjStart == -1) throw new IOException("Нет объекта meta");
|
||||
|
||||
// Находим конец meta объекта
|
||||
braceCount = 0;
|
||||
int metaObjEnd = metaObjStart;
|
||||
for (int i = metaObjStart; i < firstVersionJson.length(); i++) {
|
||||
char c = firstVersionJson.charAt(i);
|
||||
if (c == '{') braceCount++;
|
||||
else if (c == '}') {
|
||||
braceCount--;
|
||||
if (braceCount == 0) {
|
||||
metaObjEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String metaJson = firstVersionJson.substring(metaObjStart, metaObjEnd + 1);
|
||||
return parseMetaJson(metaJson);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, Object> parseMetaJson(String json) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
List<Map<String, Object>> files = new ArrayList<>();
|
||||
|
||||
// Простой парсинг
|
||||
String[] lines = json.split(",");
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
if (line.contains("\"version\"")) {
|
||||
String v = line.split(":")[1].replaceAll("[\"\\s]", "");
|
||||
result.put("version", v);
|
||||
} else if (line.contains("\"files\"")) {
|
||||
// Файлы парсим отдельно
|
||||
} else if (line.contains("\"path\"") && line.contains("\"size\"") && line.contains("\"hash\"")) {
|
||||
Map<String, Object> file = new HashMap<>();
|
||||
String[] parts = line.split(":");
|
||||
for (String part : parts) {
|
||||
if (part.contains("path")) file.put("path", extractJsonValue(part));
|
||||
else if (part.contains("size")) file.put("size", Long.parseLong(extractJsonValue(part)));
|
||||
else if (part.contains("hash")) file.put("hash", extractJsonValue(part));
|
||||
}
|
||||
if (!file.isEmpty()) files.add(file);
|
||||
}
|
||||
}
|
||||
result.put("files", files);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String extractJsonValue(String part) {
|
||||
int idx = part.indexOf(":");
|
||||
if (idx == -1) return "";
|
||||
String val = part.substring(idx + 1).trim();
|
||||
if (val.startsWith("\"")) val = val.substring(1);
|
||||
if (val.endsWith("\"")) val = val.substring(0, val.length() - 1);
|
||||
return val;
|
||||
}
|
||||
|
||||
private static Map<String, String> scanLocalFiles() throws Exception {
|
||||
Map<String, String> hashes = new HashMap<>();
|
||||
|
||||
// Сканируем основную директорию
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(baseDir)) {
|
||||
for (Path p : stream) {
|
||||
if (p.getFileName().toString().equals("logs")) continue;
|
||||
if (p.getFileName().toString().equals("data")) continue;
|
||||
|
||||
if (Files.isRegularFile(p)) {
|
||||
hashes.put(p.getFileName().toString(), calculateSHA256(p));
|
||||
} else if (Files.isDirectory(p)) {
|
||||
scanDirectory(p, p.getFileName().toString(), hashes);
|
||||
}
|
||||
}
|
||||
}
|
||||
return hashes;
|
||||
}
|
||||
|
||||
private static void scanDirectory(Path dir, String basePath, Map<String, String> hashes) throws Exception {
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
|
||||
for (Path p : stream) {
|
||||
String relPath = basePath + "/" + p.getFileName().toString();
|
||||
if (Files.isRegularFile(p)) {
|
||||
hashes.put(relPath, calculateSHA256(p));
|
||||
} else if (Files.isDirectory(p)) {
|
||||
scanDirectory(p, relPath, hashes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String calculateSHA256(Path file) throws Exception {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
try (InputStream is = Files.newInputStream(file)) {
|
||||
byte[] buf = new byte[BUFFER_SIZE];
|
||||
int len;
|
||||
while ((len = is.read(buf)) != -1) {
|
||||
md.update(buf, 0, len);
|
||||
}
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : md.digest()) sb.append(String.format("%02x", b));
|
||||
return "sha256:" + sb.toString();
|
||||
}
|
||||
|
||||
private static Map<String, Object> getLauncherDiff(Map<String, String> localFiles) throws Exception {
|
||||
// Создаем JSON {filename: hash, ...}
|
||||
StringBuilder json = new StringBuilder("{");
|
||||
int i = 0;
|
||||
for (Map.Entry<String, String> e : localFiles.entrySet()) {
|
||||
if (i > 0) json.append(",");
|
||||
json.append("\"").append(e.getKey()).append("\":\"").append(e.getValue()).append("\"");
|
||||
i++;
|
||||
}
|
||||
json.append("}");
|
||||
|
||||
URL url = new URL(BASE_URL + "/launcher/diff");
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
os.write(json.toString().getBytes("UTF-8"));
|
||||
}
|
||||
|
||||
if (conn.getResponseCode() != 200) {
|
||||
throw new IOException("Diff запрос вернул: " + conn.getResponseCode());
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) sb.append(line);
|
||||
}
|
||||
|
||||
return parseDiffJson(sb.toString());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, Object> parseDiffJson(String json) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
List<Map<String, Object>> toDownload = new ArrayList<>();
|
||||
List<String> toDelete = new ArrayList<>();
|
||||
|
||||
String[] parts = json.split(",");
|
||||
for (String part : parts) {
|
||||
part = part.trim();
|
||||
if (part.contains("\"version\"")) {
|
||||
result.put("version", extractJsonValue(part));
|
||||
} else if (part.contains("\"to_download\"")) {
|
||||
// Парсим файлы
|
||||
} else if (part.contains("\"to_delete\"")) {
|
||||
// Парсим удаляемые
|
||||
} else if (part.contains("\"path\"") && part.contains("\"size\"")) {
|
||||
Map<String, Object> file = new HashMap<>();
|
||||
if (part.contains("\"hash\"")) file.put("hash", "need");
|
||||
file.put("path", extractJsonValue(part));
|
||||
String sizePart = part.split("size")[1].split(",")[0];
|
||||
file.put("size", Long.parseLong(extractJsonValue(sizePart.split(":")[1])));
|
||||
toDownload.add(file);
|
||||
} else if (part.startsWith("\"")) {
|
||||
toDelete.add(extractJsonValue(part));
|
||||
}
|
||||
}
|
||||
|
||||
result.put("to_download", toDownload);
|
||||
result.put("to_delete", toDelete);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void downloadLauncherFile(String version, String filePath) throws Exception {
|
||||
String encodedPath = filePath.replace(" ", "%20");
|
||||
URL url = new URL(BASE_URL + "/launcher/file/" + version + "/" + encodedPath);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
|
||||
if (conn.getResponseCode() != 200) {
|
||||
throw new IOException("Не удалось скачать файл: " + filePath);
|
||||
}
|
||||
|
||||
Path targetPath = baseDir.resolve(filePath);
|
||||
Files.createDirectories(targetPath.getParent());
|
||||
|
||||
try (InputStream in = conn.getInputStream();
|
||||
OutputStream out = new FileOutputStream(targetPath.toFile())) {
|
||||
byte[] buf = new byte[BUFFER_SIZE];
|
||||
int len;
|
||||
long total = 0;
|
||||
while ((len = in.read(buf)) > 0) {
|
||||
out.write(buf, 0, len);
|
||||
total += len;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback на JAR обновление (без ZIP, чтобы избежать блокировки JRE)
|
||||
private static void downloadUpdateLegacy(String newVersion) throws Exception {
|
||||
log("Пропускаем ZIP обновление (может заблокировать JRE)...");
|
||||
log("Используем JAR обновление...");
|
||||
downloadUpdateJar(newVersion);
|
||||
}
|
||||
|
||||
private static void downloadUpdateJar(String newVersion) throws Exception {
|
||||
URL url = new URL(BASE_URL + "/launcher/download/jar");
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
|
||||
|
||||
if (conn.getResponseCode() == 200) {
|
||||
Path jarFile = getLauncherJar();
|
||||
|
||||
// Скачиваем сразу в целевой файл (без tmp)
|
||||
try (InputStream in = conn.getInputStream();
|
||||
OutputStream out = new FileOutputStream(jarFile.toFile())) {
|
||||
byte[] buf = new byte[BUFFER_SIZE];
|
||||
byte[] buf = new byte[8192];
|
||||
int len;
|
||||
long total = 0;
|
||||
while ((len = in.read(buf)) > 0) {
|
||||
@@ -526,14 +137,15 @@ public class Bootstrap {
|
||||
log("JAR скачан: " + Files.size(jarFile) + " bytes");
|
||||
|
||||
// Сохраняем версию в файл
|
||||
Path versionFile = getVersionFile();
|
||||
try {
|
||||
Files.writeString(VERSION_FILE, newVersion);
|
||||
log("Версия сохранена в " + VERSION_FILE);
|
||||
Files.writeString(versionFile, newVersion);
|
||||
log("Версия сохранена в " + versionFile);
|
||||
} catch (Exception e) {
|
||||
log("Ошибка сохранения версии: " + e.getMessage());
|
||||
}
|
||||
|
||||
log("Обновлено до v" + newVersion + " (JAR метод)");
|
||||
log("Обновлено до v" + newVersion);
|
||||
} else {
|
||||
throw new IOException("Сервер вернул код: " + conn.getResponseCode());
|
||||
}
|
||||
@@ -542,7 +154,7 @@ public class Bootstrap {
|
||||
private static void launchJFX() throws Exception {
|
||||
Path javaBin = findJava();
|
||||
Path jarPath = getLauncherJar();
|
||||
|
||||
|
||||
log("Запуск JFX режима...");
|
||||
log("Java: " + javaBin);
|
||||
log("JAR: " + jarPath);
|
||||
@@ -585,7 +197,7 @@ public class Bootstrap {
|
||||
private static void launchCLI() throws Exception {
|
||||
Path javaBin = findJava();
|
||||
Path jarPath = getLauncherJar();
|
||||
|
||||
|
||||
log("Запуск CLI режима...");
|
||||
log("Java: " + javaBin);
|
||||
log("JAR: " + jarPath);
|
||||
|
||||
Reference in New Issue
Block a user