feat(ui): добавляем прогресс-коллбэк и автообновление лаунчера

- добавили ProgressCallback в PackDownloader для отслеживания прогресса
- SSE эндпоинт /instances/{name}/install/stream для стриминга прогресса
- добавили checkLauncherUpdate() в WebServer для обновления самого лаунчера
- теперь при запуске UI проверяются обновления лаунчера автоматически
This commit is contained in:
SashegDev
2026-05-05 08:53:01 +00:00
parent 1934199ba8
commit 82391e10ea
4 changed files with 241 additions and 32 deletions
@@ -78,6 +78,9 @@ public class Main {
return; return;
} }
// Проверка обновлений лаунчера
checkAndAutoUpdateLauncher();
// Запускаем JavaFX окно // Запускаем JavaFX окно
UIWindow.start(port); UIWindow.start(port);
} }
@@ -13,6 +13,12 @@ import java.util.Map;
public class InstallService { public class InstallService {
private PackDownloader.ProgressCallback progressCallback;
public void setProgressCallback(PackDownloader.ProgressCallback callback) {
this.progressCallback = callback;
}
public ApiResponse<InstallResult> installZernMCPack(String packName, String instanceName) { public ApiResponse<InstallResult> installZernMCPack(String packName, String instanceName) {
try { try {
boolean created = InstanceManager.createInstanceFolder(instanceName); boolean created = InstanceManager.createInstanceFolder(instanceName);
@@ -26,6 +32,9 @@ public class InstallService {
} }
PackDownloader downloader = new PackDownloader(instance); PackDownloader downloader = new PackDownloader(instance);
if (progressCallback != null) {
downloader.setProgressCallback(progressCallback);
}
// Получаем список доступных сборок // Получаем список доступных сборок
List<ServerPack> availablePacks = downloader.getAvailablePacks(); List<ServerPack> availablePacks = downloader.getAvailablePacks();
@@ -29,12 +29,55 @@ public class PackDownloader {
private final Instance instance; private final Instance instance;
private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
private final HttpClient httpClient = HttpClient.newHttpClient(); private final HttpClient httpClient = HttpClient.newHttpClient();
//private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; private ProgressCallback progressCallback;
public interface ProgressCallback {
void onProgress(ProgressInfo info);
}
public static class ProgressInfo {
private String phase;
private int totalFiles;
private int downloadedFiles;
private String currentFile;
private long fileSize;
private long downloadedBytes;
private int filePercent;
private int totalPercent;
private String eta;
public ProgressInfo(String phase, int totalFiles, int downloadedFiles, String currentFile,
long fileSize, long downloadedBytes, int filePercent, int totalPercent, String eta) {
this.phase = phase;
this.totalFiles = totalFiles;
this.downloadedFiles = downloadedFiles;
this.currentFile = currentFile;
this.fileSize = fileSize;
this.downloadedBytes = downloadedBytes;
this.filePercent = filePercent;
this.totalPercent = totalPercent;
this.eta = eta;
}
public String getPhase() { return phase; }
public int getTotalFiles() { return totalFiles; }
public int getDownloadedFiles() { return downloadedFiles; }
public String getCurrentFile() { return currentFile; }
public long getFileSize() { return fileSize; }
public long getDownloadedBytes() { return downloadedBytes; }
public int getFilePercent() { return filePercent; }
public int getTotalPercent() { return totalPercent; }
public String getEta() { return eta; }
}
public PackDownloader(Instance instance) { public PackDownloader(Instance instance) {
this.instance = instance; this.instance = instance;
} }
public void setProgressCallback(ProgressCallback callback) {
this.progressCallback = callback;
}
/** /**
* Получить список доступных паков с сервера * Получить список доступных паков с сервера
*/ */
@@ -399,7 +442,10 @@ public class PackDownloader {
System.out.println(" Загрузить: " + diff.getToDownload().size() + " файлов"); System.out.println(" Загрузить: " + diff.getToDownload().size() + " файлов");
System.out.println(" Удалить: " + diff.getToDelete().size() + " файлов"); System.out.println(" Удалить: " + diff.getToDelete().size() + " файлов");
// Создаем директории если нужно if (progressCallback != null) {
progressCallback.onProgress(new ProgressInfo("starting", diff.getToDownload().size(), 0, "", 0, 0, 0, 0, ""));
}
try { try {
Files.createDirectories(instance.getPath()); Files.createDirectories(instance.getPath());
} catch (IOException e) { } catch (IOException e) {
@@ -407,7 +453,6 @@ public class PackDownloader {
return false; return false;
} }
// Удаляем файлы
for (String filePath : diff.getToDelete()) { for (String filePath : diff.getToDelete()) {
Path fullPath = instance.getPath().resolve(filePath); Path fullPath = instance.getPath().resolve(filePath);
try { try {
@@ -419,7 +464,6 @@ public class PackDownloader {
} }
} }
// Скачиваем файлы
AtomicInteger downloaded = new AtomicInteger(0); AtomicInteger downloaded = new AtomicInteger(0);
int total = diff.getToDownload().size(); int total = diff.getToDownload().size();
@@ -428,13 +472,10 @@ public class PackDownloader {
Path fullPath = instance.getPath().resolve(path); Path fullPath = instance.getPath().resolve(path);
try { try {
// Создаем директории
Files.createDirectories(fullPath.getParent()); Files.createDirectories(fullPath.getParent());
// Скачиваем файл downloadFile(file, fullPath, progressCallback, downloaded.get(), total);
downloadFile(file, fullPath);
// Проверяем хеш
String actualHash = calculateHash(fullPath); String actualHash = calculateHash(fullPath);
if (!actualHash.equals(file.getHash())) { if (!actualHash.equals(file.getHash())) {
throw new IOException("Хеш не совпадает! Ожидался: " + file.getHash() + throw new IOException("Хеш не совпадает! Ожидался: " + file.getHash() +
@@ -446,6 +487,11 @@ public class PackDownloader {
ProgressBar.show("Скачивание", downloaded.get(), total, "файлов"); ProgressBar.show("Скачивание", downloaded.get(), total, "файлов");
} }
if (progressCallback != null) {
progressCallback.onProgress(new ProgressInfo("downloading", total, downloaded.get(), path,
file.getSize(), file.getSize(), 100, (downloaded.get() * 100) / total, ""));
}
} catch (Exception e) { } catch (Exception e) {
System.err.println("\n" + ZAnsi.red(" Ошибка скачивания " + path + ": " + e.getMessage())); System.err.println("\n" + ZAnsi.red(" Ошибка скачивания " + path + ": " + e.getMessage()));
return false; return false;
@@ -456,13 +502,21 @@ public class PackDownloader {
ProgressBar.finish("Скачивание"); ProgressBar.finish("Скачивание");
} }
if (progressCallback != null) {
progressCallback.onProgress(new ProgressInfo("complete", total, total, "", 0, 0, 100, 100, ""));
}
return true; return true;
} }
/** /**
* Скачать один файл с сервера * Скачать один файл с сервера
*/ */
private void downloadFile(FileInfo file, Path destination) throws Exception { private void downloadFile(FileInfo file, Path destination) throws Exception {
downloadFile(file, destination, null, 0, 0);
}
private void downloadFile(FileInfo file, Path destination, ProgressCallback callback, int downloadedFiles, int totalFiles) throws Exception {
String url = ZHttpClient.getBaseUrl() + file.getUrl(); String url = ZHttpClient.getBaseUrl() + file.getUrl();
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = HttpRequest.newBuilder()
@@ -477,7 +531,6 @@ public class PackDownloader {
throw new IOException("HTTP " + response.statusCode()); throw new IOException("HTTP " + response.statusCode());
} }
// Скачиваем с прогрессом
try (InputStream in = response.body(); try (InputStream in = response.body();
FileOutputStream out = new FileOutputStream(destination.toFile())) { FileOutputStream out = new FileOutputStream(destination.toFile())) {
@@ -485,18 +538,28 @@ public class PackDownloader {
int bytesRead; int bytesRead;
long totalRead = 0; long totalRead = 0;
long fileSize = file.getSize(); long fileSize = file.getSize();
long lastCallbackTime = 0;
while ((bytesRead = in.read(buffer)) != -1) { while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead); out.write(buffer, 0, bytesRead);
totalRead += bytesRead; totalRead += bytesRead;
if (fileSize > 0 && totalRead % 8192 == 0) { if (fileSize > 0) {
ProgressBar.showDownload(" " + file.getPath(), totalRead, fileSize); ProgressBar.showDownload(" " + file.getPath(), totalRead, fileSize);
long now = System.currentTimeMillis();
if (callback != null && now - lastCallbackTime > 200) {
int filePercent = (int) ((totalRead * 100) / fileSize);
int totalPercent = totalFiles > 0 ? ((downloadedFiles * 100 + filePercent) / totalFiles) : 0;
callback.onProgress(new ProgressInfo("downloading", totalFiles, downloadedFiles, file.getPath(),
fileSize, totalRead, filePercent, totalPercent, ""));
lastCallbackTime = now;
}
} }
} }
}
ProgressBar.clearLine(); ProgressBar.clearLine();
}
} }
/** /**
@@ -5,13 +5,21 @@ import io.javalin.http.staticfiles.Location;
import me.sashegdev.zernmc.launcher.api.ApiResponse; import me.sashegdev.zernmc.launcher.api.ApiResponse;
import me.sashegdev.zernmc.launcher.api.LauncherAPI; import me.sashegdev.zernmc.launcher.api.LauncherAPI;
import me.sashegdev.zernmc.launcher.api.instance.InstanceService; import me.sashegdev.zernmc.launcher.api.instance.InstanceService;
import me.sashegdev.zernmc.launcher.api.install.InstallService;
import me.sashegdev.zernmc.launcher.auth.AuthManager; import me.sashegdev.zernmc.launcher.auth.AuthManager;
import me.sashegdev.zernmc.launcher.utils.ZAnsi; import me.sashegdev.zernmc.launcher.utils.ZAnsi;
import me.sashegdev.zernmc.launcher.utils.ZHttpClient;
import java.awt.Desktop; import java.awt.Desktop;
import java.io.IOException; import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.URI; import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -182,6 +190,48 @@ public class WebServer {
} }
}); });
// SSE прогресс установки
app.get("/api/instances/{name}/install/stream", ctx -> {
ctx.header("Content-Type", "text/event-stream");
ctx.header("Cache-Control", "no-cache");
ctx.header("Connection", "keep-alive");
String instanceName = ctx.pathParam("name");
var instanceInfo = api.instances().getInstance(instanceName);
if (!instanceInfo.isSuccess() || instanceInfo.getData() == null) {
ctx.result("data: {\"phase\":\"error\",\"message\":\"Instance not found\"}\n\n");
return;
}
var os = ctx.outputStream();
InstallService service = new InstallService();
service.setProgressCallback(info -> {
try {
String json = String.format(
"{\"phase\":\"%s\",\"totalFiles\":%d,\"downloadedFiles\":%d,\"currentFile\":\"%s\",\"fileSize\":%d,\"downloadedBytes\":%d,\"filePercent\":%d,\"totalPercent\":%d,\"eta\":\"%s\"}",
info.getPhase(), info.getTotalFiles(), info.getDownloadedFiles(),
info.getCurrentFile() != null ? info.getCurrentFile().replace("\"", "\\\"") : "",
info.getFileSize(), info.getDownloadedBytes(),
info.getFilePercent(), info.getTotalPercent(),
info.getEta() != null ? info.getEta() : ""
);
os.write(("data: " + json + "\n\n").getBytes());
os.flush();
} catch (Exception e) {}
});
var result = service.installZernMCPack(instanceInfo.getData().getServerPackName(), instanceName);
try {
if (!result.isSuccess()) {
os.write(("data: {\"phase\":\"error\",\"message\":\"" + result.getError().replace("\"", "\\\"") + "\"}\n\n").getBytes());
} else {
os.write("data: {\"phase\":\"complete\"}\n\n".getBytes());
}
os.flush();
} catch (Exception e) {}
});
// Проверка обновлений // Проверка обновлений
app.get("/api/instances/{name}/updates", ctx -> { app.get("/api/instances/{name}/updates", ctx -> {
String name = ctx.pathParam("name"); String name = ctx.pathParam("name");
@@ -326,4 +376,88 @@ public class WebServer {
app.stop(); app.stop();
} }
} }
// ==================== LAUNCHER AUTO-UPDATE ====================
public static void checkLauncherUpdate() {
try {
String json = ZHttpClient.getLauncherVersionInfo();
String serverVersion = extractVersion(json);
String currentVersion = me.sashegdev.zernmc.launcher.utils.Version.getCurrentVersion();
if (me.sashegdev.zernmc.launcher.utils.Version.isNewer(currentVersion, serverVersion)) {
System.out.println(ZAnsi.brightYellow("\nДоступна новая версия лаунчера! (" + serverVersion + ")"));
System.out.println(ZAnsi.cyan("Начинается автоматическое обновление...\n"));
performLauncherUpdate(serverVersion);
restartLauncher();
} else {
System.out.println(ZAnsi.brightGreen("Лаунчер актуален."));
}
} catch (Exception e) {
System.out.println(ZAnsi.yellow("Не удалось проверить обновления лаунчера."));
System.out.println(ZAnsi.white("Ошибка: ") + e.getMessage());
}
}
private static void performLauncherUpdate(String newVersion) throws Exception {
String downloadUrl = ZHttpClient.getBaseUrl() + "/launcher/download?type=jar";
Path currentJar = getCurrentJarPath();
Path tempJar = currentJar.getParent().resolve("zernmc-launcher-new.jar");
System.out.println(ZAnsi.cyan("Скачивание версии " + newVersion + "..."));
HttpClient client = HttpClient.newBuilder().build();
HttpRequest request = HttpRequest.newBuilder()
.uri(java.net.URI.create(downloadUrl))
.GET()
.build();
HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(tempJar));
if (response.statusCode() != 200) {
throw new IOException("Сервер вернул код: " + response.statusCode());
}
long size = Files.size(tempJar);
System.out.println(ZAnsi.brightGreen("Скачано успешно (" + (size / 1024) + " KB)"));
Files.move(tempJar, currentJar, StandardCopyOption.REPLACE_EXISTING);
System.out.println(ZAnsi.brightGreen("Обновление успешно установлено!"));
}
private static void restartLauncher() {
try {
String javaPath = System.getProperty("java.home") + "/bin/java";
String jarPath = getCurrentJarPath().toAbsolutePath().toString();
System.out.println(ZAnsi.brightGreen("Перезапуск лаунчера с новой версией..."));
new ProcessBuilder(javaPath, "-jar", jarPath)
.inheritIO()
.start();
System.exit(0);
} catch (Exception e) {
System.err.println(ZAnsi.brightRed("Не удалось перезапустить лаунчер."));
System.exit(1);
}
}
private static String extractVersion(String json) {
try {
return json.replaceAll(".*\"version\"\\s*:\\s*\"([^\"]+)\".*", "$1");
} catch (Exception e) {
return "unknown";
}
}
private static Path getCurrentJarPath() {
try {
return Path.of(me.sashegdev.zernmc.launcher.Main.class.getProtectionDomain()
.getCodeSource()
.getLocation()
.toURI());
} catch (Exception e) {
return Path.of("zernmc-launcher.jar");
}
}
} }