mirror of
https://github.com/skylot/jadx.git
synced 2024-10-07 01:53:34 +00:00
fix(gui): prevent UI stuck on class load (#2259)
This commit is contained in:
parent
f5307636ef
commit
2df69bbfb4
@ -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<Runnable> jobs;
|
||||
private final Consumer<TaskStatus> onFinish;
|
||||
private final @Nullable Consumer<TaskStatus> 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<Runnable> jobs) {
|
||||
this(title, jobs, null);
|
||||
@ -31,6 +40,14 @@ public class SimpleTask implements IBackgroundTask {
|
||||
return title;
|
||||
}
|
||||
|
||||
public List<Runnable> getJobs() {
|
||||
return jobs;
|
||||
}
|
||||
|
||||
public @Nullable Consumer<TaskStatus> getOnFinish() {
|
||||
return onFinish;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ITaskExecutor scheduleTasks() {
|
||||
TaskExecutor executor = new TaskExecutor();
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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<Path> paths) {
|
||||
open(paths, EMPTY_RUNNABLE);
|
||||
open(paths, UiUtils.EMPTY_RUNNABLE);
|
||||
}
|
||||
|
||||
private void open(List<Path> 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<TaskStatus> 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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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() {
|
||||
}
|
||||
|
||||
|
120
jadx-gui/src/main/java/jadx/gui/utils/dbg/UIWatchDog.java
Normal file
120
jadx-gui/src/main/java/jadx/gui/utils/dbg/UIWatchDog.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 -> {
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user