From 72350174939f238e4756b16a60074c89cf13cb63 Mon Sep 17 00:00:00 2001 From: SashegDev Date: Fri, 31 Jul 2026 11:51:21 +0000 Subject: [PATCH] =?UTF-8?q?v1.0.14.3=20=E2=80=94=20route=20Mojang=20versio?= =?UTF-8?q?n=20data=20via=20server=20proxy,=20hard-bounded=20HTTP=20sends,?= =?UTF-8?q?=20Forge=20process=20timeout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minecraft/installer/ForgeInstaller.java | 20 ++++- .../minecraft/installer/VersionInstaller.java | 30 +++----- .../zernmc/launcher/utils/ZHttpClient.java | 73 +++++++++++++++---- launcher/pom.xml | 2 +- 4 files changed, 88 insertions(+), 37 deletions(-) diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/ForgeInstaller.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/ForgeInstaller.java index 57387ab..0d257d4 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/ForgeInstaller.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/ForgeInstaller.java @@ -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) { 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 1a88902..94ddd70 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 @@ -34,8 +34,8 @@ public class VersionInstaller { } public List 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 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; - } } diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ZHttpClient.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ZHttpClient.java index bb12d64..a9763f0 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ZHttpClient.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ZHttpClient.java @@ -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 response = client.send(request, HttpResponse.BodyHandlers.ofString()); + HttpResponse 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 HttpResponse sendBounded(HttpRequest request, HttpResponse.BodyHandler 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 response = client.send(request, HttpResponse.BodyHandlers.ofString()); + HttpResponse response = sendBounded(request, HttpResponse.BodyHandlers.ofString(), 30); if (response.statusCode() == 200) { directSuccessCount++; @@ -336,7 +353,7 @@ public class ZHttpClient { .GET() .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + HttpResponse 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 response = client.send(request, HttpResponse.BodyHandlers.ofFile(target)); + HttpResponse response = sendBounded(request, HttpResponse.BodyHandlers.ofFile(target), 45); if (response.statusCode() == 200) { directSuccessCount++; @@ -412,7 +429,7 @@ public class ZHttpClient { .GET() .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofFile(target)); + HttpResponse 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 response = client.send(request, HttpResponse.BodyHandlers.ofString()); + HttpResponse 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 { diff --git a/launcher/pom.xml b/launcher/pom.xml index 2ce239c..180e2d0 100644 --- a/launcher/pom.xml +++ b/launcher/pom.xml @@ -19,7 +19,7 @@ 1.0.14 - 2 + 3 21 21 UTF-8