diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/SimpleTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/SimpleTask.java index 9acbe073e..dc57f1262 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/SimpleTask.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/SimpleTask.java @@ -1,5 +1,6 @@ package jadx.gui.jobs; +import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -14,7 +15,15 @@ import jadx.core.utils.tasks.TaskExecutor; public class SimpleTask implements IBackgroundTask { private final String title; private final List jobs; - private final Consumer onFinish; + private final @Nullable Consumer onFinish; + + public SimpleTask(String title, Runnable run) { + this(title, Collections.singletonList(run), null); + } + + public SimpleTask(String title, Runnable run, Runnable onFinish) { + this(title, Collections.singletonList(run), s -> onFinish.run()); + } public SimpleTask(String title, List jobs) { this(title, jobs, null); @@ -31,6 +40,14 @@ public class SimpleTask implements IBackgroundTask { return title; } + public List getJobs() { + return jobs; + } + + public @Nullable Consumer getOnFinish() { + return onFinish; + } + @Override public ITaskExecutor scheduleTasks() { TaskExecutor executor = new TaskExecutor(); diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java index c157bbc87..5e1fb2642 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java @@ -22,6 +22,7 @@ import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.nodes.ICodeNode; +import jadx.gui.jobs.SimpleTask; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.ClassCodeContentPanel; import jadx.gui.ui.panel.ContentPanel; @@ -57,14 +58,22 @@ public class JClass extends JLoadableNode implements JRenameNode { return cls; } + @Override + public boolean canRename() { + return !cls.getClassNode().contains(AFlag.DONT_RENAME); + } + @Override public void loadNode() { getRootClass().load(); } @Override - public boolean canRename() { - return !cls.getClassNode().contains(AFlag.DONT_RENAME); + public SimpleTask getLoadTask() { + JClass rootClass = getRootClass(); + return new SimpleTask(NLS.str("progress.decompile"), + () -> rootClass.getCls().decompile(), + rootClass::load); } private synchronized void load() { diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JLoadableNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JLoadableNode.java index eccc12231..5f506f7e7 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JLoadableNode.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JLoadableNode.java @@ -1,7 +1,11 @@ package jadx.gui.treemodel; +import jadx.gui.jobs.IBackgroundTask; + public abstract class JLoadableNode extends JNode { private static final long serialVersionUID = 5543590584166374958L; public abstract void loadNode(); + + public abstract IBackgroundTask getLoadTask(); } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java index 8f115aa3a..7925c4322 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java @@ -20,6 +20,8 @@ import jadx.api.impl.SimpleCodeInfo; import jadx.core.utils.ListUtils; import jadx.core.utils.Utils; import jadx.core.xmlgen.ResContainer; +import jadx.gui.jobs.IBackgroundTask; +import jadx.gui.jobs.SimpleTask; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.BinaryContentPanel; import jadx.gui.ui.codearea.CodeContentPanel; @@ -98,6 +100,11 @@ public class JResource extends JLoadableNode { update(); } + @Override + public synchronized IBackgroundTask getLoadTask() { + return new SimpleTask(NLS.str("progress.load"), this::getCodeInfo, this::update); + } + @Override public String getName() { return name; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index bebab60eb..9f4a2acf6 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -155,13 +155,12 @@ import jadx.gui.utils.LafManager; import jadx.gui.utils.Link; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; +import jadx.gui.utils.dbg.UIWatchDog; import jadx.gui.utils.fileswatcher.LiveReloadWorker; import jadx.gui.utils.shortcut.ShortcutsController; import jadx.gui.utils.ui.ActionHandler; import jadx.gui.utils.ui.NodeLabel; -import static io.reactivex.internal.functions.Functions.EMPTY_RUNNABLE; - public class MainWindow extends JFrame { private static final Logger LOG = LoggerFactory.getLogger(MainWindow.class); @@ -455,11 +454,11 @@ public class MainWindow extends JFrame { } public void open(Path path) { - open(Collections.singletonList(path), EMPTY_RUNNABLE); + open(Collections.singletonList(path), UiUtils.EMPTY_RUNNABLE); } public void open(List paths) { - open(paths, EMPTY_RUNNABLE); + open(paths, UiUtils.EMPTY_RUNNABLE); } private void open(List paths, Runnable onFinish) { @@ -501,7 +500,7 @@ public class MainWindow extends JFrame { synchronized (ReloadProject.EVENT) { saveAll(); closeAll(); - loadFiles(EMPTY_RUNNABLE); + loadFiles(UiUtils.EMPTY_RUNNABLE); menuBar.reloadShortcuts(); } @@ -1166,6 +1165,8 @@ public class MainWindow extends JFrame { ExceptionDialog.throwTestException(); } }); + help.add(new JCheckBoxMenuItem(new ActionHandler("UI WatchDog", UIWatchDog::toggle))); + UIWatchDog.onStart(); } help.add(aboutAction); @@ -1320,11 +1321,21 @@ public class MainWindow extends JFrame { TreePath path = event.getPath(); Object node = path.getLastPathComponent(); if (node instanceof JLoadableNode) { - ((JLoadableNode) node).loadNode(); - } - if (!treeReloading) { - project.addTreeExpansion(getPathExpansion(event.getPath())); - update(); + JLoadableNode treeNode = (JLoadableNode) node; + backgroundExecutor.execute(treeNode.getLoadTask()); + // schedule update for expanded nodes in a tree + backgroundExecutor.execute(NLS.str("progress.load"), + UiUtils.EMPTY_RUNNABLE, + status -> { + if (!treeReloading) { + treeModel.nodeStructureChanged(treeNode); + project.addTreeExpansion(getPathExpansion(event.getPath())); + } + }); + } else { + if (!treeReloading) { + project.addTreeExpansion(getPathExpansion(event.getPath())); + } } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/tab/TabsController.java b/jadx-gui/src/main/java/jadx/gui/ui/tab/TabsController.java index 753139910..fdfc13e96 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/tab/TabsController.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/TabsController.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; @@ -15,6 +16,8 @@ import jadx.api.JavaClass; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.annotations.NodeDeclareRef; +import jadx.gui.jobs.SimpleTask; +import jadx.gui.jobs.TaskStatus; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; @@ -90,22 +93,7 @@ public class TabsController { JavaClass codeParent = cls.getTopParentClass(); if (!Objects.equals(codeParent, origTopCls)) { JClass jumpCls = mainWindow.getCacheObject().getNodeCache().makeFrom(codeParent); - mainWindow.getBackgroundExecutor().execute( - NLS.str("progress.load"), - jumpCls::loadNode, // load code in background - status -> { - // search original node in jump class - codeParent.getCodeInfo().getCodeMetadata().searchDown(0, (pos, ann) -> { - if (ann.getAnnType() == ICodeAnnotation.AnnType.DECLARATION) { - ICodeNodeRef declNode = ((NodeDeclareRef) ann).getNode(); - if (declNode.equals(node.getJavaNode().getCodeNodeRef())) { - codeJump(new JumpPosition(jumpCls, pos)); - return true; - } - } - return null; - }); - }); + loadCodeWithUIAction(jumpCls, () -> jumpToInnerClass(node, codeParent, jumpCls)); return; } } @@ -120,6 +108,38 @@ public class TabsController { NLS.str("progress.load"), () -> node.getRootClass().getCodeInfo(), // run heavy loading in background status -> codeJump(new JumpPosition(node))); + + loadCodeWithUIAction(node.getRootClass(), () -> codeJump(new JumpPosition(node))); + } + + private void loadCodeWithUIAction(JClass cls, Runnable action) { + SimpleTask loadTask = cls.getLoadTask(); + mainWindow.getBackgroundExecutor().execute( + new SimpleTask(loadTask.getTitle(), + loadTask.getJobs(), + status -> { + Consumer onFinish = loadTask.getOnFinish(); + if (onFinish != null) { + onFinish.accept(status); + } + action.run(); + })); + } + + /** + * Search and jump to original node in jumpCls + */ + private void jumpToInnerClass(JNode node, JavaClass codeParent, JClass jumpCls) { + codeParent.getCodeInfo().getCodeMetadata().searchDown(0, (pos, ann) -> { + if (ann.getAnnType() == ICodeAnnotation.AnnType.DECLARATION) { + ICodeNodeRef declNode = ((NodeDeclareRef) ann).getNode(); + if (declNode.equals(node.getJavaNode().getCodeNodeRef())) { + codeJump(new JumpPosition(jumpCls, pos)); + return true; + } + } + return null; + }); } /** diff --git a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java index 8d25b928d..80b9881d7 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java @@ -63,6 +63,17 @@ public class UiUtils { */ public static final long MIN_FREE_MEMORY = calculateMinFreeMemory(); + public static final Runnable EMPTY_RUNNABLE = new Runnable() { + @Override + public void run() { + } + + @Override + public String toString() { + return "EMPTY_RUNNABLE"; + } + }; + private UiUtils() { } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/dbg/UIWatchDog.java b/jadx-gui/src/main/java/jadx/gui/utils/dbg/UIWatchDog.java new file mode 100644 index 000000000..e06784dba --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/dbg/UIWatchDog.java @@ -0,0 +1,120 @@ +package jadx.gui.utils.dbg; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.swing.SwingUtilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.gui.utils.UiUtils; + +/** + * Watch for UI thread state, if it stuck log a warning with stacktrace + */ +public class UIWatchDog { + private static final Logger LOG = LoggerFactory.getLogger(UIWatchDog.class); + + private static final boolean RUN_ON_START = false; + + private static final int UI_MAX_DELAY_MS = 1000; + private static final int CHECK_INTERVAL_MS = 100; + + public static void onStart() { + if (RUN_ON_START) { + UiUtils.uiRun(UIWatchDog::toggle); + } + } + + public static synchronized void toggle() { + if (SwingUtilities.isEventDispatchThread()) { + INSTANCE.toggleState(Thread.currentThread()); + } else { + throw new JadxRuntimeException("This method should be called in UI thread"); + } + } + + private static final UIWatchDog INSTANCE = new UIWatchDog(); + + private final AtomicBoolean enabled = new AtomicBoolean(false); + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private Future taskFuture; + + private UIWatchDog() { + // singleton + } + + private void toggleState(Thread uiThread) { + if (enabled.get()) { + // stop + enabled.set(false); + if (taskFuture != null) { + try { + taskFuture.get(CHECK_INTERVAL_MS * 5, TimeUnit.MILLISECONDS); + } catch (Throwable e) { + LOG.warn("Stopping UI watchdog error", e); + } + } + } else { + // start + enabled.set(true); + taskFuture = executor.submit(() -> start(uiThread)); + } + } + + @SuppressWarnings("BusyWait") + private void start(Thread uiThread) { + LOG.debug("UI watchdog started"); + try { + Exception e = new JadxRuntimeException("at"); + TimeMeasure tm = new TimeMeasure(); + boolean stuck = false; + long reportTime = 0; + while (enabled.get()) { + if (uiThread.getState() == Thread.State.TIMED_WAITING) { + if (!stuck) { + tm.start(); + stuck = true; + reportTime = UI_MAX_DELAY_MS; + } else { + tm.end(); + long time = tm.getTime(); + if (time > reportTime) { + e.setStackTrace(uiThread.getStackTrace()); + LOG.warn("UI events thread stuck for {}ms", time, e); + reportTime += UI_MAX_DELAY_MS; + } + } + } else { + stuck = false; + } + Thread.sleep(CHECK_INTERVAL_MS); + } + } catch (Throwable e) { + LOG.error("UI watchdog fail", e); + } + LOG.debug("UI watchdog stopped"); + } + + private static final class TimeMeasure { + private long start; + private long end; + + public void start() { + start = System.currentTimeMillis(); + } + + public void end() { + end = System.currentTimeMillis(); + } + + public long getTime() { + return end - start; + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java b/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java index 763c61da6..bb39aab52 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java @@ -24,6 +24,11 @@ public class ActionHandler extends AbstractAction { this.consumer = consumer; } + public ActionHandler(String name, Runnable action) { + this(action); + setName(name); + } + public ActionHandler() { this.consumer = ev -> { };