Слияние ui -> main #1
@@ -60,6 +60,13 @@ public final class DomainSelector {
|
||||
}
|
||||
}
|
||||
|
||||
if (best != null) {
|
||||
// Make every candidate (incl. geo mirrors) available as a fallback proxy base.
|
||||
for (String c : candidates) {
|
||||
ZHttpClient.registerProxyBase(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (best == null) {
|
||||
// nothing reachable: keep whatever is configured
|
||||
System.err.println(ZAnsi.brightRed("DomainSelector: no reachable API domain"));
|
||||
|
||||
@@ -51,6 +51,24 @@ public class ZHttpClient {
|
||||
|
||||
private static String BASE_URL = "https://api.zern.cc";
|
||||
|
||||
/**
|
||||
* Alternate proxy endpoints (geo mirrors) used when BASE_URL's proxy fails with
|
||||
* a connection error (e.g. CDN "Connection reset" from the current region).
|
||||
* Stay in sync with LAUNCHER_MIRRORS in server/main.py.
|
||||
*/
|
||||
private static final List<String> GEO_PROXY_BASES = List.of(
|
||||
"https://api.zernmc.ru",
|
||||
"https://api.zernmc.online",
|
||||
"https://api.pl.zern.cc",
|
||||
"https://api.swe.zern.cc"
|
||||
);
|
||||
|
||||
/**
|
||||
* Extra proxy bases discovered at runtime (e.g. mirrors fetched from
|
||||
* /launcher/mirrors by DomainSelector). Kept in insertion order, deduplicated.
|
||||
*/
|
||||
private static final List<String> extraProxyBases = new ArrayList<>();
|
||||
|
||||
private static final AtomicBoolean useProxyMode = new AtomicBoolean(false);
|
||||
private static final AtomicBoolean proxyTested = new AtomicBoolean(false);
|
||||
private static final AtomicBoolean healthThreadStarted = new AtomicBoolean(false);
|
||||
@@ -59,6 +77,38 @@ public class ZHttpClient {
|
||||
BASE_URL = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an additional proxy base URL (mirror/geo domain) that the smart
|
||||
* proxy fallback may rotate through. No-op if already present.
|
||||
*/
|
||||
public static void registerProxyBase(String url) {
|
||||
if (url == null || url.isBlank()) return;
|
||||
String normalized = url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
|
||||
synchronized (extraProxyBases) {
|
||||
if (!extraProxyBases.contains(normalized)) {
|
||||
extraProxyBases.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordered, deduplicated list of proxy endpoints to attempt: the primary
|
||||
* BASE_URL first, then geo mirrors and any runtime-discovered mirrors.
|
||||
*/
|
||||
private static List<String> getProxyBases() {
|
||||
List<String> bases = new ArrayList<>();
|
||||
bases.add(BASE_URL);
|
||||
for (String geo : GEO_PROXY_BASES) {
|
||||
if (!bases.contains(geo)) bases.add(geo);
|
||||
}
|
||||
synchronized (extraProxyBases) {
|
||||
for (String m : extraProxyBases) {
|
||||
if (!bases.contains(m)) bases.add(m);
|
||||
}
|
||||
}
|
||||
return bases;
|
||||
}
|
||||
|
||||
public static String getBaseUrl() {
|
||||
return BASE_URL;
|
||||
}
|
||||
@@ -418,11 +468,15 @@ public class ZHttpClient {
|
||||
}
|
||||
}
|
||||
|
||||
List<String> proxyBases = getProxyBases();
|
||||
int maxRetries = 3;
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
// Rotate through every proxy base so one region's Connection Reset does not
|
||||
// burn all retries on the same endpoint.
|
||||
String proxyBase = proxyBases.get((attempt - 1) % proxyBases.size());
|
||||
try {
|
||||
String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8);
|
||||
String proxyUrl = BASE_URL + "/proxy/download?url=" + encodedUrl;
|
||||
String proxyUrl = proxyBase + "/proxy/download?url=" + encodedUrl;
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(proxyUrl))
|
||||
@@ -442,8 +496,10 @@ public class ZHttpClient {
|
||||
|
||||
} catch (Exception e) {
|
||||
if (attempt == maxRetries || !isRetryableError(e)) {
|
||||
throw new IOException("Failed to fetch data directly or via proxy: " + e.getMessage(), e);
|
||||
throw new IOException("Failed to fetch data directly or via proxy (" + proxyBase + "): " + e.getMessage(), e);
|
||||
}
|
||||
LauncherLogger.warn("[NET] Proxy fetch attempt " + attempt + "/" + maxRetries
|
||||
+ " via " + proxyBase + " failed (" + e.getMessage() + "), next proxy base");
|
||||
try { Thread.sleep(1000 * attempt); } catch (InterruptedException ie) { break; }
|
||||
}
|
||||
}
|
||||
@@ -452,6 +508,11 @@ public class ZHttpClient {
|
||||
}
|
||||
|
||||
public static void downloadFileWithSmartProxy(String url, Path target) throws Exception {
|
||||
Path tmp = target.resolveSibling(target.getFileName() + ".part");
|
||||
try {
|
||||
Files.deleteIfExists(tmp);
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
if (!shouldUseProxyForUrl(url)) {
|
||||
try {
|
||||
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
|
||||
@@ -469,12 +530,14 @@ public class ZHttpClient {
|
||||
|
||||
HttpRequest request = requestBuilder.build();
|
||||
|
||||
HttpResponse<Path> response = sendBounded(request, HttpResponse.BodyHandlers.ofFile(target), 45);
|
||||
HttpResponse<Path> response = sendBounded(request, HttpResponse.BodyHandlers.ofFile(tmp), 45);
|
||||
|
||||
if (response.statusCode() == 200) {
|
||||
moveIntoPlace(tmp, target);
|
||||
directSuccessCount++;
|
||||
return;
|
||||
}
|
||||
// non-200 response: fall through to proxy below
|
||||
} catch (Exception e) {
|
||||
if (isConnectionError(e)) {
|
||||
directFailCount++;
|
||||
@@ -486,11 +549,19 @@ public class ZHttpClient {
|
||||
}
|
||||
}
|
||||
|
||||
List<String> proxyBases = getProxyBases();
|
||||
int maxRetries = 3;
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
// Rotate through every proxy base so one region's Connection Reset does not
|
||||
// burn all retries on the same endpoint.
|
||||
String proxyBase = proxyBases.get((attempt - 1) % proxyBases.size());
|
||||
try {
|
||||
String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8);
|
||||
String proxyUrl = BASE_URL + "/proxy/download?url=" + encodedUrl;
|
||||
String proxyUrl = proxyBase + "/proxy/download?url=" + encodedUrl;
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(tmp);
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(proxyUrl))
|
||||
@@ -499,24 +570,45 @@ public class ZHttpClient {
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<Path> response = sendBounded(request, HttpResponse.BodyHandlers.ofFile(target), 300);
|
||||
HttpResponse<Path> response = sendBounded(request, HttpResponse.BodyHandlers.ofFile(tmp), 300);
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
throw new IOException("Proxy download failed: HTTP " + response.statusCode());
|
||||
}
|
||||
|
||||
moveIntoPlace(tmp, target);
|
||||
proxySuccessCount++;
|
||||
return;
|
||||
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
Files.deleteIfExists(tmp);
|
||||
} catch (IOException ignored) {}
|
||||
if (attempt == maxRetries || !isRetryableError(e)) {
|
||||
throw new IOException("Proxy download failed: " + e.getMessage(), e);
|
||||
throw new IOException("Proxy download failed (" + proxyBase + "): " + e.getMessage(), e);
|
||||
}
|
||||
LauncherLogger.warn("[NET] Proxy download attempt " + attempt + "/" + maxRetries
|
||||
+ " via " + proxyBase + " failed (" + e.getMessage() + "), next proxy base");
|
||||
try { Thread.sleep(1000 * attempt); } catch (InterruptedException ie) { break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically replaces the destination with the fully-downloaded temp file.
|
||||
* Prevents a Connection Reset mid-transfer from leaving a corrupt partial
|
||||
* file at the final path that later "file exists" checks would treat as installed.
|
||||
*/
|
||||
private static void moveIntoPlace(Path tmp, Path target) throws IOException {
|
||||
Files.createDirectories(target.getParent());
|
||||
try {
|
||||
Files.move(tmp, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING,
|
||||
java.nio.file.StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (java.nio.file.AtomicMoveNotSupportedException e) {
|
||||
Files.move(tmp, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries a request against the Zern server. The server proxies are slow on cold cache
|
||||
* and may return transient 502/5xx when upstreams are flaky, so a single failure should
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<revision>1.0.16</revision>
|
||||
<hotfix>21</hotfix>
|
||||
<revision>1.1.0</revision>
|
||||
<hotfix>0</hotfix>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
Reference in New Issue
Block a user