From cffa6519ca8236903ece25f5f97223042b0f4abd Mon Sep 17 00:00:00 2001 From: SashegDev Date: Thu, 30 Jul 2026 14:27:41 +0000 Subject: [PATCH] =?UTF-8?q?v1.0.13.1=20=E2=80=94=20pack=20download=20stabi?= =?UTF-8?q?lity,=20proxy=20for=20MC/Forge=20downloads,=20build.version=20p?= =?UTF-8?q?riority?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- launcher/launcher/pom.xml | 2 +- .../launcher/minecraft/PackDownloader.java | 5 + .../installer/ModLoaderInstaller.java | 101 ++++------------- .../minecraft/installer/VersionInstaller.java | 106 +++++++----------- launcher/pom.xml | 2 +- server/main.py | 12 +- server/news/news6.txt | 10 ++ 7 files changed, 86 insertions(+), 152 deletions(-) create mode 100644 server/news/news6.txt diff --git a/launcher/launcher/pom.xml b/launcher/launcher/pom.xml index f4494dc..6b4c99d 100644 --- a/launcher/launcher/pom.xml +++ b/launcher/launcher/pom.xml @@ -280,7 +280,7 @@ How to use CLI: - diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/PackDownloader.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/PackDownloader.java index c1f7d07..c2d55e8 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/PackDownloader.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/PackDownloader.java @@ -177,8 +177,10 @@ public class PackDownloader { if (needsMinecraftInstall) { LauncherLogger.info("installOrUpdatePack: needs Minecraft install. loader=" + manifest.getLoaderType() + " loaderVer=" + manifest.getLoaderVersion()); + reportProgress("Installing " + manifest.getMinecraftVersion() + " with " + manifest.getLoaderType() + "...", 10, "Installing pack files", 4, 5); if ("fabric".equalsIgnoreCase(manifest.getLoaderType())) { LauncherLogger.info("installOrUpdatePack: installing Fabric mc=" + manifest.getMinecraftVersion() + " loader=" + manifest.getLoaderVersion()); + reportProgress("Installing Fabric " + manifest.getLoaderVersion() + "...", 15, "Installing pack files", 4, 5); boolean success = lib.installFabric(manifest.getMinecraftVersion(), manifest.getLoaderVersion()); LauncherLogger.info("installOrUpdatePack: Fabric install result=" + success); if (!success) { @@ -189,6 +191,7 @@ public class PackDownloader { } } else if ("neoforge".equalsIgnoreCase(manifest.getLoaderType())) { LauncherLogger.info("installOrUpdatePack: installing NeoForge mc=" + manifest.getMinecraftVersion() + " loader=" + manifest.getLoaderVersion()); + reportProgress("Installing NeoForge " + manifest.getLoaderVersion() + "...", 15, "Installing pack files", 4, 5); boolean success = lib.installNeoForge(manifest.getMinecraftVersion(), manifest.getLoaderVersion()); LauncherLogger.info("installOrUpdatePack: NeoForge install result=" + success); if (!success) { @@ -199,6 +202,7 @@ public class PackDownloader { } } else if ("forge".equalsIgnoreCase(manifest.getLoaderType())) { LauncherLogger.info("installOrUpdatePack: installing Forge mc=" + manifest.getMinecraftVersion() + " loader=" + manifest.getLoaderVersion()); + reportProgress("Installing Forge " + manifest.getLoaderVersion() + "...", 15, "Installing pack files", 4, 5); boolean success = lib.installForge(manifest.getMinecraftVersion(), manifest.getLoaderVersion()); LauncherLogger.info("installOrUpdatePack: Forge install result=" + success); if (!success) { @@ -209,6 +213,7 @@ public class PackDownloader { } } else { LauncherLogger.info("installOrUpdatePack: installing Vanilla Minecraft " + manifest.getMinecraftVersion()); + reportProgress("Installing Vanilla Minecraft...", 15, "Installing pack files", 4, 5); boolean success = lib.installMinecraft(manifest.getMinecraftVersion()); LauncherLogger.info("installOrUpdatePack: Vanilla install result=" + success); if (!success) { diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/ModLoaderInstaller.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/ModLoaderInstaller.java index fb61490..65fc356 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/ModLoaderInstaller.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/ModLoaderInstaller.java @@ -1,19 +1,15 @@ package me.sashegdev.zernmc.launcher.minecraft.installer; import me.sashegdev.zernmc.launcher.minecraft.Instance; -import me.sashegdev.zernmc.launcher.utils.ProgressBar; import me.sashegdev.zernmc.launcher.utils.ZAnsi; +import me.sashegdev.zernmc.launcher.utils.ZHttpClient; import java.io.*; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.time.Duration; import java.util.*; +import java.util.concurrent.TimeUnit; public class ModLoaderInstaller { @@ -40,9 +36,6 @@ public class ModLoaderInstaller { } private final Instance instance; - private final HttpClient httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(30)) - .build(); public ModLoaderInstaller(Instance instance) { this.instance = instance; @@ -68,7 +61,7 @@ public class ModLoaderInstaller { Path installerJar = instance.getPath().resolve(jarName); System.out.println(ZAnsi.cyan("Downloading " + type.loaderName + " Installer...")); - downloadFileWithProgress(installerUrl, installerJar, type); + ZHttpClient.downloadFileWithSmartProxy(installerUrl, installerJar); System.out.println(ZAnsi.cyan("Running " + type.loaderName + " Installer...")); System.out.println(ZAnsi.yellow("This may take a few minutes. Please wait...\n")); @@ -122,50 +115,6 @@ public class ModLoaderInstaller { System.out.println(ZAnsi.yellow("Created launcher_profiles.json")); } - private void downloadFileWithProgress(String url, Path target, LoaderType type) throws Exception { - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .GET() - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); - - if (response.statusCode() != 200) { - throw new IOException("HTTP " + response.statusCode()); - } - - long contentLength = response.headers().firstValueAsLong("Content-Length").orElse(-1); - - try (InputStream in = response.body(); - FileOutputStream out = new FileOutputStream(target.toFile())) { - - byte[] buffer = new byte[8192]; - int bytesRead; - long totalRead = 0; - int lastPercent = -1; - - while ((bytesRead = in.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - totalRead += bytesRead; - - if (contentLength > 0) { - int percent = (int) ((totalRead * 100) / contentLength); - if (percent != lastPercent) { - ProgressBar.show(type.loaderName + " Installer", percent, 100, - "% (" + ProgressBar.formatBytes(totalRead) + "/" + ProgressBar.formatBytes(contentLength) + ")"); - lastPercent = percent; - } - } else { - char[] spinner = {'|', '/', '-', '\\'}; - int idx = (int) (totalRead / 1024) % 4; - System.out.print("\rDownloading " + type.loaderName + " Installer: " + ProgressBar.formatBytes(totalRead) + " " + spinner[idx]); - } - } - } - - ProgressBar.finish(type.loaderName + " Installer (" + ProgressBar.formatBytes(Files.size(target)) + ")"); - } - private boolean runInstaller(Path installerJar, LoaderType type) throws IOException, InterruptedException { int maxRetries = 3; int attempt = 1; @@ -212,9 +161,25 @@ public class ModLoaderInstaller { } } - int exitCode = process.waitFor(); + int exitCode; + if (hasErrors) { + process.destroyForcibly(); + exitCode = 1; + } else { + if (!process.waitFor(10, TimeUnit.MINUTES)) { + process.destroyForcibly(); + System.out.println(ZAnsi.brightRed(type.loaderName + " Installer timed out after 10 minutes")); + if (attempt < maxRetries) { + attempt++; + Thread.sleep(5000); + continue; + } + return false; + } + exitCode = process.exitValue(); + } - if (exitCode == 0 && !hasErrors) { + if (exitCode == 0) { return true; } @@ -280,7 +245,7 @@ public class ModLoaderInstaller { for (String mirrorUrl : entry.getValue()) { for (int attempt = 1; attempt <= 3; attempt++) { try { - downloadDirect(mirrorUrl, target); + ZHttpClient.downloadFileWithSmartProxy(mirrorUrl, target); downloaded = true; break; } catch (Exception e) { @@ -294,26 +259,4 @@ public class ModLoaderInstaller { } } } - - private void downloadDirect(String url, Path target) throws Exception { - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .GET() - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); - - if (response.statusCode() != 200) { - throw new IOException("HTTP " + response.statusCode()); - } - - try (InputStream in = response.body(); - FileOutputStream out = new FileOutputStream(target.toFile())) { - byte[] buffer = new byte[8192]; - int bytesRead; - while ((bytesRead = in.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - } - } - } } diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/VersionInstaller.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/VersionInstaller.java index cefb9d6..1bdb478 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/VersionInstaller.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/VersionInstaller.java @@ -3,43 +3,38 @@ package me.sashegdev.zernmc.launcher.minecraft.installer; import me.sashegdev.zernmc.launcher.minecraft.model.MinecraftVersion; import me.sashegdev.zernmc.launcher.utils.ProgressBar; import me.sashegdev.zernmc.launcher.utils.ZAnsi; +import me.sashegdev.zernmc.launcher.utils.ZHttpClient; import org.json.JSONArray; import org.json.JSONObject; -import java.io.IOException; import java.io.InputStream; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; -import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.jar.JarEntry; import java.util.jar.JarFile; public class VersionInstaller { private final Path minecraftDir; - private final HttpClient httpClient; private final ExecutorService executor = Executors.newFixedThreadPool(32); public VersionInstaller(Path minecraftDir) { this.minecraftDir = minecraftDir; - this.httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(15)) - .build(); } public List getAvailableVersions() throws Exception { - String jsonString = downloadString("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"); + String jsonString = ZHttpClient.getWithSmartProxy("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"); JSONObject root = new JSONObject(jsonString); JSONArray versionsArray = root.getJSONArray("versions"); @@ -70,7 +65,7 @@ public class VersionInstaller { if (versionUrl == null) throw new Exception("Version " + versionId + " not found"); ProgressBar.show("Fetching version info", 0, 1, "files"); - String versionJson = downloadString(versionUrl); + String versionJson = ZHttpClient.getWithSmartProxy(versionUrl); Files.writeString(versionDir.resolve(versionId + ".json"), versionJson); ProgressBar.show("Version info", 1, 1, "files"); @@ -78,8 +73,9 @@ public class VersionInstaller { // client.jar ProgressBar.show("Downloading client.jar", 0, 1, "files"); - downloadFile(versionData.getJSONObject("downloads").getJSONObject("client").getString("url"), - versionDir.resolve(versionId + ".jar"), "client.jar"); + ZHttpClient.downloadFileWithSmartProxy( + versionData.getJSONObject("downloads").getJSONObject("client").getString("url"), + versionDir.resolve(versionId + ".jar")); ProgressBar.show("Client.jar", 1, 1, "files"); // Libraries @@ -147,7 +143,7 @@ public class VersionInstaller { if (!Files.exists(libJar)) { try { Files.createDirectories(libJar.getParent()); - downloadFile(url, libJar, ""); + ZHttpClient.downloadFileWithSmartProxy(url, libJar); } catch (Exception e) { failed++; continue; @@ -199,7 +195,7 @@ public class VersionInstaller { Files.createDirectories(target.getParent()); try { - downloadFile(url, target, "library"); + ZHttpClient.downloadFileWithSmartProxy(url, target); } catch (Exception e) { // Skip problematic libraries } @@ -220,7 +216,7 @@ public class VersionInstaller { Path indexPath = indexesDir.resolve(assetIndex + ".json"); System.out.println(ZAnsi.cyan("Downloading asset index (" + assetIndex + ")...")); - downloadFile(indexUrl, indexPath, "asset index"); + ZHttpClient.downloadFileWithSmartProxy(indexUrl, indexPath); String jsonContent = Files.readString(indexPath); JSONObject root = new JSONObject(jsonContent); @@ -249,20 +245,20 @@ public class VersionInstaller { boolean downloaded = false; for (int attempt = 1; attempt <= 3; attempt++) { try { - downloadFile(url, target, ""); + ZHttpClient.downloadFileWithSmartProxy(url, target); + synchronized (this) { + success[0]++; + ProgressBar.show("Assets", success[0], total, "files"); + } + downloaded = true; + break; + } catch (Exception e) { + if (attempt == 3) { synchronized (this) { - success[0]++; - ProgressBar.show("Assets", success[0], total, "files"); + failed[0]++; } - downloaded = true; - break; - } catch (Exception e) { - if (attempt == 3) { - synchronized (this) { - failed[0]++; - } - System.err.println("Failed to download " + hash); - } else { + System.err.println("Failed to download " + hash); + } else { try { Thread.sleep(500 * attempt); } catch (InterruptedException ignored) {} } } @@ -272,8 +268,17 @@ public class VersionInstaller { futures.add(future); } - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - executor.shutdown(); + try { + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .get(10, TimeUnit.MINUTES); + } catch (TimeoutException e) { + futures.forEach(f -> f.cancel(true)); + System.err.println("Asset download timed out after 10 minutes"); + } catch (CancellationException e) { + // one of the futures was cancelled + } catch (CompletionException e) { + // one of the futures failed exceptionally + } ProgressBar.finish("Assets downloaded (" + success[0] + " ok, " + failed[0] + " skipped)"); @@ -287,13 +292,13 @@ public class VersionInstaller { String versionUrl = getVersionUrl(versionId); if (versionUrl == null) throw new Exception("Version not found"); - String versionJson = downloadString(versionUrl); + String versionJson = ZHttpClient.getWithSmartProxy(versionUrl); JSONObject versionData = new JSONObject(versionJson); if (versionData.has("assetIndex") && versionData.getJSONObject("assetIndex").has("id")) { - return versionData.getJSONObject("assetIndex").getString("id"); // "5" для 1.20.1 + return versionData.getJSONObject("assetIndex").getString("id"); } - return versionData.getString("assets"); // fallback (very old versions) + return versionData.getString("assets"); } private String getVersionUrl(String versionId) throws Exception { @@ -302,35 +307,4 @@ public class VersionInstaller { } return null; } - - private String downloadString(String url) throws Exception { - HttpRequest req = HttpRequest.newBuilder().uri(URI.create(url)).GET().build(); - HttpResponse resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString()); - if (resp.statusCode() != 200) throw new IOException("HTTP " + resp.statusCode()); - return resp.body(); - } - - private void downloadFile(String url, Path target, String label) throws Exception { - if (!label.isEmpty()) { - ProgressBar.clearLine(); - System.out.println(ZAnsi.cyan("Downloading " + label + "...")); - } - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .GET() - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target)); - - if (response.statusCode() != 200) { - if (label.isEmpty()) return; // for assets silently - throw new IOException("HTTP " + response.statusCode() + " while downloading " + label); - } - - if (!label.isEmpty()) { - long size = Files.size(target); - ProgressBar.finish(label + " (" + ProgressBar.formatBytes(size) + ")"); - } - } -} \ No newline at end of file +} diff --git a/launcher/pom.xml b/launcher/pom.xml index 2363537..2a8e4a1 100644 --- a/launcher/pom.xml +++ b/launcher/pom.xml @@ -19,7 +19,7 @@ 1.0.13 - 0 + 1 21 21 UTF-8 diff --git a/server/main.py b/server/main.py index 79c3370..7cc625e 100644 --- a/server/main.py +++ b/server/main.py @@ -1261,15 +1261,17 @@ async def get_pack_file(pack_name: str, file_path: str, request: Request, curren # ====================== ЭНДПОИНТЫ ДЛЯ ЛАУНЧЕРА ====================== def get_current_launcher_version() -> str: - """Get current launcher version from meta system (new format) or build.version (legacy)""" + """Get current launcher version — prefers build.version (includes hotfix), falls back to extracted versions""" + version_file = BUILDS_DIR / "build.version" + if version_file.exists(): + v = version_file.read_text().strip() + if v: + return v + versions = get_launcher_versions() if versions: return versions[0]["meta"]["version"] - # Fallback to build.version for legacy - version_file = BUILDS_DIR / "build.version" - if version_file.exists(): - return version_file.read_text().strip() return "1.0.0" diff --git a/server/news/news6.txt b/server/news/news6.txt new file mode 100644 index 0000000..5d3564c --- /dev/null +++ b/server/news/news6.txt @@ -0,0 +1,10 @@ +Pack Download Stability Fix +Update +v1.0.13.1 +[{"bold":1,"text":"Fixed pack downloads getting stuck on Minecraft/Forge installation","click_action":"none"}] +- All Minecraft downloads now use smart proxy system (like the rest of the launcher) +- Added read timeouts to prevent hanging on slow/unstable connections +- Asset download no longer blocks forever if Mojang CDN stalls +- Forge installer process now has a 10-minute timeout +- Progress bar now updates during Minecraft installation phase +[{"clickable":1,"text":"Download v1.0.13.1","click_action":"open_url","url":"https://zernmc.ru/download"}]