v1.0.13.1 — pack download stability, proxy for MC/Forge downloads, build.version priority

This commit is contained in:
SashegDev
2026-07-30 14:27:41 +00:00
parent ce8cf32ddd
commit cffa6519ca
7 changed files with 86 additions and 152 deletions
+1 -1
View File
@@ -280,7 +280,7 @@ How to use CLI:
</echo>
<!-- Создаём один архив со всем -->
<zip destfile="../../server/builds/ZernMC-win-${project.version}.zip"
<zip destfile="../../server/builds/ZernMC-win-${revision}.${hotfix}.zip"
basedir="../../server/builds"
includes="zernmc.exe,zernmc-cli.exe,bin/**,assets/**,lib/**,README.txt"
excludes="build.version,*.jar"/>
@@ -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) {
@@ -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<InputStream> 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<InputStream> 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);
}
}
}
}
@@ -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<MinecraftVersion> 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<String> 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<Path> 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) + ")");
}
}
}
+1 -1
View File
@@ -19,7 +19,7 @@
<properties>
<revision>1.0.13</revision>
<hotfix>0</hotfix>
<hotfix>1</hotfix>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+7 -5
View File
@@ -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"
+10
View File
@@ -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"}]