Слияние ui -> main #1

Merged
sasheg merged 145 commits from ui into main 2026-08-20 13:54:02 +00:00
4 changed files with 70 additions and 24 deletions
Showing only changes of commit 4f5bd5387a - Show all commits
@@ -64,7 +64,8 @@ public class VersionInstaller {
Files.createDirectories(versionDir);
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) {
LauncherLogger.info("Minecraft " + versionId + " already installed, skipping");
String existing = Files.readString(versionDir.resolve(versionId + ".json"));
@@ -73,6 +74,13 @@ public class VersionInstaller {
? existingData.getJSONObject("assetIndex").getString("id")
: 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");
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 {
JSONObject versionData = ZHttpClient.getMojangVersionJson(versionId);
@@ -1624,22 +1624,10 @@ public class JFXLauncher extends Application {
Files.createDirectories(modsDir);
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;
java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder()
.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;
}
ZHttpClient.downloadFileWithSmartProxy(url, targetFile);
sendJson(exchange, Map.of("success", true, "message", modName + " installed"));
} catch (Exception e) {
@@ -11,6 +11,7 @@ import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -19,15 +20,33 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
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.stream.Stream;
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()
.connectTimeout(Duration.ofSeconds(15))
.version(HttpClient.Version.HTTP_1_1)
.executor(CLIENT_EXECUTOR)
.build();
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
* calling thread) rather than sendAsync(...).get(...) — the latter submits work
* to the HttpClient's executor and, when that was a non-daemon CachedThreadPool,
* kept the JVM alive / could starve the common ForkJoinPool (the v1.0.14.2
* "JFX hang (ForkJoinPool)" regression, re-introduced in v1.0.14.3).
* The request-level .timeout(Duration) bounds the actual network I/O.
* HTTP send with a hard, actually-enforced timeout.
*
* NB: HttpRequest.timeout() alone is NOT reliable for streaming body handlers
* (BodyHandlers.ofFile/ofString): the JDK cancels the request timer as soon as
* response headers arrive, so a download that stalls mid-body blocks
* 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 {
HttpRequest timed = HttpRequest.newBuilder(request, (name, value) -> true)
.timeout(Duration.ofSeconds(timeoutSeconds))
.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 {
+1 -1
View File
@@ -19,7 +19,7 @@
<properties>
<revision>1.0.16</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>