fix(gui): try to prevent jadx node leaks in UI objects

This commit is contained in:
Skylot
2022-06-03 16:15:14 +01:00
parent d6c851eed4
commit 0c3afcc24c
13 changed files with 149 additions and 34 deletions
@@ -212,8 +212,15 @@ public class BackgroundExecutor {
// force termination
task.cancel();
executor.shutdown();
boolean complete = executor.awaitTermination(5, TimeUnit.SECONDS);
LOG.debug("Task cancel complete: {}", complete ? "success" : "aborted");
if (executor.awaitTermination(5, TimeUnit.SECONDS)) {
LOG.debug("Task cancel complete");
return;
}
LOG.debug("Forcing tasks cancel");
executor.shutdownNow();
boolean complete = executor.awaitTermination(30, TimeUnit.SECONDS);
LOG.debug("Forced task cancel status: {}",
complete ? "success" : "fail, still active: " + executor.getActiveCount());
}
private Supplier<TaskStatus> buildCancelCheck(long startTime) {
@@ -309,6 +309,7 @@ public class TabbedPane extends JTabbedPane {
public void closeCodePanel(ContentPanel contentPanel) {
openTabs.remove(contentPanel.getNode());
remove(contentPanel);
contentPanel.dispose();
}
@Nullable
@@ -1,5 +1,6 @@
package jadx.gui.ui.codearea;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
@@ -8,15 +9,20 @@ import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.DefaultCaret;
@@ -64,8 +70,8 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
}
}
protected final ContentPanel contentPanel;
protected final JNode node;
protected ContentPanel contentPanel;
protected JNode node;
public AbstractCodeArea(ContentPanel contentPanel, JNode node) {
this.contentPanel = contentPanel;
@@ -348,4 +354,33 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
}
return null;
}
public void dispose() {
// code area reference can still be used somewhere in UI objects,
// reset node reference to allow to GC jadx objects tree
node = null;
contentPanel = null;
// also clear internals
setLinkGenerator(null);
for (MouseListener mouseListener : getMouseListeners()) {
removeMouseListener(mouseListener);
}
for (MouseMotionListener mouseMotionListener : getMouseMotionListeners()) {
removeMouseMotionListener(mouseMotionListener);
}
JPopupMenu popupMenu = getPopupMenu();
for (PopupMenuListener popupMenuListener : popupMenu.getPopupMenuListeners()) {
popupMenu.removePopupMenuListener(popupMenuListener);
}
for (Component component : popupMenu.getComponents()) {
if (component instanceof JMenuItem) {
Action action = ((JMenuItem) component).getAction();
if (action instanceof JNodeAction) {
((JNodeAction) action).dispose();
}
}
}
popupMenu.removeAll();
}
}
@@ -1,6 +1,7 @@
package jadx.gui.ui.codearea;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
@@ -170,4 +171,16 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem
LOG.debug("Failed to restore caret position: {}", viewState.getCaretPos(), e);
}
}
@Override
public void dispose() {
javaCodePanel.dispose();
smaliCodePanel.dispose();
for (Component component : areaTabbedPane.getComponents()) {
if (component instanceof CodePanel) {
((CodePanel) component).dispose();
}
}
super.dispose();
}
}
@@ -282,4 +282,10 @@ public final class CodeArea extends AbstractCodeArea {
public JadxProject getProject() {
return getMainWindow().getProject();
}
@Override
public void dispose() {
super.dispose();
cachedCodeInfo = null;
}
}
@@ -62,4 +62,9 @@ public final class CodeContentPanel extends AbstractCodeContentPanel implements
codePanel.getCodeScrollPane().getViewport().setViewPosition(viewState.getViewPoint());
codePanel.getCodeArea().setCaretPosition(viewState.getCaretPos());
}
@Override
public void dispose() {
codePanel.dispose();
}
}
@@ -195,4 +195,8 @@ public class CodePanel extends JPanel {
return this.codeArea.getContentPanel().getTabbedPane()
.getMainWindow().getSettings();
}
public void dispose() {
codeArea.dispose();
}
}
@@ -11,7 +11,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.api.JavaClass;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.api.data.ICodeComment;
@@ -26,7 +25,6 @@ import jadx.api.metadata.annotations.InsnCodeOffset;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.gui.JadxWrapper;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.dialog.CommentDialog;
import jadx.gui.utils.DefaultPopupMenuListener;
import jadx.gui.utils.NLS;
@@ -39,28 +37,29 @@ public class CommentAction extends AbstractAction implements DefaultPopupMenuLis
private static final Logger LOG = LoggerFactory.getLogger(CommentAction.class);
private final CodeArea codeArea;
private final JavaClass topCls;
private final boolean enabled;
private ICodeComment actionComment;
public CommentAction(CodeArea codeArea) {
super(NLS.str("popup.add_comment") + " (;)");
this.codeArea = codeArea;
JNode topNode = codeArea.getNode();
if (topNode instanceof JClass) {
this.topCls = ((JClass) topNode).getCls();
} else {
this.topCls = null;
this.enabled = codeArea.getNode() instanceof JClass;
if (enabled) {
UiUtils.addKeyBinding(codeArea, getKeyStroke(KeyEvent.VK_SEMICOLON, 0), "popup.add_comment",
() -> showCommentDialog(getCommentRef(codeArea.getCaretPosition())));
}
UiUtils.addKeyBinding(codeArea, getKeyStroke(KeyEvent.VK_SEMICOLON, 0), "popup.add_comment",
() -> showCommentDialog(getCommentRef(codeArea.getCaretPosition())));
}
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
ICodeComment codeComment = getCommentRef(UiUtils.getOffsetAtMousePosition(codeArea));
setEnabled(codeComment != null);
this.actionComment = codeComment;
if (enabled) {
ICodeComment codeComment = getCommentRef(UiUtils.getOffsetAtMousePosition(codeArea));
setEnabled(codeComment != null);
this.actionComment = codeComment;
} else {
setEnabled(false);
}
}
@Override
@@ -83,7 +82,7 @@ public class CommentAction extends AbstractAction implements DefaultPopupMenuLis
*/
@Nullable
private ICodeComment getCommentRef(int pos) {
if (pos == -1 || this.topCls == null) {
if (pos == -1) {
return null;
}
try {
@@ -1,6 +1,7 @@
package jadx.gui.ui.codearea;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import javax.swing.AbstractAction;
import javax.swing.KeyStroke;
@@ -16,7 +17,7 @@ import jadx.gui.utils.UiUtils;
public abstract class JNodeAction extends AbstractAction {
private static final long serialVersionUID = -2600154727884853550L;
private final transient CodeArea codeArea;
private transient CodeArea codeArea;
private transient @Nullable JNode node;
public JNodeAction(String name, CodeArea codeArea) {
@@ -26,7 +27,7 @@ public abstract class JNodeAction extends AbstractAction {
public abstract void runAction(JNode node);
public boolean isActionEnabled(JNode node) {
public boolean isActionEnabled(@Nullable JNode node) {
return node != null;
}
@@ -47,7 +48,7 @@ public abstract class JNodeAction extends AbstractAction {
runAction(node);
}
public void changeNode(JNode node) {
public void changeNode(@Nullable JNode node) {
this.node = node;
setEnabled(isActionEnabled(node));
}
@@ -55,4 +56,12 @@ public abstract class JNodeAction extends AbstractAction {
public CodeArea getCodeArea() {
return codeArea;
}
public void dispose() {
node = null;
codeArea = null;
for (PropertyChangeListener changeListener : getPropertyChangeListeners()) {
removePropertyChangeListener(changeListener);
}
}
}
@@ -4,11 +4,11 @@ import java.util.ArrayList;
import java.util.List;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import jadx.gui.treemodel.JNode;
import jadx.gui.utils.DefaultPopupMenuListener;
public final class JNodePopupListener implements DefaultPopupMenuListener {
public final class JNodePopupListener implements PopupMenuListener {
private final CodeArea codeArea;
private final List<JNodeAction> actions = new ArrayList<>();
@@ -16,13 +16,28 @@ public final class JNodePopupListener implements DefaultPopupMenuListener {
this.codeArea = codeArea;
}
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
JNode node = codeArea.getNodeUnderMouse();
actions.forEach(action -> action.changeNode(node));
}
public void addActions(JNodeAction action) {
actions.add(action);
}
private void updateNode(JNode node) {
for (JNodeAction action : actions) {
action.changeNode(node);
}
}
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
updateNode(codeArea.getNodeUnderMouse());
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
updateNode(null);
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
updateNode(null);
}
}
@@ -143,12 +143,18 @@ public class SearchDialog extends CommonSearchDialog {
@Override
public void dispose() {
stopSearchTask();
if (searchDisposable != null && !searchDisposable.isDisposed()) {
searchDisposable.dispose();
}
resultsModel.clear();
removeActiveTabListener();
if (searchTask != null) {
searchTask.cancel();
mainWindow.getBackgroundExecutor().execute(NLS.str("progress.load"), () -> {
stopSearchTask();
unloadTempData();
});
}
super.dispose();
}
@@ -477,9 +483,15 @@ public class SearchDialog extends CommonSearchDialog {
loadAllButton.setEnabled(!complete);
loadMoreButton.setEnabled(!complete);
updateProgressLabel(complete);
unloadTempData();
progressFinishedCommon();
}
private void unloadTempData() {
mainWindow.getWrapper().unloadClasses();
System.gc();
}
private static Flowable<String> onTextFieldChanges(final JTextField textField) {
return Flowable.<String>create(emitter -> {
DocumentListener listener = new DocumentListener() {
@@ -12,8 +12,8 @@ public abstract class ContentPanel extends JPanel {
private static final long serialVersionUID = 3237031760631677822L;
protected final TabbedPane tabbedPane;
protected final JNode node;
protected TabbedPane tabbedPane;
protected JNode node;
protected ContentPanel(TabbedPane panel, JNode jnode) {
tabbedPane = panel;
@@ -44,4 +44,9 @@ public abstract class ContentPanel extends JPanel {
}
return node.getName();
}
public void dispose() {
tabbedPane = null;
node = null;
}
}
@@ -109,13 +109,17 @@ public class SummaryNode extends JNode {
private void writeDecompilationSummary(StringEscapeUtils.Builder builder) {
builder.append("<h2>Decompilation</h2>");
List<ClassNode> classes = wrapper.getRootNode().getClasses(false);
List<ClassNode> classes = wrapper.getRootNode().getClassesWithoutInner();
int classesCount = classes.size();
long notLoadedClasses = classes.stream().filter(c -> c.getState() == ProcessState.NOT_LOADED).count();
long loadedClasses = classes.stream().filter(c -> c.getState() == ProcessState.LOADED).count();
long processedClasses = classes.stream().filter(c -> c.getState() == ProcessState.PROCESS_COMPLETE).count();
long generatedClasses = classes.stream().filter(c -> c.getState() == ProcessState.GENERATED_AND_UNLOADED).count();
builder.append("<ul>");
builder.append("<li>Top level classes: " + classesCount + "</li>");
builder.append("<li>At process stage: " + valueAndPercent(processedClasses, classesCount) + "</li>");
builder.append("<li>Not loaded: " + valueAndPercent(notLoadedClasses, classesCount) + "</li>");
builder.append("<li>Loaded: " + valueAndPercent(loadedClasses, classesCount) + "</li>");
builder.append("<li>Processed: " + valueAndPercent(processedClasses, classesCount) + "</li>");
builder.append("<li>Code generated: " + valueAndPercent(generatedClasses, classesCount) + "</li>");
builder.append("</ul>");