Skip to content

Commit 51cd577

Browse files
committed
Add retry, backoff, and debug options to plugin updater
Introduces configurable retry and exponential backoff for HTTP downloads, rotating User-Agent support, and enhanced debug logging throughout the plugin update process. Adds new options to UpdateOptions and configuration, updates AupCommand to allow toggling debug mode, and improves error handling and diagnostics for GitHub and generic downloads.
1 parent f6a75c9 commit 51cd577

5 files changed

Lines changed: 380 additions & 133 deletions

File tree

src/main/java/common/PluginDownloader.java

Lines changed: 162 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
package common;
2-
1+
package common;
2+
33
import org.jetbrains.annotations.NotNull;
44

55
import java.io.*;
@@ -14,6 +14,7 @@
1414
import java.util.Objects;
1515
import java.util.Optional;
1616
import java.util.logging.Logger;
17+
import java.util.logging.Level;
1718
import java.util.zip.ZipEntry;
1819
import java.util.zip.ZipException;
1920
import java.util.zip.ZipFile;
@@ -32,13 +33,13 @@
3233
import java.nio.file.SimpleFileVisitor;
3334
import java.nio.file.attribute.BasicFileAttributes;
3435
import java.nio.file.FileVisitResult;
35-
36+
3637
public class PluginDownloader {
3738

3839
private final Logger logger;
3940
private static java.util.Map<String, String> extraHeaders = new java.util.HashMap<>();
4041
private static String overrideUserAgent = null;
41-
42+
4243
public PluginDownloader(Logger logger) {
4344
this.logger = logger;
4445
}
@@ -47,7 +48,7 @@ public static void setHttpHeaders(java.util.Map<String, String> headers, String
4748
extraHeaders = headers != null ? new java.util.HashMap<>(headers) : new java.util.HashMap<>();
4849
overrideUserAgent = (userAgent != null && !userAgent.trim().isEmpty()) ? userAgent.trim() : null;
4950
}
50-
51+
5152
public boolean downloadPlugin(String link, String fileName, String githubToken) throws IOException{
5253
boolean requiresAuth = link.toLowerCase().contains("actions")
5354
&& link.toLowerCase().contains("github")
@@ -66,17 +67,35 @@ public boolean downloadPlugin(String link, String fileName, String githubToken)
6667
return false;
6768
}
6869

69-
for (int attempt = 1; attempt <= 2; attempt++) {
70+
for (int attempt = 1; attempt <= Math.max(2, common.UpdateOptions.maxRetries); attempt++) {
7071
File rawTmp = new File(rawTempPath);
7172
File outTmp = new File(outputTempPath);
7273
cleanupQuietly(rawTmp);
7374
cleanupQuietly(outTmp);
7475
try {
7576
HttpURLConnection connection = openConnection(link, githubToken, requiresAuth);
76-
if (!downloadWithVerification(rawTmp, connection)) {
77-
logger.warning("Download failed (attempt " + attempt + ")");
77+
int code = 0;
78+
try { code = connection.getResponseCode(); } catch (IOException ignored) {}
79+
if (code == 403 || code == 429 || (code >= 500 && code < 600)) {
80+
int base = Math.max(0, common.UpdateOptions.backoffBaseMs);
81+
int max = Math.max(base, common.UpdateOptions.backoffMaxMs);
82+
int delay = Math.min(max, base * (1 << Math.min(attempt, 10))) + new java.util.Random().nextInt(250);
83+
if (common.UpdateOptions.debug) logger.info("[DEBUG] HTTP " + code + " for " + link + ", retry in ~" + delay + "ms (attempt " + attempt + ")");
84+
try { Thread.sleep(delay); } catch (InterruptedException ignored2) { Thread.currentThread().interrupt(); }
7885
continue;
7986
}
87+
if (!downloadWithVerification(rawTmp, connection)) {
88+
logger.warning("Download failed (attempt " + attempt + ") — retrying lenient mode (old-plugin behavior)");
89+
try {
90+
connection = openConnection(link, githubToken, requiresAuth);
91+
if (!downloadLenient(rawTmp, connection)) {
92+
continue;
93+
}
94+
} catch (IOException ex) {
95+
continue;
96+
}
97+
}
98+
8099

81100
if (isZipFile(rawTempPath)) {
82101
boolean extracted = extractFirstJarFromZip(rawTempPath, outputTempPath);
@@ -98,6 +117,7 @@ public boolean downloadPlugin(String link, String fileName, String githubToken)
98117
}
99118

100119
File target = new File(outputFilePath);
120+
if (common.UpdateOptions.debug) logger.info("[DEBUG] Ready to install: temp=" + outTmp.getAbsolutePath() + " -> target=" + target.getAbsolutePath());
101121
if (common.UpdateOptions.ignoreDuplicates && target.exists() && sameDigest(target, outTmp, "MD5")) {
102122
cleanupQuietly(outTmp);
103123
cleanupQuietly(rawTmp);
@@ -115,10 +135,10 @@ public boolean downloadPlugin(String link, String fileName, String githubToken)
115135
}
116136
return false;
117137
}
118-
119-
120-
121-
138+
139+
140+
141+
122142
private String getString(String fileName) {
123143
String basePlugins = "plugins/";
124144
String configuredFilePath = common.UpdateOptions.filePath;
@@ -153,39 +173,12 @@ private String ensureDir(String dir) {
153173
new File(dir).mkdirs();
154174
return dir;
155175
}
156-
176+
157177
private boolean downloadPluginToFile(String outputFilePath, HttpURLConnection connection) throws IOException {
158178
return downloadWithVerification(new File(outputFilePath), connection);
159179
}
160180

161-
private boolean downloadWithVerification(File outFile, HttpURLConnection connection) throws IOException {
162-
long expected = connection.getContentLengthLong();
163-
long written = 0L;
164-
try (InputStream in = connection.getInputStream(); FileOutputStream out = new FileOutputStream(outFile)) {
165-
byte[] buffer = new byte[8192];
166-
int bytesRead;
167-
while ((bytesRead = in.read(buffer)) != -1) {
168-
out.write(buffer, 0, bytesRead);
169-
written += bytesRead;
170-
}
171-
out.getFD().sync();
172-
}
173-
if (expected >= 0 && written != expected) {
174-
logger.warning("Content-Length mismatch: expected=" + expected + ", got=" + written);
175-
cleanupQuietly(outFile);
176-
return false;
177-
}
178-
if (isZipFile(outFile.getPath())) {
179-
try (ZipFile zf = new ZipFile(outFile)) {
180-
} catch (IOException ex) {
181-
logger.warning("Downloaded ZIP appears corrupt: " + ex.getMessage());
182-
cleanupQuietly(outFile);
183-
return false;
184-
}
185-
}
186-
return true;
187-
}
188-
181+
189182
private boolean extractFirstJarFromZip(String zipFilePath, String outputFilePath) throws IOException {
190183
try (ZipFile zipFile = new ZipFile(zipFilePath)) {
191184
List<? extends ZipEntry> entries = Collections.list(zipFile.entries());
@@ -213,7 +206,7 @@ private boolean extractFirstJarFromZip(String zipFilePath, String outputFilePath
213206
return true;
214207
}
215208
}
216-
209+
217210
public boolean downloadJenkinsPlugin(String link, String fileName){
218211
String tempBase = common.UpdateOptions.tempPath != null && !common.UpdateOptions.tempPath.isEmpty() ? ensureDir(common.UpdateOptions.tempPath) : "plugins/";
219212
String rawTempPath = tempBase + fileName + ".download.tmp";
@@ -228,13 +221,23 @@ public boolean downloadJenkinsPlugin(String link, String fileName){
228221
return false;
229222
}
230223

231-
for (int attempt = 1; attempt <= 2; attempt++) {
224+
for (int attempt = 1; attempt <= Math.max(2, common.UpdateOptions.maxRetries); attempt++) {
232225
File rawTmp = new File(rawTempPath);
233226
File outTmp = new File(outputTempPath);
234227
cleanupQuietly(rawTmp);
235228
cleanupQuietly(outTmp);
236229
try {
237230
HttpURLConnection connection = openConnection(link, null, false);
231+
int code = 0;
232+
try { code = connection.getResponseCode(); } catch (IOException ignored) {}
233+
if (code == 403 || code == 429 || (code >= 500 && code < 600)) {
234+
int base = Math.max(0, common.UpdateOptions.backoffBaseMs);
235+
int max = Math.max(base, common.UpdateOptions.backoffMaxMs);
236+
int delay = Math.min(max, base * (1 << Math.min(attempt, 10))) + new java.util.Random().nextInt(250);
237+
if (common.UpdateOptions.debug) logger.info("[DEBUG] HTTP " + code + " for " + link + ", retry in ~" + delay + "ms (attempt " + attempt + ")");
238+
try { Thread.sleep(delay); } catch (InterruptedException ignored2) { Thread.currentThread().interrupt(); }
239+
continue;
240+
}
238241
if (!downloadWithVerification(rawTmp, connection)) {
239242
logger.info("Download failed (attempt " + attempt + ")");
240243
continue;
@@ -256,6 +259,7 @@ public boolean downloadJenkinsPlugin(String link, String fileName){
256259
continue;
257260
}
258261
File target = new File(outputFilePath);
262+
if (common.UpdateOptions.debug) logger.info("[DEBUG] Ready to install: temp=" + outTmp.getAbsolutePath() + " -> target=" + target.getAbsolutePath());
259263
if (target.exists() && sameDigest(target, outTmp, "MD5")) {
260264
cleanupQuietly(outTmp);
261265
cleanupQuietly(rawTmp);
@@ -273,8 +277,8 @@ public boolean downloadJenkinsPlugin(String link, String fileName){
273277
}
274278
return false;
275279
}
276-
277-
280+
281+
278282
public static boolean isZipFile(String filePath) {
279283
Objects.requireNonNull(filePath, "filePath");
280284
Path path = Paths.get(filePath);
@@ -301,22 +305,128 @@ private void moveReplace(File from, File to) throws IOException {
301305

302306
private HttpURLConnection openConnection(String link, String githubToken, boolean requiresAuth) throws IOException {
303307
HttpURLConnection connection = (HttpURLConnection) new URL(link).openConnection();
304-
connection.setRequestProperty("User-Agent", overrideUserAgent != null ? overrideUserAgent : "AutoUpdatePlugins");
305-
connection.setConnectTimeout(common.UpdateOptions.connectTimeoutMs);
306-
connection.setReadTimeout(common.UpdateOptions.readTimeoutMs);
308+
connection.setInstanceFollowRedirects(true);
309+
310+
String ua = (overrideUserAgent != null && !overrideUserAgent.trim().isEmpty()
311+
&& !"AutoUpdatePlugins".equalsIgnoreCase(overrideUserAgent))
312+
? overrideUserAgent.trim() : null;
313+
if (ua == null && common.UpdateOptions.userAgents != null && !common.UpdateOptions.userAgents.isEmpty()) {
314+
ua = common.UpdateOptions.userAgents.get(new java.util.Random().nextInt(common.UpdateOptions.userAgents.size()));
315+
}
316+
if (ua == null) {
317+
ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36";
318+
}
319+
320+
connection.setRequestProperty("User-Agent", ua);
321+
connection.setRequestProperty("Accept-Encoding", "identity");
322+
connection.setRequestProperty("Accept", "application/octet-stream, */*");
323+
connection.setRequestProperty("Accept-Language", "en-US,en;q=0.9");
324+
connection.setRequestProperty("Connection", "keep-alive");
325+
307326
if (requiresAuth && githubToken != null && !githubToken.isEmpty()) {
308327
connection.setRequestProperty("Authorization", "Bearer " + githubToken);
309328
}
329+
try {
330+
java.net.URI uri = java.net.URI.create(link);
331+
String origin = uri.getScheme() + "://" + uri.getHost() + (uri.getPort() > 0 ? (":" + uri.getPort()) : "");
332+
connection.setRequestProperty("Referer", origin);
333+
} catch (Throwable ignored) {}
334+
310335
if (extraHeaders != null) {
311336
for (java.util.Map.Entry<String, String> e : extraHeaders.entrySet()) {
312337
if (e.getKey() != null && e.getValue() != null) {
313338
connection.setRequestProperty(e.getKey(), e.getValue());
314339
}
315340
}
316341
}
342+
343+
connection.setConnectTimeout(common.UpdateOptions.connectTimeoutMs);
344+
connection.setReadTimeout(common.UpdateOptions.readTimeoutMs);
345+
346+
if (common.UpdateOptions.debug) {
347+
logger.info("[DEBUG] OpenConnection url=" + link + ", auth=" + requiresAuth + ", ua=" + ua);
348+
}
317349
return connection;
318350
}
319351

352+
353+
private static boolean isGithubishHost(String host) {
354+
if (host == null) return false;
355+
String h = host.toLowerCase(java.util.Locale.ROOT);
356+
return h.endsWith("github.com")
357+
|| h.endsWith("githubusercontent.com")
358+
|| h.endsWith("codeload.github.com")
359+
|| h.endsWith("objects.githubusercontent.com");
360+
}
361+
private boolean downloadWithVerification(File outFile, HttpURLConnection connection) throws IOException {
362+
long expected = -1L;
363+
boolean canTrustLength = true;
364+
365+
try {
366+
expected = connection.getContentLengthLong();
367+
String ce = connection.getHeaderField("Content-Encoding");
368+
String te = connection.getHeaderField("Transfer-Encoding");
369+
if (expected < 0 || (ce != null && !"identity".equalsIgnoreCase(ce)) || (te != null && "chunked".equalsIgnoreCase(te))) {
370+
canTrustLength = false;
371+
}
372+
} catch (Throwable ignored) {}
373+
374+
long written = 0L;
375+
try (InputStream in = connection.getInputStream(); FileOutputStream out = new FileOutputStream(outFile)) {
376+
if (common.UpdateOptions.debug) {
377+
try {
378+
logger.info("[DEBUG] HTTP code=" + connection.getResponseCode()
379+
+ ", type=" + connection.getContentType()
380+
+ ", length=" + expected
381+
+ (connection.getHeaderField("Content-Encoding") != null
382+
? ", enc=" + connection.getHeaderField("Content-Encoding") : ""));
383+
} catch (IOException ignored) {}
384+
}
385+
byte[] buffer = new byte[8192];
386+
int n;
387+
while ((n = in.read(buffer)) != -1) {
388+
out.write(buffer, 0, n);
389+
written += n;
390+
}
391+
out.getFD().sync();
392+
}
393+
394+
if (canTrustLength && expected >= 0 && written != expected) {
395+
logger.warning("Content-Length mismatch: expected=" + expected + ", got=" + written);
396+
cleanupQuietly(outFile);
397+
return false;
398+
}
399+
try (FileInputStream fis = new FileInputStream(outFile)) {
400+
byte[] probe = new byte[64];
401+
int n = fis.read(probe);
402+
String head = (n > 0) ? new String(probe, 0, n, java.nio.charset.StandardCharsets.ISO_8859_1) : "";
403+
String t = head.trim().toLowerCase(java.util.Locale.ROOT);
404+
if (t.startsWith("<!doctype html") || t.startsWith("<html")) {
405+
cleanupQuietly(outFile);
406+
return false;
407+
}
408+
} catch (Throwable ignored) {}
409+
410+
return true;
411+
}
412+
413+
414+
415+
416+
private boolean downloadLenient(File outFile, HttpURLConnection connection) {
417+
try (InputStream in = connection.getInputStream(); FileOutputStream out = new FileOutputStream(outFile)) {
418+
byte[] buffer = new byte[8192];
419+
int r;
420+
while ((r = in.read(buffer)) != -1) out.write(buffer, 0, r);
421+
out.getFD().sync();
422+
return true;
423+
} catch (IOException e) {
424+
return false;
425+
}
426+
}
427+
428+
429+
320430
public boolean buildFromGitHubRepo(String repoPath, String fileName, String githubToken) throws IOException {
321431
String repoApi = "https://api.github.com/repos" + repoPath;
322432
String defaultBranch = "main";
@@ -395,14 +505,17 @@ public boolean buildFromGitHubRepo(String repoPath, String fileName, String gith
395505
cleanupQuietly(outTmp);
396506
Files.createDirectories(outTmp.getParentFile().toPath());
397507
Files.copy(jar, outTmp.toPath(), StandardCopyOption.REPLACE_EXISTING);
508+
if (common.UpdateOptions.debug) logger.info("[DEBUG] Built jar selected: " + jar.toAbsolutePath());
398509
if (!validateJar(outTmp)) {
399510
logger.info("Built jar is not a valid plugin jar.");
400511
cleanupQuietly(outTmp);
401512
cleanupTreeQuietly(workDir);
402513
cleanupQuietly(rawZipFile);
403514
return false;
404515
}
405-
moveReplace(outTmp, new File(getString(fileName)));
516+
File target = new File(getString(fileName));
517+
if (common.UpdateOptions.debug) logger.info("[DEBUG] Installing built jar: temp=" + outTmp.getAbsolutePath() + " -> target=" + target.getAbsolutePath());
518+
moveReplace(outTmp, target);
406519
cleanupTreeQuietly(workDir);
407520
cleanupQuietly(rawZipFile);
408521
return true;

0 commit comments

Comments
 (0)