fix(plugins): improve errors handling and reporting (#2597)

This commit is contained in:
Skylot
2025-12-22 21:26:43 +00:00
parent b78745a87b
commit 02ea3be8f2
9 changed files with 240 additions and 43 deletions
@@ -290,10 +290,11 @@ public class FileUtils {
}
public static byte[] streamToByteArray(InputStream input) throws IOException {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
copyStream(input, out);
return out.toByteArray();
}
return input.readAllBytes();
}
public static String streamToString(InputStream input) throws IOException {
return new String(streamToByteArray(input), StandardCharsets.UTF_8);
}
public static void close(Closeable c) {
@@ -82,7 +82,7 @@ public class PluginSettings {
LOG.info("Plugin installed: {}", metadata);
requestReload();
} catch (Exception e) {
LOG.error("Install failed", e);
LOG.error("Plugin install failed", e);
mainWindow.showLogViewer(LogOptions.forLevel(Level.ERROR));
}
});
+3
View File
@@ -1,4 +1,5 @@
plugins {
id("jadx-java")
id("jadx-library")
}
@@ -8,4 +9,6 @@ dependencies {
implementation(project(":jadx-commons:jadx-app-commons"))
implementation("com.google.code.gson:gson:2.13.2")
testImplementation("com.squareup.okhttp3:mockwebserver3:5.3.0")
}
@@ -102,8 +102,8 @@ public class JadxPluginsList {
}
private Release fetchLatestRelease() {
LocationInfo latest = new LocationInfo("jadx-decompiler", "jadx-plugins-list", "list", null);
Release release = GithubTools.fetchRelease(latest);
LocationInfo pluginsList = new LocationInfo("jadx-decompiler", "jadx-plugins-list", "list");
Release release = GithubTools.fetchRelease(pluginsList);
List<Asset> assets = release.getAssets();
if (assets.isEmpty()) {
throw new RuntimeException("Release don't have assets");
@@ -9,9 +9,11 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -51,30 +53,43 @@ public class JadxPluginsTools {
public JadxPluginMetadata install(String locationId) {
IJadxPluginResolver resolver = ResolversRegistry.getResolver(locationId);
boolean hasVersion = resolver.hasVersion(locationId);
if (hasVersion) {
JadxPluginMetadata pluginMetadata = resolver.resolve(locationId)
.orElseThrow(() -> new JadxRuntimeException("Failed to resolve plugin location: " + locationId));
fillMetadata(pluginMetadata);
install(pluginMetadata);
return pluginMetadata;
Supplier<List<JadxPluginMetadata>> fetchVersions;
if (resolver.hasVersion(locationId)) {
fetchVersions = () -> {
JadxPluginMetadata version = resolver.resolve(locationId)
.orElseThrow(() -> new JadxRuntimeException("Failed to resolve plugin location: " + locationId));
return Collections.singletonList(version);
};
} else {
// load latest 10 version to search for compatible one
fetchVersions = () -> resolver.resolveVersions(locationId, 1, 10);
}
List<JadxPluginMetadata> versionsMetadata;
try {
versionsMetadata = fetchVersions.get();
} catch (Exception e) {
throw new JadxRuntimeException("Plugin info fetch failed, locationId: " + locationId, e);
}
if (versionsMetadata.isEmpty()) {
throw new JadxRuntimeException("Plugin release not found, locationId: " + locationId);
}
// try other versions in case latest is not compatible with current jadx
VerifyRequiredVersion verifyRequiredVersion = new VerifyRequiredVersion();
for (int i = 1; i <= 5; i++) {
try {
for (JadxPluginMetadata pluginMetadata : resolver.resolveVersions(locationId, i, 1)) {
fillMetadata(pluginMetadata);
if (verifyRequiredVersion.isCompatible(pluginMetadata.getRequiredJadxVersion())) {
install(pluginMetadata);
return pluginMetadata;
}
}
} catch (Exception e) {
LOG.warn("Failed to fetch plugin ({} version before latest)", i, e);
List<String> rejectedVersions = new ArrayList<>();
for (JadxPluginMetadata pluginMetadata : versionsMetadata) {
// download plugin jar and fill metadata
// any download or plugin instantiation errors will stop versions check
fillMetadata(pluginMetadata);
if (verifyRequiredVersion.isCompatible(pluginMetadata.getRequiredJadxVersion())) {
install(pluginMetadata);
return pluginMetadata;
}
rejectedVersions.add(" version " + pluginMetadata.getVersion()
+ " not compatible, require: " + pluginMetadata.getRequiredJadxVersion());
}
throw new JadxRuntimeException("Can't find compatible version to install");
throw new JadxRuntimeException("Can't find compatible version to install"
+ ", current jadx version: " + verifyRequiredVersion.getJadxVersion()
+ "\nrejected versions:\n"
+ String.join("\n", rejectedVersions));
}
public JadxPluginMetadata resolveMetadata(String locationId) {
@@ -1,23 +1,26 @@
package jadx.plugins.tools.resolvers.github;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.List;
import java.util.stream.Collectors;
import com.google.gson.reflect.TypeToken;
import jadx.core.utils.files.FileUtils;
import jadx.plugins.tools.resolvers.github.data.Release;
import static jadx.core.utils.GsonUtils.buildGson;
public class GithubTools {
private static final String GITHUB_API_URL = "https://api.github.com/";
private static final GithubTools GITHUB_INSTANCE = new GithubTools("https://api.github.com");
private static final Type RELEASE_TYPE = new TypeToken<Release>() {
}.getType();
@@ -25,7 +28,21 @@ public class GithubTools {
}.getType();
public static Release fetchRelease(LocationInfo info) {
String projectUrl = GITHUB_API_URL + "repos/" + info.getOwner() + "/" + info.getProject();
return GITHUB_INSTANCE.getRelease(info);
}
public static List<Release> fetchReleases(LocationInfo info, int page, int perPage) {
return GITHUB_INSTANCE.getReleases(info, page, perPage);
}
private final String baseUrl;
GithubTools(String baseUrl) {
this.baseUrl = baseUrl;
}
Release getRelease(LocationInfo info) {
String projectUrl = baseUrl + "/repos/" + info.getOwner() + "/" + info.getProject();
String version = info.getVersion();
if (version == null) {
// get latest version
@@ -40,29 +57,70 @@ public class GithubTools {
+ " Available versions: " + releases.stream().map(Release::getName).collect(Collectors.joining(", "))));
}
public static List<Release> fetchReleases(LocationInfo info, int page, int perPage) {
String projectUrl = GITHUB_API_URL + "repos/" + info.getOwner() + "/" + info.getProject();
List<Release> getReleases(LocationInfo info, int page, int perPage) {
String projectUrl = baseUrl + "/repos/" + info.getOwner() + "/" + info.getProject();
String requestUrl = projectUrl + "/releases?page=" + page + "&per_page=" + perPage;
return get(requestUrl, RELEASE_LIST_TYPE);
}
private static <T> T get(String url, Type type) {
HttpURLConnection con;
HttpURLConnection con = null;
try {
con = (HttpURLConnection) URI.create(url).toURL().openConnection();
con.setRequestMethod("GET");
int code = con.getResponseCode();
if (code != 200) {
// TODO: support redirects?
throw new RuntimeException("Request failed, response: " + code + ", url: " + url);
try {
con = (HttpURLConnection) URI.create(url).toURL().openConnection();
con.setRequestMethod("GET");
con.setInstanceFollowRedirects(true);
int code = con.getResponseCode();
if (code != 200) {
throw new RuntimeException(buildErrorDetails(con, url));
}
} catch (IOException e) {
throw new RuntimeException("Request failed, url: " + url, e);
}
try (Reader reader = new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8)) {
return buildGson().fromJson(reader, type);
} catch (Exception e) {
throw new RuntimeException("Failed to parse response, url: " + url, e);
}
} finally {
if (con != null) {
con.disconnect();
}
} catch (IOException e) {
throw new RuntimeException("Request failed, url: " + url, e);
}
try (Reader reader = new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8)) {
return buildGson().fromJson(reader, type);
}
private static String buildErrorDetails(HttpURLConnection con, String url) throws IOException {
String shortMsg = con.getResponseMessage();
String remainRateLimit = con.getHeaderField("X-RateLimit-Remaining");
if ("0".equals(remainRateLimit)) {
String resetTimeMs = con.getHeaderField("X-RateLimit-Reset");
String timeStr = resetTimeMs != null ? "after " + Instant.ofEpochSecond(Long.parseLong(resetTimeMs)) : "in one hour";
shortMsg += " (rate limit reached, try again " + timeStr + ')';
}
StringBuilder headers = new StringBuilder();
for (int i = 0;; i++) {
String value = con.getHeaderField(i);
if (value == null) {
break;
}
String key = con.getHeaderFieldKey(i);
if (key != null) {
headers.append('\n').append(key).append(": ").append(value);
}
}
String responseStr = getResponseString(con);
return "Request failed: " + con.getResponseCode() + ' ' + shortMsg
+ "\nURL: " + url
+ "\nHeaders:" + headers
+ (responseStr.isEmpty() ? "" : "\nresponse:\n" + responseStr);
}
private static String getResponseString(HttpURLConnection con) {
try (InputStream in = con.getInputStream()) {
return new String(FileUtils.streamToByteArray(in), StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("Failed to parse response, url: " + url, e);
// ignore
return "";
}
}
}
@@ -8,6 +8,10 @@ public class LocationInfo {
private final String artifactPrefix;
private final @Nullable String version;
public LocationInfo(String owner, String project, String artifactPrefix) {
this(owner, project, artifactPrefix, null);
}
public LocationInfo(String owner, String project, String artifactPrefix, @Nullable String version) {
this.owner = owner;
this.project = project;
@@ -0,0 +1,78 @@
package jadx.plugins.tools.resolvers.github;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import mockwebserver3.MockResponse;
import mockwebserver3.MockWebServer;
import jadx.core.utils.files.FileUtils;
import jadx.plugins.tools.resolvers.github.data.Release;
import static org.assertj.core.api.Assertions.assertThat;
class GithubToolsTest {
private static final Logger LOG = LoggerFactory.getLogger(GithubToolsTest.class);
private MockWebServer server;
private GithubTools githubTools;
@BeforeEach
public void setup() throws IOException {
server = new MockWebServer();
server.start();
String baseUrl = server.url("/").toString();
githubTools = new GithubTools(baseUrl);
}
@AfterEach
public void close() {
server.close();
}
@Test
public void getReleaseGood() {
server.enqueue(new MockResponse.Builder()
.body(loadFromResource("plugins-list-good.json"))
.build());
LocationInfo pluginsList = new LocationInfo("jadx-decompiler", "jadx-plugins-list", "list");
Release release = githubTools.getRelease(pluginsList);
LOG.info("Result release: {}", release);
assertThat(release.getName()).isEqualTo("v15");
assertThat(release.getAssets()).hasSize(1);
}
@Test
public void getReleaseRateLimit() {
server.enqueue(new MockResponse.Builder()
.code(403)
.addHeader("x-ratelimit-remaining", "0")
.addHeader("x-ratelimit-reset", Instant.now().plusSeconds(60 * 60).getEpochSecond()) // 1 hour from now
.body("{}")
.build());
LocationInfo pluginsList = new LocationInfo("jadx-decompiler", "jadx-plugins-list", "list");
Assertions.assertThatThrownBy(() -> githubTools.getRelease(pluginsList))
.hasMessageContaining("403")
.hasMessageContaining("Client Error")
.hasMessageContaining("rate limit reached");
}
private static String loadFromResource(String resName) {
try (InputStream stream = GithubToolsTest.class.getResourceAsStream("/github/" + resName)) {
return FileUtils.streamToString(stream);
} catch (IOException e) {
throw new RuntimeException("Failed to load resource: " + resName, e);
}
}
}
@@ -0,0 +1,38 @@
{
"assets": [
{
"browser_download_url": "https://github.com/jadx-decompiler/jadx-plugins-list/releases/download/v15/jadx-plugins-list.zip",
"content_type": "application/zip",
"created_at": "2025-11-29T16:20:40Z",
"digest": "sha256:a2a45c3a22be56b6f9c7e24d52c6411c4d546f386d7ea1e4ba124d4d28b4cf75",
"download_count": 3412,
"id": 322246364,
"label": null,
"name": "jadx-plugins-list.zip",
"node_id": "RA_kwDOKEBYcM4TNRbc",
"size": 1260,
"state": "uploaded",
"updated_at": "2025-11-29T16:20:42Z",
"url": "https://api.github.com/repos/jadx-decompiler/jadx-plugins-list/releases/assets/322246364"
}
],
"assets_url": "https://api.github.com/repos/jadx-decompiler/jadx-plugins-list/releases/266146702/assets",
"body": "What's Changed...",
"created_at": "2025-11-29T16:19:33Z",
"draft": false,
"html_url": "https://github.com/jadx-decompiler/jadx-plugins-list/releases/tag/v15",
"id": 266146702,
"immutable": false,
"mentions_count": 1,
"name": "v15",
"node_id": "RE_kwDOKEBYcM4P3ROO",
"prerelease": false,
"published_at": "2025-11-29T16:20:50Z",
"tag_name": "v15",
"tarball_url": "https://api.github.com/repos/jadx-decompiler/jadx-plugins-list/tarball/v15",
"target_commitish": "main",
"updated_at": "2025-11-29T16:20:50Z",
"upload_url": "https://uploads.github.com/repos/jadx-decompiler/jadx-plugins-list/releases/266146702/assets{?name,label}",
"url": "https://api.github.com/repos/jadx-decompiler/jadx-plugins-list/releases/266146702",
"zipball_url": "https://api.github.com/repos/jadx-decompiler/jadx-plugins-list/zipball/v15"
}