v1.0.16.3 — fix install hang at 0% (JDK request timeout not enforced on stalled body, cancel via sendAsync; proxy fallback on timeout), verify client.jar integrity, mod install via smart proxy
This commit is contained in:
+17
-1
@@ -64,7 +64,8 @@ public class VersionInstaller {
|
|||||||
Files.createDirectories(versionDir);
|
Files.createDirectories(versionDir);
|
||||||
|
|
||||||
boolean alreadyInstalled = Files.exists(versionDir.resolve(versionId + ".json"))
|
boolean alreadyInstalled = Files.exists(versionDir.resolve(versionId + ".json"))
|
||||||
&& Files.exists(versionDir.resolve(versionId + ".jar"));
|
&& Files.exists(versionDir.resolve(versionId + ".jar"))
|
||||||
|
&& isValidJar(versionDir.resolve(versionId + ".jar"));
|
||||||
if (alreadyInstalled) {
|
if (alreadyInstalled) {
|
||||||
LauncherLogger.info("Minecraft " + versionId + " already installed, skipping");
|
LauncherLogger.info("Minecraft " + versionId + " already installed, skipping");
|
||||||
String existing = Files.readString(versionDir.resolve(versionId + ".json"));
|
String existing = Files.readString(versionDir.resolve(versionId + ".json"));
|
||||||
@@ -73,6 +74,13 @@ public class VersionInstaller {
|
|||||||
? existingData.getJSONObject("assetIndex").getString("id")
|
? existingData.getJSONObject("assetIndex").getString("id")
|
||||||
: existingData.getString("assets");
|
: existingData.getString("assets");
|
||||||
}
|
}
|
||||||
|
// A stale/partial client.jar (e.g. aborted by the download timeout) must be
|
||||||
|
// removed, otherwise it would be skipped as "already installed" and stay corrupt.
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(versionDir.resolve(versionId + ".jar"));
|
||||||
|
} catch (IOException e) {
|
||||||
|
LauncherLogger.warn("Could not remove stale client.jar: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
ProgressBar.show("Fetching version info", 0, 1, "files");
|
ProgressBar.show("Fetching version info", 0, 1, "files");
|
||||||
String versionJson;
|
String versionJson;
|
||||||
@@ -337,6 +345,14 @@ public class VersionInstaller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isValidJar(Path path) {
|
||||||
|
try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(path.toFile())) {
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public String getAssetIndexId(String versionId) throws Exception {
|
public String getAssetIndexId(String versionId) throws Exception {
|
||||||
JSONObject versionData = ZHttpClient.getMojangVersionJson(versionId);
|
JSONObject versionData = ZHttpClient.getMojangVersionJson(versionId);
|
||||||
|
|
||||||
|
|||||||
@@ -1624,22 +1624,10 @@ public class JFXLauncher extends Application {
|
|||||||
Files.createDirectories(modsDir);
|
Files.createDirectories(modsDir);
|
||||||
Path targetFile = modsDir.resolve(modName);
|
Path targetFile = modsDir.resolve(modName);
|
||||||
|
|
||||||
// Download mod from server
|
// Download mod from server (through ZHttpClient so the read timeout is
|
||||||
|
// actually enforced — raw client.send() hangs forever on a stalled body)
|
||||||
String url = ZHttpClient.getBaseUrl() + "/whitelist/mods/" + modHash + "/" + modName;
|
String url = ZHttpClient.getBaseUrl() + "/whitelist/mods/" + modHash + "/" + modName;
|
||||||
java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder()
|
ZHttpClient.downloadFileWithSmartProxy(url, targetFile);
|
||||||
.uri(java.net.URI.create(url))
|
|
||||||
.timeout(java.time.Duration.ofMinutes(5))
|
|
||||||
.header("Authorization", "Bearer " + AuthManager.getAccessToken())
|
|
||||||
.GET()
|
|
||||||
.build();
|
|
||||||
java.net.http.HttpResponse<Path> resp = java.net.http.HttpClient.newHttpClient()
|
|
||||||
.send(request, java.net.http.HttpResponse.BodyHandlers.ofFile(targetFile));
|
|
||||||
|
|
||||||
if (resp.statusCode() != 200) {
|
|
||||||
Files.deleteIfExists(targetFile);
|
|
||||||
sendJson(exchange, Map.of("success", false, "error", "HTTP " + resp.statusCode()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sendJson(exchange, Map.of("success", true, "message", modName + " installed"));
|
sendJson(exchange, Map.of("success", true, "message", modName + " installed"));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import java.net.URLEncoder;
|
|||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
|
import java.net.http.HttpTimeoutException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -19,15 +20,33 @@ import java.util.ArrayList;
|
|||||||
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.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public class ZHttpClient {
|
public class ZHttpClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dedicated daemon executor for the shared HttpClient. sendAsync() work runs
|
||||||
|
* here (never the common ForkJoinPool, so a stalled download cannot starve
|
||||||
|
* JFX's pools), and daemon threads never keep the launcher JVM alive.
|
||||||
|
*/
|
||||||
|
private static final ExecutorService CLIENT_EXECUTOR = Executors.newCachedThreadPool(r -> {
|
||||||
|
Thread t = new Thread(r, "zern-http-client");
|
||||||
|
t.setDaemon(true);
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
|
||||||
private static final HttpClient client = HttpClient.newBuilder()
|
private static final HttpClient client = HttpClient.newBuilder()
|
||||||
.connectTimeout(Duration.ofSeconds(15))
|
.connectTimeout(Duration.ofSeconds(15))
|
||||||
.version(HttpClient.Version.HTTP_1_1)
|
.version(HttpClient.Version.HTTP_1_1)
|
||||||
|
.executor(CLIENT_EXECUTOR)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
private static String BASE_URL = "https://api.zernmc.ru";
|
private static String BASE_URL = "https://api.zernmc.ru";
|
||||||
@@ -301,18 +320,41 @@ public class ZHttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronous HTTP send with a hard timeout. Uses client.send() (blocking on the
|
* HTTP send with a hard, actually-enforced timeout.
|
||||||
* calling thread) rather than sendAsync(...).get(...) — the latter submits work
|
*
|
||||||
* to the HttpClient's executor and, when that was a non-daemon CachedThreadPool,
|
* NB: HttpRequest.timeout() alone is NOT reliable for streaming body handlers
|
||||||
* kept the JVM alive / could starve the common ForkJoinPool (the v1.0.14.2
|
* (BodyHandlers.ofFile/ofString): the JDK cancels the request timer as soon as
|
||||||
* "JFX hang (ForkJoinPool)" regression, re-introduced in v1.0.14.3).
|
* response headers arrive, so a download that stalls mid-body blocks
|
||||||
* The request-level .timeout(Duration) bounds the actual network I/O.
|
* client.send() forever (no exception, no retry, no proxy fallback — the
|
||||||
|
* exact "install hangs at 0%" bug). Therefore we drive the exchange through
|
||||||
|
* sendAsync() + future.get(timeout) and abort it with cancel() on timeout.
|
||||||
|
*
|
||||||
|
* The client runs on a dedicated daemon executor (see CLIENT_EXECUTOR), so
|
||||||
|
* this does not reintroduce the v1.0.14.2 JFX hang (ForkJoinPool starvation /
|
||||||
|
* non-daemon executor keeping the JVM alive) that forced the old sync-send
|
||||||
|
* implementation.
|
||||||
*/
|
*/
|
||||||
private static <T> HttpResponse<T> sendBounded(HttpRequest request, HttpResponse.BodyHandler<T> handler, long timeoutSeconds) throws IOException, InterruptedException {
|
private static <T> HttpResponse<T> sendBounded(HttpRequest request, HttpResponse.BodyHandler<T> handler, long timeoutSeconds) throws IOException, InterruptedException {
|
||||||
HttpRequest timed = HttpRequest.newBuilder(request, (name, value) -> true)
|
HttpRequest timed = HttpRequest.newBuilder(request, (name, value) -> true)
|
||||||
.timeout(Duration.ofSeconds(timeoutSeconds))
|
.timeout(Duration.ofSeconds(timeoutSeconds))
|
||||||
.build();
|
.build();
|
||||||
return client.send(timed, handler);
|
CompletableFuture<HttpResponse<T>> future = client.sendAsync(timed, handler);
|
||||||
|
try {
|
||||||
|
return future.get(timeoutSeconds, TimeUnit.SECONDS);
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
future.cancel(true);
|
||||||
|
throw new HttpTimeoutException("Response not received within " + timeoutSeconds + "s: " + timed.uri());
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
Throwable cause = e.getCause();
|
||||||
|
if (cause instanceof IOException) throw (IOException) cause;
|
||||||
|
if (cause instanceof InterruptedException) throw (InterruptedException) cause;
|
||||||
|
if (cause instanceof RuntimeException) throw (RuntimeException) cause;
|
||||||
|
throw new IOException("HTTP request failed: " + timed.uri(), cause);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
future.cancel(true);
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getWithSmartProxy(String url) throws IOException, InterruptedException {
|
public static String getWithSmartProxy(String url) throws IOException, InterruptedException {
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>1.0.16</revision>
|
<revision>1.0.16</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