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"}]