v1.0.14.3 — route Mojang version data via server proxy, hard-bounded HTTP sends, Forge process timeout
This commit is contained in:
+19
-1
@@ -15,6 +15,7 @@ import java.util.Arrays;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class ForgeInstaller {
|
public class ForgeInstaller {
|
||||||
|
|
||||||
@@ -102,6 +103,7 @@ public class ForgeInstaller {
|
|||||||
private void downloadFileWithProgress(String url, Path target) throws Exception {
|
private void downloadFileWithProgress(String url, Path target) throws Exception {
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create(url))
|
.uri(URI.create(url))
|
||||||
|
.timeout(java.time.Duration.ofMinutes(5))
|
||||||
.GET()
|
.GET()
|
||||||
.build();
|
.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 successful or no download errors
|
||||||
if (exitCode == 0 && !hasErrors) {
|
if (exitCode == 0 && !hasErrors) {
|
||||||
|
|||||||
+11
-19
@@ -34,8 +34,8 @@ public class VersionInstaller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<MinecraftVersion> getAvailableVersions() throws Exception {
|
public List<MinecraftVersion> getAvailableVersions() throws Exception {
|
||||||
String jsonString = ZHttpClient.getWithSmartProxy("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json");
|
// Prefers Zern server Mojang proxy (reachable + cached), falls back to direct piston-meta
|
||||||
JSONObject root = new JSONObject(jsonString);
|
JSONObject root = ZHttpClient.getMojangVersionManifest();
|
||||||
JSONArray versionsArray = root.getJSONArray("versions");
|
JSONArray versionsArray = root.getJSONArray("versions");
|
||||||
|
|
||||||
List<MinecraftVersion> versions = new ArrayList<>();
|
List<MinecraftVersion> versions = new ArrayList<>();
|
||||||
@@ -61,18 +61,21 @@ public class VersionInstaller {
|
|||||||
Path versionDir = minecraftDir.resolve("versions").resolve(versionId);
|
Path versionDir = minecraftDir.resolve("versions").resolve(versionId);
|
||||||
Files.createDirectories(versionDir);
|
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");
|
ProgressBar.show("Fetching version info", 0, 1, "files");
|
||||||
String versionJson;
|
String versionJson;
|
||||||
try {
|
try {
|
||||||
versionJson = ZHttpClient.getWithSmartProxy(versionUrl);
|
// Prefers Zern server Mojang proxy, falls back to direct piston-meta
|
||||||
Files.writeString(versionDir.resolve(versionId + ".json"), versionJson);
|
versionJson = ZHttpClient.getMojangVersionJson(versionId).toString();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.err.println(ZAnsi.red("[VERSION] Failed to fetch version info: " + e.getMessage()));
|
System.err.println(ZAnsi.red("[VERSION] Failed to fetch version info: " + e.getMessage()));
|
||||||
throw e;
|
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");
|
ProgressBar.show("Version info", 1, 1, "files");
|
||||||
|
|
||||||
JSONObject versionData = new JSONObject(versionJson);
|
JSONObject versionData = new JSONObject(versionJson);
|
||||||
@@ -300,22 +303,11 @@ public class VersionInstaller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getAssetIndexId(String versionId) throws Exception {
|
public String getAssetIndexId(String versionId) throws Exception {
|
||||||
String versionUrl = getVersionUrl(versionId);
|
JSONObject versionData = ZHttpClient.getMojangVersionJson(versionId);
|
||||||
if (versionUrl == null) throw new Exception("Version not found");
|
|
||||||
|
|
||||||
String versionJson = ZHttpClient.getWithSmartProxy(versionUrl);
|
|
||||||
JSONObject versionData = new JSONObject(versionJson);
|
|
||||||
|
|
||||||
if (versionData.has("assetIndex") && versionData.getJSONObject("assetIndex").has("id")) {
|
if (versionData.has("assetIndex") && versionData.getJSONObject("assetIndex").has("id")) {
|
||||||
return versionData.getJSONObject("assetIndex").getString("id");
|
return versionData.getJSONObject("assetIndex").getString("id");
|
||||||
}
|
}
|
||||||
return versionData.getString("assets");
|
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.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public class ZHttpClient {
|
public class ZHttpClient {
|
||||||
@@ -182,7 +183,7 @@ public class ZHttpClient {
|
|||||||
.header("User-Agent", "ZernMC-Launcher/HealthCheck")
|
.header("User-Agent", "ZernMC-Launcher/HealthCheck")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = sendBounded(request, HttpResponse.BodyHandlers.ofString(), 10);
|
||||||
int code = response.statusCode();
|
int code = response.statusCode();
|
||||||
return code == 200 || code == 404;
|
return code == 200 || code == 404;
|
||||||
} catch (Exception e) {
|
} 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 {
|
public static String getWithSmartProxy(String url) throws IOException, InterruptedException {
|
||||||
if (!shouldUseProxyForUrl(url)) {
|
if (!shouldUseProxyForUrl(url)) {
|
||||||
int directRetries = 3;
|
int directRetries = 3;
|
||||||
@@ -296,7 +313,7 @@ public class ZHttpClient {
|
|||||||
.GET()
|
.GET()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = sendBounded(request, HttpResponse.BodyHandlers.ofString(), 30);
|
||||||
|
|
||||||
if (response.statusCode() == 200) {
|
if (response.statusCode() == 200) {
|
||||||
directSuccessCount++;
|
directSuccessCount++;
|
||||||
@@ -336,7 +353,7 @@ public class ZHttpClient {
|
|||||||
.GET()
|
.GET()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = sendBounded(request, HttpResponse.BodyHandlers.ofString(), 45);
|
||||||
|
|
||||||
if (response.statusCode() != 200) {
|
if (response.statusCode() != 200) {
|
||||||
throw new IOException("Proxy HTTP " + response.statusCode());
|
throw new IOException("Proxy HTTP " + response.statusCode());
|
||||||
@@ -376,7 +393,7 @@ public class ZHttpClient {
|
|||||||
|
|
||||||
HttpRequest request = requestBuilder.build();
|
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) {
|
if (response.statusCode() == 200) {
|
||||||
directSuccessCount++;
|
directSuccessCount++;
|
||||||
@@ -412,7 +429,7 @@ public class ZHttpClient {
|
|||||||
.GET()
|
.GET()
|
||||||
.build();
|
.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) {
|
if (response.statusCode() != 200) {
|
||||||
throw new IOException("Proxy download failed: HTTP " + response.statusCode());
|
throw new IOException("Proxy download failed: HTTP " + response.statusCode());
|
||||||
@@ -444,7 +461,7 @@ public class ZHttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HttpRequest request = requestBuilder.build();
|
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) {
|
if (response.statusCode() != 200) {
|
||||||
throw new IOException("HTTP " + response.statusCode());
|
throw new IOException("HTTP " + response.statusCode());
|
||||||
@@ -462,12 +479,35 @@ public class ZHttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static JSONObject getMojangVersionManifest() throws IOException, InterruptedException {
|
public static JSONObject getMojangVersionManifest() throws IOException, InterruptedException {
|
||||||
String url = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
|
try {
|
||||||
String response = getWithSmartProxy(url);
|
return getMojangVersionManifestViaServer();
|
||||||
return new JSONObject(response);
|
} 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 {
|
public static JSONObject getMojangVersionJson(String versionId) throws IOException, InterruptedException {
|
||||||
|
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();
|
JSONObject manifest = getMojangVersionManifest();
|
||||||
JSONArray versions = manifest.getJSONArray("versions");
|
JSONArray versions = manifest.getJSONArray("versions");
|
||||||
|
|
||||||
@@ -479,6 +519,7 @@ public class ZHttpClient {
|
|||||||
}
|
}
|
||||||
throw new IOException("Version " + versionId + " not found");
|
throw new IOException("Version " + versionId + " not found");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static String getForgeVersionsXml() throws IOException, InterruptedException {
|
public static String getForgeVersionsXml() throws IOException, InterruptedException {
|
||||||
String url = "https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml";
|
String url = "https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml";
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>1.0.14</revision>
|
<revision>1.0.14</revision>
|
||||||
<hotfix>2</hotfix>
|
<hotfix>3</hotfix>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
<maven.compiler.target>21</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
|||||||
Reference in New Issue
Block a user