From 9c252fb2268e808c1c0bcf4a89818c4286d22d76 Mon Sep 17 00:00:00 2001 From: Skylot Date: Fri, 28 May 2021 16:23:28 +0100 Subject: [PATCH] fix(gui): add memory and time limits for decompile task (#1181) --- .../jadx/gui/jobs/BackgroundExecutor.java | 142 +++++++++++------- .../java/jadx/gui/jobs/BackgroundJob.java | 87 ----------- .../java/jadx/gui/jobs/BackgroundWorker.java | 100 ------------ .../main/java/jadx/gui/jobs/DecompileJob.java | 24 --- .../java/jadx/gui/jobs/DecompileTask.java | 113 ++++++++++++++ .../java/jadx/gui/jobs/IBackgroundTask.java | 20 ++- .../jobs/{IndexJob.java => IndexService.java} | 47 +++--- .../main/java/jadx/gui/jobs/TaskStatus.java | 10 ++ .../java/jadx/gui/ui/CommonSearchDialog.java | 25 +-- .../src/main/java/jadx/gui/ui/MainWindow.java | 68 +++++---- .../main/java/jadx/gui/ui/RenameDialog.java | 3 +- .../main/java/jadx/gui/ui/SearchDialog.java | 2 +- .../java/jadx/gui/ui/codearea/CodeArea.java | 3 +- .../main/java/jadx/gui/utils/CacheObject.java | 25 +-- .../java/jadx/gui/utils/JumpPosition.java | 6 +- .../src/main/java/jadx/gui/utils/UiUtils.java | 8 + .../resources/i18n/Messages_de_DE.properties | 5 +- .../resources/i18n/Messages_en_US.properties | 5 +- .../resources/i18n/Messages_es_ES.properties | 5 +- .../resources/i18n/Messages_ko_KR.properties | 5 +- .../resources/i18n/Messages_zh_CN.properties | 5 +- 21 files changed, 332 insertions(+), 376 deletions(-) delete mode 100644 jadx-gui/src/main/java/jadx/gui/jobs/BackgroundJob.java delete mode 100644 jadx-gui/src/main/java/jadx/gui/jobs/BackgroundWorker.java delete mode 100644 jadx-gui/src/main/java/jadx/gui/jobs/DecompileJob.java create mode 100644 jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java rename jadx-gui/src/main/java/jadx/gui/jobs/{IndexJob.java => IndexService.java} (66%) create mode 100644 jadx-gui/src/main/java/jadx/gui/jobs/TaskStatus.java diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java index 8510de5f6..b7ca3c6de 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java @@ -6,8 +6,9 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.function.BooleanSupplier; -import javax.swing.*; +import javax.swing.SwingWorker; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -15,6 +16,7 @@ import org.slf4j.LoggerFactory; import jadx.gui.ui.MainWindow; import jadx.gui.ui.ProgressPanel; +import jadx.gui.utils.UiUtils; /** * Class for run tasks in background with progress bar indication. @@ -34,7 +36,7 @@ public class BackgroundExecutor { this.taskQueueExecutor = makeTaskQueueExecutor(); } - public Future execute(IBackgroundTask task) { + public Future execute(IBackgroundTask task) { TaskWorker taskWorker = new TaskWorker(task); taskQueueExecutor.execute(() -> { taskWorker.init(); @@ -46,7 +48,8 @@ public class BackgroundExecutor { public void cancelAll() { try { taskQueueExecutor.shutdownNow(); - taskQueueExecutor.awaitTermination(1, TimeUnit.SECONDS); + boolean complete = taskQueueExecutor.awaitTermination(2, TimeUnit.SECONDS); + LOG.debug("Background task executor terminated with status: {}", complete ? "complete" : "interrupted"); } catch (Exception e) { LOG.error("Error terminating task executor", e); } finally { @@ -58,25 +61,18 @@ public class BackgroundExecutor { execute(new SimpleTask(title, backgroundJobs, onFinishUiRunnable)); } - public void execute(String title, List backgroundJobs) { - execute(new SimpleTask(title, backgroundJobs, null)); - } - public void execute(String title, Runnable backgroundRunnable, Runnable onFinishUiRunnable) { execute(new SimpleTask(title, backgroundRunnable, onFinishUiRunnable)); } - public void execute(String title, Runnable backgroundRunnable) { - execute(new SimpleTask(title, backgroundRunnable, null)); - } - private ThreadPoolExecutor makeTaskQueueExecutor() { return (ThreadPoolExecutor) Executors.newFixedThreadPool(1); } - private final class TaskWorker extends SwingWorker { + private final class TaskWorker extends SwingWorker { private final IBackgroundTask task; private long jobsCount; + private TaskStatus status = TaskStatus.WAIT; public TaskWorker(IBackgroundTask task) { this.task = task; @@ -88,39 +84,24 @@ public class BackgroundExecutor { } @Override - protected Boolean doInBackground() throws Exception { + protected TaskStatus doInBackground() throws Exception { progressPane.changeLabel(this, task.getTitle() + "… "); progressPane.changeCancelBtnVisible(this, task.canBeCanceled()); progressPane.changeVisibility(this, true); + if (runJobs()) { + status = TaskStatus.COMPLETE; + } + return status; + } + + private boolean runJobs() throws InterruptedException { List jobs = task.scheduleJobs(); jobsCount = jobs.size(); - LOG.debug("Starting background task '{}', jobs count: {}", task.getTitle(), jobsCount); - if (jobsCount == 1) { - jobs.get(0).run(); - return true; - } + LOG.debug("Starting background task '{}', jobs count: {}, time limit: {} ms, memory check: {}", + task.getTitle(), jobsCount, task.timeLimit(), task.checkMemoryUsage()); + status = TaskStatus.STARTED; int threadsCount = mainWindow.getSettings().getThreadsCount(); - if (threadsCount == 1) { - return runInCurrentThread(jobs); - } - return runInExecutor(jobs, threadsCount); - } - - private boolean runInCurrentThread(List jobs) { - int k = 0; - for (Runnable job : jobs) { - job.run(); - k++; - setProgress(calcProgress(k)); - if (isCancelled()) { - return false; - } - } - return true; - } - - private boolean runInExecutor(List jobs, int threadsCount) throws InterruptedException { ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadsCount); for (Runnable job : jobs) { executor.execute(job); @@ -129,22 +110,74 @@ public class BackgroundExecutor { return waitTermination(executor); } + @SuppressWarnings("BusyWait") private boolean waitTermination(ThreadPoolExecutor executor) throws InterruptedException { - while (true) { - if (executor.isTerminated()) { + BooleanSupplier cancelCheck = buildCancelCheck(); + try { + while (true) { + if (executor.isTerminated()) { + return true; + } + if (cancelCheck.getAsBoolean()) { + performCancel(executor); + return false; + } + setProgress(calcProgress(executor.getCompletedTaskCount())); + Thread.sleep(1000); + } + } catch (InterruptedException e) { + LOG.debug("Task wait interrupted"); + status = TaskStatus.CANCEL_BY_USER; + performCancel(executor); + return false; + } catch (Exception e) { + LOG.error("Task wait aborted by exception", e); + performCancel(executor); + return false; + } + } + + private void performCancel(ThreadPoolExecutor executor) throws InterruptedException { + progressPane.changeLabel(this, task.getTitle() + " (Canceling)… "); + progressPane.changeIndeterminate(this, true); + // force termination + executor.shutdownNow(); + boolean complete = executor.awaitTermination(5, TimeUnit.SECONDS); + LOG.debug("Task cancel complete: {}", complete); + } + + private boolean isSimpleTask() { + return task.timeLimit() == 0 && !task.checkMemoryUsage(); + } + + private boolean simpleCancelCheck() { + if (isCancelled() || Thread.currentThread().isInterrupted()) { + LOG.debug("Task '{}' canceled", task.getTitle()); + status = TaskStatus.CANCEL_BY_USER; + return true; + } + return false; + } + + private BooleanSupplier buildCancelCheck() { + if (isSimpleTask()) { + return this::simpleCancelCheck; + } + long waitUntilTime = task.timeLimit() == 0 ? 0 : System.currentTimeMillis() + task.timeLimit(); + boolean checkMemoryUsage = task.checkMemoryUsage(); + return () -> { + if (waitUntilTime != 0 && waitUntilTime < System.currentTimeMillis()) { + LOG.debug("Task '{}' execution timeout, force cancel", task.getTitle()); + status = TaskStatus.CANCEL_BY_TIMEOUT; return true; } - if (isCancelled()) { - executor.shutdownNow(); - progressPane.changeLabel(this, task.getTitle() + " (Canceling)… "); - progressPane.changeIndeterminate(this, true); - // force termination - executor.awaitTermination(5, TimeUnit.SECONDS); - return false; + if (checkMemoryUsage && !UiUtils.isFreeMemoryAvailable()) { + LOG.debug("Task '{}' memory limit reached, force cancel", task.getTitle()); + status = TaskStatus.CANCEL_BY_MEMORY; + return true; } - setProgress(calcProgress(executor.getCompletedTaskCount())); - Thread.sleep(500); - } + return simpleCancelCheck(); + }; } private int calcProgress(long done) { @@ -154,7 +187,7 @@ public class BackgroundExecutor { @Override protected void done() { progressPane.setVisible(false); - task.onFinish(); + task.onFinish(status); } } @@ -184,12 +217,7 @@ public class BackgroundExecutor { } @Override - public boolean canBeCanceled() { - return false; - } - - @Override - public void onFinish() { + public void onFinish(TaskStatus status) { if (onFinish != null) { onFinish.run(); } diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundJob.java b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundJob.java deleted file mode 100644 index cd86c3747..000000000 --- a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundJob.java +++ /dev/null @@ -1,87 +0,0 @@ -package jadx.gui.jobs; - -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jadx.gui.JadxWrapper; - -public abstract class BackgroundJob { - private static final Logger LOG = LoggerFactory.getLogger(BackgroundJob.class); - - protected final JadxWrapper wrapper; - private final ThreadPoolExecutor executor; - private Future future; - - public BackgroundJob(JadxWrapper wrapper, int threadsCount) { - this.wrapper = wrapper; - this.executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadsCount); - } - - public synchronized Future process() { - if (future != null) { - return future; - } - ExecutorService shutdownExecutor = Executors.newSingleThreadExecutor(); - FutureTask task = new ShutdownTask(); - shutdownExecutor.execute(task); - shutdownExecutor.shutdown(); - future = task; - return future; - } - - private class ShutdownTask extends FutureTask { - public ShutdownTask() { - super(new Callable() { - @Override - public Boolean call() throws Exception { - runJob(); - executor.shutdown(); - return executor.awaitTermination(5, TimeUnit.DAYS); - } - }); - } - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - executor.shutdownNow(); - return super.cancel(mayInterruptIfRunning); - } - } - - protected abstract void runJob(); - - public abstract String getInfoString(); - - protected void addTask(Runnable runnable) { - executor.execute(runnable); - } - - public void processAndWait() { - try { - process().get(); - } catch (Exception e) { - LOG.error("BackgroundJob.processAndWait failed", e); - } - } - - public synchronized boolean isComplete() { - try { - return future != null && future.isDone(); - } catch (Exception e) { - LOG.error("BackgroundJob.isComplete failed", e); - return false; - } - } - - public int getProgress() { - return (int) (executor.getCompletedTaskCount() * 100 / (double) executor.getTaskCount()); - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundWorker.java b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundWorker.java deleted file mode 100644 index 837ca7418..000000000 --- a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundWorker.java +++ /dev/null @@ -1,100 +0,0 @@ -package jadx.gui.jobs; - -import java.util.concurrent.Future; - -import javax.swing.JOptionPane; -import javax.swing.SwingUtilities; -import javax.swing.SwingWorker; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jadx.gui.ui.ProgressPanel; -import jadx.gui.utils.CacheObject; -import jadx.gui.utils.NLS; -import jadx.gui.utils.UiUtils; -import jadx.gui.utils.search.TextSearchIndex; - -/** - * Deprecated. Use {@link BackgroundExecutor} instead. - */ -public class BackgroundWorker extends SwingWorker { - private static final Logger LOG = LoggerFactory.getLogger(BackgroundWorker.class); - - private final CacheObject cache; - private final ProgressPanel progressPane; - - public BackgroundWorker(CacheObject cacheObject, ProgressPanel progressPane) { - this.cache = cacheObject; - this.progressPane = progressPane; - } - - public void exec() { - if (isDone()) { - return; - } - SwingUtilities.invokeLater(() -> progressPane.setVisible(true)); - addPropertyChangeListener(progressPane); - execute(); - } - - public void stop() { - if (isDone()) { - return; - } - LOG.debug("Canceling background jobs ..."); - cancel(false); - } - - @Override - protected Void doInBackground() { - try { - System.gc(); - LOG.debug("Memory usage: Before decompile: {}", UiUtils.memoryInfo()); - runJob(cache.getDecompileJob()); - LOG.debug("Memory usage: After decompile: {}", UiUtils.memoryInfo()); - - LOG.debug("Memory usage: Before index: {}", UiUtils.memoryInfo()); - runJob(cache.getIndexJob()); - LOG.debug("Memory usage: After index: {}", UiUtils.memoryInfo()); - - System.gc(); - LOG.debug("Memory usage: After gc: {}", UiUtils.memoryInfo()); - - TextSearchIndex searchIndex = cache.getTextIndex(); - if (searchIndex != null && searchIndex.getSkippedCount() > 0) { - LOG.warn("Indexing of some classes skipped, count: {}, low memory: {}", - searchIndex.getSkippedCount(), UiUtils.memoryInfo()); - String msg = NLS.str("message.indexingClassesSkipped", searchIndex.getSkippedCount()); - JOptionPane.showMessageDialog(null, msg); - } - } catch (Exception e) { - LOG.error("Exception in background worker", e); - } - return null; - } - - private void runJob(BackgroundJob job) { - if (isCancelled() || job == null) { - return; - } - progressPane.changeLabel(this, job.getInfoString()); - Future future = job.process(); - while (!future.isDone()) { - try { - setProgress(job.getProgress()); - if (isCancelled()) { - future.cancel(false); - } - Thread.sleep(500); - } catch (Exception e) { - LOG.error("Background worker error", e); - } - } - } - - @Override - protected void done() { - progressPane.setVisible(false); - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/DecompileJob.java b/jadx-gui/src/main/java/jadx/gui/jobs/DecompileJob.java deleted file mode 100644 index aa7230c55..000000000 --- a/jadx-gui/src/main/java/jadx/gui/jobs/DecompileJob.java +++ /dev/null @@ -1,24 +0,0 @@ -package jadx.gui.jobs; - -import jadx.api.JavaClass; -import jadx.gui.JadxWrapper; -import jadx.gui.utils.NLS; - -public class DecompileJob extends BackgroundJob { - - public DecompileJob(JadxWrapper wrapper, int threadsCount) { - super(wrapper, threadsCount); - } - - @Override - protected void runJob() { - for (final JavaClass cls : wrapper.getIncludedClasses()) { - addTask(cls::decompile); - } - } - - @Override - public String getInfoString() { - return NLS.str("progress.decompile") + "… "; - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java new file mode 100644 index 000000000..9d3bff3f9 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java @@ -0,0 +1,113 @@ +package jadx.gui.jobs; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.swing.JOptionPane; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.JavaClass; +import jadx.gui.JadxWrapper; +import jadx.gui.ui.MainWindow; +import jadx.gui.utils.NLS; +import jadx.gui.utils.UiUtils; + +public class DecompileTask implements IBackgroundTask { + private static final Logger LOG = LoggerFactory.getLogger(DecompileTask.class); + + private static final int CLS_LIMIT = Integer.parseInt(UiUtils.getEnvVar("JADX_CLS_PROCESS_LIMIT", "50")); + + private final MainWindow mainWindow; + private final JadxWrapper wrapper; + private final AtomicInteger complete = new AtomicInteger(0); + private int expectedCompleteCount; + private long startTime; + + public DecompileTask(MainWindow mainWindow, JadxWrapper wrapper) { + this.mainWindow = mainWindow; + this.wrapper = wrapper; + } + + @Override + public String getTitle() { + return NLS.str("progress.decompile"); + } + + @Override + public List scheduleJobs() { + List classes = wrapper.getIncludedClasses(); + expectedCompleteCount = classes.size(); + + IndexService indexService = mainWindow.getCacheObject().getIndexService(); + indexService.setComplete(false); + complete.set(0); + + List jobs = new ArrayList<>(expectedCompleteCount + 1); + for (JavaClass cls : classes) { + jobs.add(() -> { + cls.decompile(); + indexService.indexCls(cls); + complete.incrementAndGet(); + }); + } + jobs.add(indexService::indexResources); + startTime = System.currentTimeMillis(); + return jobs; + } + + @Override + public void onFinish(TaskStatus status) { + long taskTime = System.currentTimeMillis() - startTime; + long avgPerCls = taskTime / expectedCompleteCount; + LOG.info("Decompile task complete in {} ms (avg {} ms per class), classes: {}," + + " time limit:{ total: {}ms, per cls: {}ms }, status: {}", + taskTime, avgPerCls, expectedCompleteCount, timeLimit(), CLS_LIMIT, status); + + IndexService indexService = mainWindow.getCacheObject().getIndexService(); + indexService.setComplete(true); + + int complete = this.complete.get(); + int skipped = expectedCompleteCount - complete; + if (skipped == 0) { + return; + } + LOG.warn("Decompile and indexing of some classes skipped: {}, status: {}", skipped, status); + switch (status) { + case CANCEL_BY_USER: { + String reason = NLS.str("message.userCancelTask"); + String message = NLS.str("message.indexIncomplete", reason, skipped); + JOptionPane.showMessageDialog(mainWindow, message); + break; + } + case CANCEL_BY_TIMEOUT: { + String reason = NLS.str("message.taskTimeout", timeLimit()); + String message = NLS.str("message.indexIncomplete", reason, skipped); + JOptionPane.showMessageDialog(mainWindow, message); + break; + } + case CANCEL_BY_MEMORY: { + mainWindow.showHeapUsageBar(); + JOptionPane.showMessageDialog(mainWindow, NLS.str("message.indexingClassesSkipped", skipped)); + break; + } + } + } + + @Override + public boolean canBeCanceled() { + return true; + } + + @Override + public int timeLimit() { + return expectedCompleteCount * CLS_LIMIT + 5000; + } + + @Override + public boolean checkMemoryUsage() { + return true; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java index 5467b324f..b82f2cbdb 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java @@ -8,7 +8,23 @@ public interface IBackgroundTask { List scheduleJobs(); - void onFinish(); + void onFinish(TaskStatus status); - boolean canBeCanceled(); + default boolean canBeCanceled() { + return false; + } + + /** + * Global (for all jobs) time limit in milliseconds (0 - to disable). + */ + default int timeLimit() { + return 0; + } + + /** + * Executor will check memory usage on every tick and cancel job if no free memory available. + */ + default boolean checkMemoryUsage() { + return false; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/IndexJob.java b/jadx-gui/src/main/java/jadx/gui/jobs/IndexService.java similarity index 66% rename from jadx-gui/src/main/java/jadx/gui/jobs/IndexJob.java rename to jadx-gui/src/main/java/jadx/gui/jobs/IndexService.java index 43a2c60d8..5e1cf76a5 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/IndexJob.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/IndexService.java @@ -8,36 +8,25 @@ import org.slf4j.LoggerFactory; import jadx.api.ICodeWriter; import jadx.api.JavaClass; -import jadx.gui.JadxWrapper; import jadx.gui.utils.CacheObject; import jadx.gui.utils.CodeLinesInfo; import jadx.gui.utils.CodeUsageInfo; -import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.search.StringRef; import jadx.gui.utils.search.TextSearchIndex; -public class IndexJob extends BackgroundJob { +public class IndexService { - private static final Logger LOG = LoggerFactory.getLogger(IndexJob.class); + private static final Logger LOG = LoggerFactory.getLogger(IndexService.class); private final CacheObject cache; + private boolean indexComplete; - public IndexJob(JadxWrapper wrapper, CacheObject cache, int threadsCount) { - super(wrapper, threadsCount); + public IndexService(CacheObject cache) { this.cache = cache; } - @Override - protected void runJob() { - TextSearchIndex index = cache.getTextIndex(); - addTask(index::indexResource); - for (final JavaClass cls : wrapper.getIncludedClasses()) { - addTask(() -> indexCls(cache, cls)); - } - } - - private static void indexCls(CacheObject cache, JavaClass cls) { + public void indexCls(JavaClass cls) { try { TextSearchIndex index = cache.getTextIndex(); CodeUsageInfo usageInfo = cache.getUsageInfo(); @@ -51,17 +40,18 @@ public class IndexJob extends BackgroundJob { List lines = splitLines(cls); usageInfo.processClass(cls, linesInfo, lines); - if (UiUtils.isFreeMemoryAvailable()) { - index.indexCode(cls, linesInfo, lines); - } else { - index.classCodeIndexSkipped(cls); - } + index.indexCode(cls, linesInfo, lines); } catch (Exception e) { LOG.error("Index error in class: {}", cls.getFullName(), e); } } - public static void refreshIndex(CacheObject cache, JavaClass cls) { + public void indexResources() { + TextSearchIndex index = cache.getTextIndex(); + index.indexResource(); + } + + public void refreshIndex(JavaClass cls) { TextSearchIndex index = cache.getTextIndex(); CodeUsageInfo usageInfo = cache.getUsageInfo(); if (index == null || usageInfo == null) { @@ -69,7 +59,9 @@ public class IndexJob extends BackgroundJob { } index.remove(cls); usageInfo.remove(cls); - indexCls(cache, cls); + if (UiUtils.isFreeMemoryAvailable()) { + indexCls(cls); + } } @NotNull @@ -82,8 +74,11 @@ public class IndexJob extends BackgroundJob { return lines; } - @Override - public String getInfoString() { - return NLS.str("progress.index") + "… "; + public boolean isComplete() { + return indexComplete; + } + + public void setComplete(boolean indexComplete) { + this.indexComplete = indexComplete; } } diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/TaskStatus.java b/jadx-gui/src/main/java/jadx/gui/jobs/TaskStatus.java new file mode 100644 index 000000000..ecfbf2a66 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/jobs/TaskStatus.java @@ -0,0 +1,10 @@ +package jadx.gui.jobs; + +public enum TaskStatus { + WAIT, + STARTED, + COMPLETE, + CANCEL_BY_USER, + CANCEL_BY_TIMEOUT, + CANCEL_BY_MEMORY +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java index ea777411b..a4b189d46 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java @@ -46,9 +46,6 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jadx.gui.jobs.BackgroundJob; -import jadx.gui.jobs.BackgroundWorker; -import jadx.gui.jobs.DecompileJob; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JResSearchNode; import jadx.gui.ui.codearea.AbstractCodeArea; @@ -101,7 +98,7 @@ public abstract class CommonSearchDialog extends JDialog { } public void prepare() { - if (cache.getIndexJob().isComplete()) { + if (cache.getIndexService().isComplete()) { loadFinishedCommon(); loadFinished(); return; @@ -532,19 +529,8 @@ public abstract class CommonSearchDialog extends JDialog { @Override public Void doInBackground() { try { - BackgroundWorker backgroundWorker = mainWindow.getBackgroundWorker(); - if (backgroundWorker == null) { - return null; - } - backgroundWorker.exec(); - - DecompileJob decompileJob = cache.getDecompileJob(); - progressPane.changeLabel(this, decompileJob.getInfoString()); - decompileJob.processAndWait(); - - BackgroundJob indexJob = cache.getIndexJob(); - progressPane.changeLabel(this, indexJob.getInfoString()); - indexJob.processAndWait(); + progressPane.changeLabel(this, NLS.str("progress.decompile") + ": "); + mainWindow.waitDecompileTask(); } catch (Exception e) { LOG.error("Waiting background tasks failed", e); } @@ -553,11 +539,6 @@ public abstract class CommonSearchDialog extends JDialog { @Override public void done() { - try { - get(); - } catch (Exception e) { - LOG.error("Load task failed", e); - } loadFinishedCommon(); loadFinished(); } 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 a1d9b4e6d..9d87781ba 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -35,6 +35,9 @@ import java.util.Map; import java.util.Set; import java.util.Timer; import java.util.TimerTask; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.stream.Collectors; import javax.swing.AbstractAction; @@ -87,9 +90,9 @@ import jadx.core.utils.files.FileUtils; import jadx.gui.JadxWrapper; import jadx.gui.device.debugger.BreakpointManager; import jadx.gui.jobs.BackgroundExecutor; -import jadx.gui.jobs.BackgroundWorker; -import jadx.gui.jobs.DecompileJob; -import jadx.gui.jobs.IndexJob; +import jadx.gui.jobs.DecompileTask; +import jadx.gui.jobs.IndexService; +import jadx.gui.jobs.TaskStatus; import jadx.gui.settings.JadxProject; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.JadxSettingsWindow; @@ -154,6 +157,7 @@ public class MainWindow extends JFrame { private final transient JadxWrapper wrapper; private final transient JadxSettings settings; private final transient CacheObject cacheObject; + private final transient BackgroundExecutor backgroundExecutor; private transient JadxProject project; private transient Action newProjectAction; private transient Action saveProjectAction; @@ -177,8 +181,6 @@ public class MainWindow extends JFrame { private transient Link updateLink; private transient ProgressPanel progressPane; - private transient BackgroundWorker backgroundWorker; - private transient BackgroundExecutor backgroundExecutor; private transient Theme editorTheme; private JDebuggerPanel debuggerPanel; @@ -196,10 +198,11 @@ public class MainWindow extends JFrame { registerMouseNavigationButtons(); UiUtils.setWindowIcons(this); loadSettings(); - checkForUpdate(); - newProject(); this.backgroundExecutor = new BackgroundExecutor(this); + + checkForUpdate(); + newProject(); } public void init() { @@ -393,7 +396,7 @@ public class MainWindow extends JFrame { deobfToggleBtn.setSelected(settings.isDeobfuscationOn()); initTree(); update(); - runBackgroundJobs(); + runInitialBackgroundJobs(); BreakpointManager.init(paths.get(0).getParent()); onFinish.run(); }); @@ -469,37 +472,42 @@ public class MainWindow extends JFrame { cacheObject.setJRoot(treeRoot); cacheObject.setJadxSettings(settings); - int threadsCount = settings.getThreadsCount(); - cacheObject.setDecompileJob(new DecompileJob(wrapper, threadsCount)); - cacheObject.setIndexJob(new IndexJob(wrapper, cacheObject, threadsCount)); + cacheObject.setIndexService(new IndexService(cacheObject)); cacheObject.setUsageInfo(new CodeUsageInfo(cacheObject.getNodeCache())); cacheObject.setTextIndex(new TextSearchIndex(this)); } - synchronized void runBackgroundJobs() { - cancelBackgroundJobs(); - backgroundWorker = new BackgroundWorker(cacheObject, progressPane); + synchronized void runInitialBackgroundJobs() { if (settings.isAutoStartJobs()) { new Timer().schedule(new TimerTask() { @Override public void run() { - backgroundWorker.exec(); + waitDecompileTask(); } }, 1000); } } - public synchronized void cancelBackgroundJobs() { - if (backgroundExecutor != null) { - backgroundExecutor.cancelAll(); - } - if (backgroundWorker != null) { - backgroundWorker.stop(); - backgroundWorker = new BackgroundWorker(cacheObject, progressPane); - resetCache(); + private static final Object DECOMPILER_TASK_SYNC = new Object(); + + public void waitDecompileTask() { + synchronized (DECOMPILER_TASK_SYNC) { + try { + DecompileTask decompileTask = new DecompileTask(this, wrapper); + Future task = backgroundExecutor.execute(decompileTask); + task.get(); + } catch (Exception e) { + LOG.error("Decompile task execution failed", e); + } } } + public void cancelBackgroundJobs() { + ExecutorService worker = Executors.newSingleThreadExecutor(); + worker.execute(backgroundExecutor::cancelAll); + worker.shutdown(); + } + public void reOpenFile() { List openedFile = wrapper.getOpenPaths(); Map openTabs = storeOpenTabs(); @@ -673,7 +681,10 @@ public class MainWindow extends JFrame { } else if (obj instanceof QuarkReport) { tabbedPane.showSimpleNode((JNode) obj); } else if (obj instanceof JNode) { - tabbedPane.codeJump(new JumpPosition((JNode) obj)); + JNode node = (JNode) obj; + if (node.getRootClass() != null) { + tabbedPane.codeJump(new JumpPosition(node)); + } } } catch (Exception e) { LOG.error("Content loading error", e); @@ -1283,10 +1294,6 @@ public class MainWindow extends JFrame { return cacheObject; } - public BackgroundWorker getBackgroundWorker() { - return backgroundWorker; - } - public BackgroundExecutor getBackgroundExecutor() { return backgroundExecutor; } @@ -1314,6 +1321,11 @@ public class MainWindow extends JFrame { debuggerPanel = null; } + public void showHeapUsageBar() { + settings.setShowHeapUsageBar(true); + heapUsageBar.setVisible(true); + } + private void initDebuggerPanel() { if (debuggerPanel == null) { debuggerPanel = new JDebuggerPanel(this); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java index 419090bf3..41b683a2e 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java @@ -41,7 +41,6 @@ import jadx.core.dex.nodes.VariableNode; import jadx.core.dex.visitors.RenameVisitor; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; -import jadx.gui.jobs.IndexJob; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JField; @@ -244,7 +243,7 @@ public class RenameDialog extends JDialog { private void refreshJClass(JClass cls) { try { cls.reload(); - IndexJob.refreshIndex(cache, cls.getCls()); + cache.getIndexService().refreshIndex(cls.getCls()); } catch (Exception e) { LOG.error("Failed to reload class: {}", cls.getFullName(), e); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java index adc450495..07466c299 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java @@ -153,7 +153,7 @@ public class SearchDialog extends CommonSearchDialog { } private TextSearchIndex checkIndex() { - if (!cache.getIndexJob().isComplete()) { + if (!cache.getIndexService().isComplete()) { if (isFullIndexNeeded()) { prepare(); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java index 7697b8540..19b98c651 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java @@ -18,7 +18,6 @@ import org.slf4j.LoggerFactory; import jadx.api.CodePosition; import jadx.api.JadxDecompiler; import jadx.api.JavaNode; -import jadx.gui.jobs.IndexJob; import jadx.gui.settings.JadxProject; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; @@ -222,7 +221,7 @@ public final class CodeArea extends AbstractCodeArea { caretFix.save(); cls.reload(); - IndexJob.refreshIndex(getMainWindow().getCacheObject(), cls.getCls()); + getMainWindow().getCacheObject().getIndexService().refreshIndex(cls.getCls()); ClassCodeContentPanel codeContentPanel = (ClassCodeContentPanel) this.contentPanel; codeContentPanel.getTabbedPane().refresh(cls); diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java index 3c47b6bf3..9e7b80f79 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java @@ -6,8 +6,7 @@ import java.util.Set; import org.jetbrains.annotations.Nullable; -import jadx.gui.jobs.DecompileJob; -import jadx.gui.jobs.IndexJob; +import jadx.gui.jobs.IndexService; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JRoot; import jadx.gui.ui.SearchDialog; @@ -16,8 +15,7 @@ import jadx.gui.utils.search.TextSearchIndex; public class CacheObject { - private DecompileJob decompileJob; - private IndexJob indexJob; + private IndexService indexService; private TextSearchIndex textIndex; private CodeUsageInfo usageInfo; @@ -36,8 +34,7 @@ public class CacheObject { public void reset() { jRoot = null; settings = null; - decompileJob = null; - indexJob = null; + indexService = null; textIndex = null; lastSearch = null; jNodeCache = new JNodeCache(); @@ -45,14 +42,6 @@ public class CacheObject { lastSearchOptions = new HashMap<>(); } - public DecompileJob getDecompileJob() { - return decompileJob; - } - - public void setDecompileJob(DecompileJob decompileJob) { - this.decompileJob = decompileJob; - } - public TextSearchIndex getTextIndex() { return textIndex; } @@ -87,12 +76,12 @@ public class CacheObject { this.commentsIndex = commentsIndex; } - public IndexJob getIndexJob() { - return indexJob; + public IndexService getIndexService() { + return indexService; } - public void setIndexJob(IndexJob indexJob) { - this.indexJob = indexJob; + public void setIndexService(IndexService indexService) { + this.indexService = indexService; } public JNodeCache getNodeCache() { diff --git a/jadx-gui/src/main/java/jadx/gui/utils/JumpPosition.java b/jadx-gui/src/main/java/jadx/gui/utils/JumpPosition.java index 1f89aaf43..383a74aaa 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/JumpPosition.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/JumpPosition.java @@ -1,5 +1,7 @@ package jadx.gui.utils; +import java.util.Objects; + import jadx.api.CodePosition; import jadx.api.JavaNode; import jadx.gui.treemodel.JNode; @@ -10,11 +12,11 @@ public class JumpPosition { private int pos; public JumpPosition(JNode jumpNode) { - this(jumpNode.getRootClass(), jumpNode.getLine(), jumpNode.getPos()); + this(Objects.requireNonNull(jumpNode.getRootClass()), jumpNode.getLine(), jumpNode.getPos()); } public JumpPosition(JNode jumpNode, CodePosition codePos) { - this(jumpNode.getRootClass(), codePos.getLine(), codePos.getPos()); + this(Objects.requireNonNull(jumpNode.getRootClass()), codePos.getLine(), codePos.getPos()); } public JumpPosition(JNode node, int line, int pos) { 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 1088e4013..870467e6b 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java @@ -250,4 +250,12 @@ public class UiUtils { SwingUtilities.convertPointFromScreen(pos, comp); return pos; } + + public static String getEnvVar(String varName, String defValue) { + String envVal = System.getenv(varName); + if (envVal == null) { + return defValue; + } + return envVal; + } } diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index 032aa6d75..c85a83aae 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -36,7 +36,7 @@ tree.loading=Laden… progress.load=Laden progress.decompile=Dekompilieren -progress.index=Indizieren +#progress.index=Indizieren error_dialog.title=Fehler @@ -58,6 +58,9 @@ tabs.smali=Smali nav.back=Zurück nav.forward=Vorwärts +#message.taskTimeout=Task exceeded time limit of %d ms. +#message.userCancelTask=Task was canceled by user. +#message.indexIncomplete=Index of some classes skipped.
%s
%d classes were not indexed and will not appear in search results! message.indexingClassesSkipped=Jadx hat nur noch wenig Speicherplatz. Daher wurden %d Klassen nicht indiziert.
Wenn Sie möchten, dass alle Klassen indiziert werden, Jadx mit erhöhter maximaler Heap-Größe neustarten. heapUsage.text=JADX-Speicherauslastung: %.2f GB von %.2f GB diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index 82b116a5d..da06b442a 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -36,7 +36,7 @@ tree.loading=Loading... progress.load=Loading progress.decompile=Decompiling -progress.index=Indexing +#progress.index=Indexing error_dialog.title=Error @@ -58,6 +58,9 @@ tabs.smali=Smali nav.back=Back nav.forward=Forward +message.taskTimeout=Task exceeded time limit of %d ms. +message.userCancelTask=Task was canceled by user. +message.indexIncomplete=Index of some classes skipped.
%s
%d classes were not indexed and will not appear in search results! message.indexingClassesSkipped=Jadx is running low on memory. Therefore %d classes were not indexed.
If you want all classes to be indexed restart Jadx with increased maximum heap size. heapUsage.text=JADX memory usage: %.2f GB of %.2f GB diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index b001d87a4..a7aa1c096 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -36,7 +36,7 @@ tree.loading=Cargando... progress.load=Cargando progress.decompile=Decompiling -progress.index=Indexing +#progress.index=Indexing #error_dialog.title= @@ -58,6 +58,9 @@ tabs.closeAll=Cerrar todo nav.back=Atrás nav.forward=Adelante +#message.taskTimeout=Task exceeded time limit of %d ms. +#message.userCancelTask=Task was canceled by user. +#message.indexIncomplete=Index of some classes skipped.
%s
%d classes were not indexed and will not appear in search results! #message.indexingClassesSkipped= #heapUsage.text= diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index 195a62b86..3542a6c00 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -36,7 +36,7 @@ tree.loading=로딩중... progress.load=로딩중 progress.decompile=디컴파일 중 -progress.index=인덱싱 중 +#progress.index=인덱싱 중 error_dialog.title=오류 @@ -58,6 +58,9 @@ tabs.smali=Smali nav.back=뒤로 nav.forward=앞으로 +#message.taskTimeout=Task exceeded time limit of %d ms. +#message.userCancelTask=Task was canceled by user. +#message.indexIncomplete=Index of some classes skipped.
%s
%d classes were not indexed and will not appear in search results! message.indexingClassesSkipped=Jadx의 메모리가 부족합니다. 따라서 %d 개의 클래스가 인덱싱되지 않았습니다.
모든 클래스를 인덱싱하려면 최대 힙 크기를 늘린 상태로 Jadx를 다시 시작하십시오. heapUsage.text=JADX 메모리 사용량 : %.2f GB / %.2f GB diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index 8f0fff584..457ae0854 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -36,7 +36,7 @@ tree.loading=稍等... progress.load=稍等 progress.decompile=Decompiling -progress.index=Indexing +#progress.index=Indexing error_dialog.title=错误 @@ -58,6 +58,9 @@ tabs.code=代码 nav.back=后退 nav.forward=前进 +#message.taskTimeout=Task exceeded time limit of %d ms. +#message.userCancelTask=Task was canceled by user. +#message.indexIncomplete=Index of some classes skipped.
%s
%d classes were not indexed and will not appear in search results! message.indexingClassesSkipped=Jadx 的内存不足。因此,%d 类没有编入索引。
如果要将所有类编入索引,请使用增加的最大堆大小重新启动 Jadx。 heapUsage.text=JADX 内存使用率:%.2f GB 共 %.2f GB