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:
SashegDev
2026-05-08 13:17:30 +00:00
parent 6f56012e3a
commit d8f189558a
@@ -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;
@@ -24,6 +22,10 @@ public class Bootstrap {
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();
binDir = baseDir.resolve("bin");
@@ -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,327 +115,6 @@ 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");
@@ -514,7 +125,7 @@ public class Bootstrap {
// Скачиваем сразу в целевой файл (без 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());
}