v1.0.13.1 — pack download stability, proxy for MC/Forge downloads, build.version priority
This commit is contained in:
@@ -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"/>
|
||||
|
||||
+5
@@ -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) {
|
||||
|
||||
+22
-79
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-54
@@ -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,7 +245,7 @@ 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");
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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"}]
|
||||
Reference in New Issue
Block a user