From 9981949a2b1d319bc8514075383ebf263af7c7b9 Mon Sep 17 00:00:00 2001 From: Skylot <118523+skylot@users.noreply.github.com> Date: Sun, 20 Apr 2025 21:44:47 +0100 Subject: [PATCH] feat(api): add 'unload' method to JadxPlugin (#2463) --- .../main/java/jadx/cli/JCommanderWrapper.java | 14 +++-- .../main/java/jadx/api/JadxDecompiler.java | 5 ++ .../java/jadx/api/plugins/JadxPlugin.java | 8 +++ .../jadx/api/plugins/gui/ISettingsGroup.java | 9 +++ .../jadx/core/plugins/JadxPluginManager.java | 21 ++++++- .../java/jadx/core/plugins/PluginContext.java | 8 ++- .../gui/settings/ui/JadxSettingsWindow.java | 14 ++++- .../settings/ui/plugins/PluginSettings.java | 8 ++- .../ui/plugins/PluginSettingsGroup.java | 15 +++-- .../gui/utils/plugins/CloseablePlugins.java | 31 +++++++++ .../gui/utils/plugins/CollectPlugins.java | 7 ++- .../plugins/SettingsGroupPluginWrap.java | 63 +++++++++++++++++++ 12 files changed, 183 insertions(+), 20 deletions(-) create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/plugins/CloseablePlugins.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/plugins/SettingsGroupPluginWrap.java diff --git a/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java b/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java index 58c50d093..bc25d8658 100644 --- a/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java +++ b/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java @@ -244,13 +244,17 @@ public class JCommanderWrapper { JadxPluginManager pluginManager = decompiler.getPluginManager(); pluginManager.load(new JadxExternalPluginsLoader()); pluginManager.initAll(); - for (PluginContext context : pluginManager.getAllPluginContexts()) { - JadxPluginOptions options = context.getOptions(); - if (options != null) { - if (appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen)) { - k++; + try { + for (PluginContext context : pluginManager.getAllPluginContexts()) { + JadxPluginOptions options = context.getOptions(); + if (options != null) { + if (appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen)) { + k++; + } } } + } finally { + pluginManager.unloadAll(); } } if (sb.length() == 0) { diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index e40a44bfb..3fef15d03 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -170,6 +170,7 @@ public final class JadxDecompiler implements Closeable { } private void reset() { + unloadPlugins(); root = null; classes = null; resources = null; @@ -215,6 +216,10 @@ public final class JadxDecompiler implements Closeable { } } + private void unloadPlugins() { + pluginManager.unloadResolved(); + } + private void loadFinished() { LOG.debug("Load finished"); List list = customPasses.get(JadxAfterLoadPass.TYPE); diff --git a/jadx-core/src/main/java/jadx/api/plugins/JadxPlugin.java b/jadx-core/src/main/java/jadx/api/plugins/JadxPlugin.java index d63eb34f7..ba859142b 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/JadxPlugin.java +++ b/jadx-core/src/main/java/jadx/api/plugins/JadxPlugin.java @@ -23,4 +23,12 @@ public interface JadxPlugin { * For long operation, prefer {@link JadxPreparePass} or {@link JadxAfterLoadPass} instead. */ void init(JadxPluginContext context); + + /** + * Plugin unload handler. + * Can be used to clean up resources on plugin unloading. + */ + default void unload() { + // optional method + } } diff --git a/jadx-core/src/main/java/jadx/api/plugins/gui/ISettingsGroup.java b/jadx-core/src/main/java/jadx/api/plugins/gui/ISettingsGroup.java index 8a8539331..efecea188 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/gui/ISettingsGroup.java +++ b/jadx-core/src/main/java/jadx/api/plugins/gui/ISettingsGroup.java @@ -26,4 +26,13 @@ public interface ISettingsGroup { default List getSubGroups() { return Collections.emptyList(); } + + /** + * Settings close handler. + * Apply settings if 'save' param set to true. + * It can be used to clean up resources. + */ + default void close(boolean save) { + // optional method + } } diff --git a/jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java b/jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java index eb0d204a6..d0d8fd438 100644 --- a/jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java +++ b/jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java @@ -22,7 +22,6 @@ import jadx.api.plugins.loader.JadxPluginLoader; import jadx.api.plugins.options.JadxPluginOptions; import jadx.api.plugins.options.OptionDescription; import jadx.core.plugins.versions.VerifyRequiredVersion; -import jadx.core.utils.exceptions.JadxRuntimeException; public class JadxPluginManager { private static final Logger LOG = LoggerFactory.getLogger(JadxPluginManager.class); @@ -149,7 +148,7 @@ public class JadxPluginManager { } context.init(); } catch (Exception e) { - throw new JadxRuntimeException("Failed to init plugin: " + context.getPluginId(), e); + LOG.warn("Failed to init plugin: {}", context.getPluginId(), e); } } for (PluginContext context : pluginContexts) { @@ -160,6 +159,24 @@ public class JadxPluginManager { } } + public void unloadAll() { + unload(allPlugins); + } + + public void unloadResolved() { + unload(resolvedPlugins); + } + + public void unload(SortedSet pluginContexts) { + for (PluginContext context : pluginContexts) { + try { + context.unload(); + } catch (Exception e) { + LOG.warn("Failed to unload plugin: {}", context.getPluginId(), e); + } + } + } + private AppContext buildDefaultAppContext() { AppContext appContext = new AppContext(); appContext.setGuiContext(null); diff --git a/jadx-core/src/main/java/jadx/core/plugins/PluginContext.java b/jadx-core/src/main/java/jadx/core/plugins/PluginContext.java index 043424e70..9fb4de31c 100644 --- a/jadx-core/src/main/java/jadx/core/plugins/PluginContext.java +++ b/jadx-core/src/main/java/jadx/core/plugins/PluginContext.java @@ -55,11 +55,17 @@ public class PluginContext implements JadxPluginContext, JadxPluginRuntimeData, this.pluginInfo = plugin.getPluginInfo(); } - void init() { + public void init() { plugin.init(this); initialized = true; } + public void unload() { + if (initialized) { + plugin.unload(); + } + } + @Override public boolean isInitialized() { return initialized; diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/JadxSettingsWindow.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/JadxSettingsWindow.java index 4c033b533..62fcb4a1a 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/ui/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/JadxSettingsWindow.java @@ -91,6 +91,7 @@ public class JadxSettingsWindow extends JDialog { private transient boolean needReload = false; private transient SettingsTree tree; + private List groups; public JadxSettingsWindow(MainWindow mainWindow, JadxSettings settings) { this.mainWindow = mainWindow; @@ -116,6 +117,7 @@ public class JadxSettingsWindow extends JDialog { private void reloadUI() { int[] selection = tree.getSelectionRows(); + closeGroups(false); getContentPane().removeAll(); initUI(); // wait for other events to process @@ -128,7 +130,7 @@ public class JadxSettingsWindow extends JDialog { private void initUI() { JPanel wrapGroupPanel = new JPanel(new BorderLayout(10, 10)); - List groups = new ArrayList<>(); + groups = new ArrayList<>(); groups.add(makeDecompilationGroup()); groups.add(makeDeobfuscationGroup()); groups.add(makeRenameGroup()); @@ -690,7 +692,7 @@ public class JadxSettingsWindow extends JDialog { sizeLimit.addChangeListener(ev -> settings.setSrhResourceSkipSize((Integer) sizeLimit.getValue())); JTextField fileExtField = new JTextField(); - fileExtField.getDocument().addDocumentListener(new DocumentUpdateListener((ev) -> { + fileExtField.getDocument().addDocumentListener(new DocumentUpdateListener(ev -> { String ext = fileExtField.getText(); settings.setSrhResourceFileExt(ext); })); @@ -703,7 +705,14 @@ public class JadxSettingsWindow extends JDialog { return searchGroup; } + private void closeGroups(boolean save) { + for (ISettingsGroup group : groups) { + group.close(save); + } + } + private void save() { + closeGroups(true); settings.sync(); enableComponents(this, false); SwingUtilities.invokeLater(() -> { @@ -723,6 +732,7 @@ public class JadxSettingsWindow extends JDialog { } private void cancel() { + closeGroups(false); JadxSettingsAdapter.fill(settings, startSettings); mainWindow.loadSettings(); dispose(); diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettings.java index aaa3b49e1..4e5cef0c7 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettings.java @@ -35,7 +35,9 @@ import jadx.gui.settings.JadxSettings; import jadx.gui.settings.ui.SettingsGroup; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; +import jadx.gui.utils.plugins.CloseablePlugins; import jadx.gui.utils.plugins.CollectPlugins; +import jadx.gui.utils.plugins.SettingsGroupPluginWrap; import jadx.gui.utils.ui.DocumentUpdateListener; import jadx.plugins.tools.JadxPluginsTools; import jadx.plugins.tools.data.JadxPluginMetadata; @@ -53,12 +55,12 @@ public class PluginSettings { } public ISettingsGroup build() { - List collectedPlugins = new CollectPlugins(mainWindow).build(); + CloseablePlugins collectedPlugins = new CollectPlugins(mainWindow).build(); ISettingsGroup pluginsGroup = new PluginSettingsGroup(this, mainWindow, collectedPlugins); - for (PluginContext context : collectedPlugins) { + for (PluginContext context : collectedPlugins.getList()) { ISettingsGroup pluginGroup = addPluginGroup(context); if (pluginGroup != null) { - pluginsGroup.getSubGroups().add(pluginGroup); + pluginsGroup.getSubGroups().add(new SettingsGroupPluginWrap(context.getPluginId(), pluginGroup)); } } return pluginsGroup; diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettingsGroup.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettingsGroup.java index 857aece72..5a25260b2 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettingsGroup.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettingsGroup.java @@ -40,6 +40,7 @@ import jadx.gui.ui.MainWindow; import jadx.gui.utils.Link; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; +import jadx.gui.utils.plugins.CloseablePlugins; import jadx.plugins.tools.JadxPluginsList; import jadx.plugins.tools.JadxPluginsTools; import jadx.plugins.tools.data.JadxPluginMetadata; @@ -51,11 +52,11 @@ class PluginSettingsGroup implements ISettingsGroup { private final MainWindow mainWindow; private final String title; private final List subGroups = new ArrayList<>(); - private final List collectedPlugins; + private final CloseablePlugins collectedPlugins; private JPanel detailsPanel; - public PluginSettingsGroup(PluginSettings pluginSettings, MainWindow mainWindow, List collectedPlugins) { + public PluginSettingsGroup(PluginSettings pluginSettings, MainWindow mainWindow, CloseablePlugins collectedPlugins) { this.pluginsSettings = pluginSettings; this.mainWindow = mainWindow; this.title = NLS.str("preferences.plugins"); @@ -78,6 +79,12 @@ class PluginSettingsGroup implements ISettingsGroup { return buildMainSettingsPage(); } + @Override + public void close(boolean save) { + subGroups.forEach(subGroup -> subGroup.close(save)); + collectedPlugins.close(); + } + private JPanel buildMainSettingsPage() { JButton installPluginBtn = new JButton(NLS.str("preferences.plugins.install")); installPluginBtn.addActionListener(ev -> pluginsSettings.addPlugin()); @@ -124,13 +131,13 @@ class PluginSettingsGroup implements ISettingsGroup { private void applyData(DefaultListModel listModel) { List installed = JadxPluginsTools.getInstance().getInstalled(); - List nodes = new ArrayList<>(installed.size() + collectedPlugins.size()); + List nodes = new ArrayList<>(installed.size() + collectedPlugins.getList().size()); Set installedSet = new HashSet<>(installed.size()); for (JadxPluginMetadata pluginMetadata : installed) { installedSet.add(pluginMetadata.getPluginId()); nodes.add(new InstalledPluginNode(pluginMetadata)); } - for (PluginContext plugin : collectedPlugins) { + for (PluginContext plugin : collectedPlugins.getList()) { if (!installedSet.contains(plugin.getPluginId())) { nodes.add(new LoadedPluginNode(plugin)); } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/plugins/CloseablePlugins.java b/jadx-gui/src/main/java/jadx/gui/utils/plugins/CloseablePlugins.java new file mode 100644 index 000000000..3fe54dcf3 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/plugins/CloseablePlugins.java @@ -0,0 +1,31 @@ +package jadx.gui.utils.plugins; + +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.plugins.PluginContext; + +public class CloseablePlugins { + private final List list; + private final @Nullable Runnable closeable; + + public CloseablePlugins(List list, @Nullable Runnable closeable) { + this.list = list; + this.closeable = closeable; + } + + public void close() { + if (closeable != null) { + closeable.run(); + } + } + + public @Nullable Runnable getCloseable() { + return closeable; + } + + public List getList() { + return list; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/plugins/CollectPlugins.java b/jadx-gui/src/main/java/jadx/gui/utils/plugins/CollectPlugins.java index c25ff29bf..edefbb3c2 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/plugins/CollectPlugins.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/plugins/CollectPlugins.java @@ -1,7 +1,6 @@ package jadx.gui.utils.plugins; import java.util.ArrayList; -import java.util.List; import java.util.SortedSet; import java.util.TreeSet; @@ -27,12 +26,13 @@ public class CollectPlugins { this.mainWindow = mainWindow; } - public List build() { + public CloseablePlugins build() { SortedSet allPlugins = new TreeSet<>(); mainWindow.getWrapper().getCurrentDecompiler() .ifPresent(decompiler -> allPlugins.addAll(decompiler.getPluginManager().getResolvedPluginContexts())); // collect and init not loaded plugins in new temp context + Runnable closeable = null; JadxArgs jadxArgs = mainWindow.getSettings().toJadxArgs(); try (JadxDecompiler decompiler = new JadxDecompiler(jadxArgs)) { JadxPluginManager pluginManager = decompiler.getPluginManager(); @@ -52,8 +52,9 @@ public class CollectPlugins { if (!missingPlugins.isEmpty()) { pluginManager.init(missingPlugins); allPlugins.addAll(missingPlugins); + closeable = () -> pluginManager.unload(missingPlugins); } } - return new ArrayList<>(allPlugins); + return new CloseablePlugins(new ArrayList<>(allPlugins), closeable); } } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/plugins/SettingsGroupPluginWrap.java b/jadx-gui/src/main/java/jadx/gui/utils/plugins/SettingsGroupPluginWrap.java new file mode 100644 index 000000000..bc16b35cd --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/plugins/SettingsGroupPluginWrap.java @@ -0,0 +1,63 @@ +package jadx.gui.utils.plugins; + +import java.util.Collections; +import java.util.List; + +import javax.swing.JComponent; +import javax.swing.JLabel; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.plugins.gui.ISettingsGroup; + +public class SettingsGroupPluginWrap implements ISettingsGroup { + private static final Logger LOG = LoggerFactory.getLogger(SettingsGroupPluginWrap.class); + + private final String pluginId; + private final ISettingsGroup pluginSettingGroup; + + public SettingsGroupPluginWrap(String pluginId, ISettingsGroup pluginSettingGroup) { + this.pluginId = pluginId; + this.pluginSettingGroup = pluginSettingGroup; + } + + @Override + public String getTitle() { + try { + return pluginSettingGroup.getTitle(); + } catch (Throwable t) { + LOG.warn("Failed to get settings group title for plugin: {}", pluginId, t); + return ""; + } + } + + @Override + public JComponent buildComponent() { + try { + return pluginSettingGroup.buildComponent(); + } catch (Throwable t) { + LOG.warn("Failed to build settings group component for plugin: {}", pluginId, t); + return new JLabel(""); + } + } + + @Override + public List getSubGroups() { + try { + return pluginSettingGroup.getSubGroups(); + } catch (Throwable t) { + LOG.warn("Failed to get settings group sub-groups for plugin: {}", pluginId, t); + return Collections.emptyList(); + } + } + + @Override + public void close(boolean save) { + try { + pluginSettingGroup.close(save); + } catch (Throwable t) { + LOG.warn("Failed to close settings group for plugin: {}", pluginId, t); + } + } +}