v1.0.14.3 — route Mojang version data via server proxy, hard-bounded HTTP sends, Forge process timeout

This commit is contained in:
SashegDev
2026-07-31 11:51:21 +00:00
parent 4acbdedf70
commit 7235017493
4 changed files with 88 additions and 37 deletions
@@ -15,6 +15,7 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class ForgeInstaller {
@@ -102,6 +103,7 @@ public class ForgeInstaller {
private void downloadFileWithProgress(String url, Path target) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(java.time.Duration.ofMinutes(5))
.GET()
.build();
@@ -196,7 +198,23 @@ public class ForgeInstaller {
}
}
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("Forge Installer timed out after 10 minutes"));
if (attempt < maxRetries) {
attempt++;
Thread.sleep(5000);
continue;
}
return false;
}
exitCode = process.exitValue();
}
// If successful or no download errors
if (exitCode == 0 && !hasErrors) {
@@ -34,8 +34,8 @@ public class VersionInstaller {
}
public List<MinecraftVersion> getAvailableVersions() throws Exception {
String jsonString = ZHttpClient.getWithSmartProxy("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json");
JSONObject root = new JSONObject(jsonString);
// Prefers Zern server Mojang proxy (reachable + cached), falls back to direct piston-meta
JSONObject root = ZHttpClient.getMojangVersionManifest();
JSONArray versionsArray = root.getJSONArray("versions");
List<MinecraftVersion> versions = new ArrayList<>();
@@ -61,18 +61,21 @@ public class VersionInstaller {
Path versionDir = minecraftDir.resolve("versions").resolve(versionId);
Files.createDirectories(versionDir);
String versionUrl = getVersionUrl(versionId);
if (versionUrl == null) throw new Exception("Version " + versionId + " not found");
ProgressBar.show("Fetching version info", 0, 1, "files");
String versionJson;
try {
versionJson = ZHttpClient.getWithSmartProxy(versionUrl);
Files.writeString(versionDir.resolve(versionId + ".json"), versionJson);
// Prefers Zern server Mojang proxy, falls back to direct piston-meta
versionJson = ZHttpClient.getMojangVersionJson(versionId).toString();
} catch (Exception e) {
System.err.println(ZAnsi.red("[VERSION] Failed to fetch version info: " + e.getMessage()));
throw e;
}
try {
Files.writeString(versionDir.resolve(versionId + ".json"), versionJson);
} catch (Exception e) {
System.err.println(ZAnsi.red("[VERSION] Failed to write version info: " + e.getMessage()));
throw e;
}
ProgressBar.show("Version info", 1, 1, "files");
JSONObject versionData = new JSONObject(versionJson);
@@ -300,22 +303,11 @@ public class VersionInstaller {
}
public String getAssetIndexId(String versionId) throws Exception {
String versionUrl = getVersionUrl(versionId);
if (versionUrl == null) throw new Exception("Version not found");
String versionJson = ZHttpClient.getWithSmartProxy(versionUrl);
JSONObject versionData = new JSONObject(versionJson);
JSONObject versionData = ZHttpClient.getMojangVersionJson(versionId);
if (versionData.has("assetIndex") && versionData.getJSONObject("assetIndex").has("id")) {
return versionData.getJSONObject("assetIndex").getString("id");
}
return versionData.getString("assets");
}
private String getVersionUrl(String versionId) throws Exception {
for (MinecraftVersion v : getAvailableVersions()) {
if (v.getId().equals(versionId)) return v.getUrl();
}
return null;
}
}
@@ -19,6 +19,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class ZHttpClient {
@@ -182,7 +183,7 @@ public class ZHttpClient {
.header("User-Agent", "ZernMC-Launcher/HealthCheck")
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
HttpResponse<String> response = sendBounded(request, HttpResponse.BodyHandlers.ofString(), 10);
int code = response.statusCode();
return code == 200 || code == 404;
} catch (Exception e) {
@@ -284,6 +285,22 @@ public class ZHttpClient {
}
}
private static <T> HttpResponse<T> sendBounded(HttpRequest request, HttpResponse.BodyHandler<T> handler, long timeoutSeconds) throws IOException, InterruptedException {
try {
return client.sendAsync(request, handler).get(timeoutSeconds, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw e;
} catch (java.util.concurrent.ExecutionException e) {
Throwable cause = e.getCause() != null ? e.getCause() : e;
if (cause instanceof IOException) throw (IOException) cause;
if (cause instanceof InterruptedException) throw (InterruptedException) cause;
throw new IOException(cause);
} catch (java.util.concurrent.TimeoutException e) {
throw new IOException("Request timed out after " + timeoutSeconds + "s", e);
}
}
public static String getWithSmartProxy(String url) throws IOException, InterruptedException {
if (!shouldUseProxyForUrl(url)) {
int directRetries = 3;
@@ -296,7 +313,7 @@ public class ZHttpClient {
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
HttpResponse<String> response = sendBounded(request, HttpResponse.BodyHandlers.ofString(), 30);
if (response.statusCode() == 200) {
directSuccessCount++;
@@ -336,7 +353,7 @@ public class ZHttpClient {
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
HttpResponse<String> response = sendBounded(request, HttpResponse.BodyHandlers.ofString(), 45);
if (response.statusCode() != 200) {
throw new IOException("Proxy HTTP " + response.statusCode());
@@ -376,7 +393,7 @@ public class ZHttpClient {
HttpRequest request = requestBuilder.build();
HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(target));
HttpResponse<Path> response = sendBounded(request, HttpResponse.BodyHandlers.ofFile(target), 45);
if (response.statusCode() == 200) {
directSuccessCount++;
@@ -412,7 +429,7 @@ public class ZHttpClient {
.GET()
.build();
HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(target));
HttpResponse<Path> response = sendBounded(request, HttpResponse.BodyHandlers.ofFile(target), 300);
if (response.statusCode() != 200) {
throw new IOException("Proxy download failed: HTTP " + response.statusCode());
@@ -444,7 +461,7 @@ public class ZHttpClient {
}
HttpRequest request = requestBuilder.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
HttpResponse<String> response = sendBounded(request, HttpResponse.BodyHandlers.ofString(), 20);
if (response.statusCode() != 200) {
throw new IOException("HTTP " + response.statusCode());
@@ -462,22 +479,46 @@ public class ZHttpClient {
}
public static JSONObject getMojangVersionManifest() throws IOException, InterruptedException {
String url = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
String response = getWithSmartProxy(url);
return new JSONObject(response);
try {
return getMojangVersionManifestViaServer();
} catch (Exception e) {
System.out.println(ZAnsi.yellow("[NET] Server manifest proxy failed (" + e.getMessage() + "), trying direct piston-meta..."));
return new JSONObject(getWithSmartProxy("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"));
}
}
/**
* Fetch the Mojang version manifest through the Zern server proxy.
* The launcher's own API is reliably reachable, and the manifest is cached server-side.
*/
public static JSONObject getMojangVersionManifestViaServer() throws IOException, InterruptedException {
return new JSONObject(get("/proxy/mojang/version_manifest"));
}
/**
* Fetch a single Mojang version JSON through the Zern server proxy.
* Avoids direct connections to piston-meta.mojang.com entirely.
*/
public static JSONObject getMojangVersionViaServer(String versionId) throws IOException, InterruptedException {
return new JSONObject(get("/proxy/mojang/version/" + URLEncoder.encode(versionId, StandardCharsets.UTF_8)));
}
public static JSONObject getMojangVersionJson(String versionId) throws IOException, InterruptedException {
JSONObject manifest = getMojangVersionManifest();
JSONArray versions = manifest.getJSONArray("versions");
try {
return getMojangVersionViaServer(versionId);
} catch (Exception e) {
System.out.println(ZAnsi.yellow("[NET] Server version proxy failed (" + e.getMessage() + "), trying direct piston-meta..."));
JSONObject manifest = getMojangVersionManifest();
JSONArray versions = manifest.getJSONArray("versions");
for (int i = 0; i < versions.length(); i++) {
JSONObject v = versions.getJSONObject(i);
if (v.getString("id").equals(versionId)) {
return new JSONObject(getWithSmartProxy(v.getString("url")));
for (int i = 0; i < versions.length(); i++) {
JSONObject v = versions.getJSONObject(i);
if (v.getString("id").equals(versionId)) {
return new JSONObject(getWithSmartProxy(v.getString("url")));
}
}
throw new IOException("Version " + versionId + " not found");
}
throw new IOException("Version " + versionId + " not found");
}
public static String getForgeVersionsXml() throws IOException, InterruptedException {
+1 -1
View File
@@ -19,7 +19,7 @@
<properties>
<revision>1.0.14</revision>
<hotfix>2</hotfix>
<hotfix>3</hotfix>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>