From 1c08d854fb3474d38d439d8e686720d6b99e2622 Mon Sep 17 00:00:00 2001 From: Skylot Date: Sat, 29 May 2021 16:35:56 +0100 Subject: [PATCH] fix(gui): add memory limit checks to export and load tasks (#1181) --- .../main/java/jadx/api/JadxDecompiler.java | 39 +++++--- .../java/jadx/api/impl/InMemoryCodeCache.java | 2 +- .../java/jadx/core/dex/nodes/RootNode.java | 7 +- .../src/main/java/jadx/core/utils/Utils.java | 7 ++ .../src/main/java/jadx/gui/JadxWrapper.java | 13 --- .../jadx/gui/jobs/BackgroundExecutor.java | 90 ++++++++--------- .../java/jadx/gui/jobs/DecompileTask.java | 23 +++-- .../main/java/jadx/gui/jobs/ExportTask.java | 99 +++++++++++++++++++ .../java/jadx/gui/jobs/IBackgroundTask.java | 2 +- .../main/java/jadx/gui/jobs/IndexService.java | 5 +- .../main/java/jadx/gui/jobs/TaskStatus.java | 3 +- .../src/main/java/jadx/gui/ui/MainWindow.java | 13 ++- .../main/java/jadx/gui/ui/RenameDialog.java | 7 +- .../java/jadx/gui/utils/FixedCodeCache.java | 33 +++++++ .../src/main/java/jadx/gui/utils/NLS.java | 8 +- .../src/main/java/jadx/gui/utils/UiUtils.java | 5 + .../resources/i18n/Messages_de_DE.properties | 7 +- .../resources/i18n/Messages_en_US.properties | 7 +- .../resources/i18n/Messages_es_ES.properties | 7 +- .../resources/i18n/Messages_ko_KR.properties | 7 +- .../resources/i18n/Messages_zh_CN.properties | 7 +- 21 files changed, 273 insertions(+), 118 deletions(-) create mode 100644 jadx-gui/src/main/java/jadx/gui/jobs/ExportTask.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/FixedCodeCache.java diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 6b6dddf69..279fd9cd5 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -207,16 +207,24 @@ public final class JadxDecompiler implements Closeable { return getSaveExecutor(!args.isSkipSources(), !args.isSkipResources()); } + public List getSaveTasks() { + return getSaveTasks(!args.isSkipSources(), !args.isSkipResources()); + } + private ExecutorService getSaveExecutor(boolean saveSources, boolean saveResources) { + int threadsCount = args.getThreadsCount(); + LOG.debug("processing threads count: {}", threadsCount); + LOG.info("processing ..."); + ExecutorService executor = Executors.newFixedThreadPool(threadsCount); + List tasks = getSaveTasks(saveSources, saveResources); + tasks.forEach(executor::execute); + return executor; + } + + private List getSaveTasks(boolean saveSources, boolean saveResources) { if (root == null) { throw new JadxRuntimeException("No loaded files"); } - int threadsCount = args.getThreadsCount(); - LOG.debug("processing threads count: {}", threadsCount); - - LOG.info("processing ..."); - ExecutorService executor = Executors.newFixedThreadPool(threadsCount); - File sourcesOutDir; File resOutDir; if (args.isExportAsGradleProject()) { @@ -244,16 +252,17 @@ public final class JadxDecompiler implements Closeable { sourcesOutDir = args.getOutDirSrc(); resOutDir = args.getOutDirRes(); } - if (saveResources) { - appendResourcesSave(executor, resOutDir); - } + List tasks = new ArrayList<>(); if (saveSources) { - appendSourcesSave(executor, sourcesOutDir); + appendSourcesSave(tasks, sourcesOutDir); } - return executor; + if (saveResources) { + appendResourcesSaveTasks(tasks, resOutDir); + } + return tasks; } - private void appendResourcesSave(ExecutorService executor, File outDir) { + private void appendResourcesSaveTasks(List tasks, File outDir) { Set inputFileNames = args.getInputFiles().stream().map(File::getAbsolutePath).collect(Collectors.toSet()); for (ResourceFile resourceFile : getResources()) { if (resourceFile.getType() != ResourceType.ARSC @@ -261,11 +270,11 @@ public final class JadxDecompiler implements Closeable { // ignore resource made from input file continue; } - executor.execute(new ResourcesSaver(outDir, resourceFile)); + tasks.add(new ResourcesSaver(outDir, resourceFile)); } } - private void appendSourcesSave(ExecutorService executor, File outDir) { + private void appendSourcesSave(List tasks, File outDir) { Predicate classFilter = args.getClassFilter(); for (JavaClass cls : getClasses()) { if (cls.getClassNode().contains(AFlag.DONT_GENERATE)) { @@ -274,7 +283,7 @@ public final class JadxDecompiler implements Closeable { if (classFilter != null && !classFilter.test(cls.getFullName())) { continue; } - executor.execute(() -> { + tasks.add(() -> { try { ICodeInfo code = cls.getCodeInfo(); SaveCode.save(outDir, cls.getClassNode(), code); diff --git a/jadx-core/src/main/java/jadx/api/impl/InMemoryCodeCache.java b/jadx-core/src/main/java/jadx/api/impl/InMemoryCodeCache.java index be6ae3dee..a72f120ee 100644 --- a/jadx-core/src/main/java/jadx/api/impl/InMemoryCodeCache.java +++ b/jadx-core/src/main/java/jadx/api/impl/InMemoryCodeCache.java @@ -29,6 +29,6 @@ public class InMemoryCodeCache implements ICodeCache { @Override public String toString() { - return "InMemoryCodeCache"; + return "InMemoryCodeCache: size=" + storage.size(); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java index 2568f7987..fbf3151fb 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java @@ -38,6 +38,7 @@ import jadx.core.dex.visitors.typeinference.TypeUpdate; import jadx.core.utils.CacheStorage; import jadx.core.utils.ErrorsCounter; import jadx.core.utils.StringUtils; +import jadx.core.utils.Utils; import jadx.core.utils.android.AndroidResourcesUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.xmlgen.ResTableParser; @@ -61,8 +62,6 @@ public class RootNode { private final MethodUtils methodUtils; private final TypeUtils typeUtils; - private final ICodeCache codeCache; - private final Map clsMap = new HashMap<>(); private List classes = new ArrayList<>(); @@ -80,7 +79,6 @@ public class RootNode { this.stringUtils = new StringUtils(args); this.constValues = new ConstStorage(args); this.typeUpdate = new TypeUpdate(this); - this.codeCache = args.getCodeCache(); this.methodUtils = new MethodUtils(this); this.typeUtils = new TypeUtils(this); this.isProto = args.getInputFiles().size() > 0 && args.getInputFiles().get(0).getName().toLowerCase().endsWith(".aab"); @@ -94,6 +92,7 @@ public class RootNode { } catch (Exception e) { addDummyClass(cls, e); } + Utils.checkThreadInterrupt(); }); } if (classes.size() != clsMap.size()) { @@ -498,7 +497,7 @@ public class RootNode { } public ICodeCache getCodeCache() { - return codeCache; + return args.getCodeCache(); } public MethodUtils getMethodUtils() { diff --git a/jadx-core/src/main/java/jadx/core/utils/Utils.java b/jadx-core/src/main/java/jadx/core/utils/Utils.java index 54204d304..fd651b252 100644 --- a/jadx-core/src/main/java/jadx/core/utils/Utils.java +++ b/jadx-core/src/main/java/jadx/core/utils/Utils.java @@ -19,6 +19,7 @@ import org.jetbrains.annotations.Nullable; import jadx.api.ICodeWriter; import jadx.api.JadxDecompiler; import jadx.core.dex.visitors.DepthTraversal; +import jadx.core.utils.exceptions.JadxRuntimeException; public class Utils { @@ -373,4 +374,10 @@ public class Utils { public static boolean notEmpty(T[] arr) { return arr != null && arr.length != 0; } + + public static void checkThreadInterrupt() { + if (Thread.interrupted()) { + throw new JadxRuntimeException("Thread interrupted"); + } + } } diff --git a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java index cb21c3b72..d9d701186 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java @@ -1,6 +1,5 @@ package jadx.gui; -import java.io.File; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -8,8 +7,6 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; -import javax.swing.ProgressMonitor; - import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,16 +60,6 @@ public class JadxWrapper { this.openPaths = Collections.emptyList(); } - public void saveAll(File dir, ProgressMonitor progressMonitor) { - Runnable save = () -> { - decompiler.getArgs().setRootDir(dir); - decompiler.save(500, (done, total) -> progressMonitor.setProgress((int) (done * 100.0 / total))); - progressMonitor.close(); - LOG.info("done"); - }; - new Thread(save).start(); - } - /** * Get the complete list of classes */ 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 b7ca3c6de..61f3f5d90 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java @@ -6,7 +6,8 @@ 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 java.util.function.Consumer; +import java.util.function.Supplier; import javax.swing.SwingWorker; @@ -57,12 +58,12 @@ public class BackgroundExecutor { } } - public void execute(String title, List backgroundJobs, Runnable onFinishUiRunnable) { + public void execute(String title, List backgroundJobs, Consumer onFinishUiRunnable) { execute(new SimpleTask(title, backgroundJobs, onFinishUiRunnable)); } - public void execute(String title, Runnable backgroundRunnable, Runnable onFinishUiRunnable) { - execute(new SimpleTask(title, backgroundRunnable, onFinishUiRunnable)); + public void execute(String title, Runnable backgroundRunnable, Consumer onFinishUiRunnable) { + execute(new SimpleTask(title, Collections.singletonList(backgroundRunnable), onFinishUiRunnable)); } private ThreadPoolExecutor makeTaskQueueExecutor() { @@ -71,8 +72,9 @@ public class BackgroundExecutor { private final class TaskWorker extends SwingWorker { private final IBackgroundTask task; - private long jobsCount; private TaskStatus status = TaskStatus.WAIT; + private long jobsCount; + private long jobsComplete; public TaskWorker(IBackgroundTask task) { this.task = task; @@ -89,13 +91,11 @@ public class BackgroundExecutor { progressPane.changeCancelBtnVisible(this, task.canBeCanceled()); progressPane.changeVisibility(this, true); - if (runJobs()) { - status = TaskStatus.COMPLETE; - } + runJobs(); return status; } - private boolean runJobs() throws InterruptedException { + private void runJobs() throws InterruptedException { List jobs = task.scheduleJobs(); jobsCount = jobs.size(); LOG.debug("Starting background task '{}', jobs count: {}, time limit: {} ms, memory check: {}", @@ -107,33 +107,34 @@ public class BackgroundExecutor { executor.execute(job); } executor.shutdown(); - return waitTermination(executor); + status = waitTermination(executor); + jobsComplete = executor.getCompletedTaskCount(); } @SuppressWarnings("BusyWait") - private boolean waitTermination(ThreadPoolExecutor executor) throws InterruptedException { - BooleanSupplier cancelCheck = buildCancelCheck(); + private TaskStatus waitTermination(ThreadPoolExecutor executor) throws InterruptedException { + Supplier cancelCheck = buildCancelCheck(); try { while (true) { if (executor.isTerminated()) { - return true; + return TaskStatus.COMPLETE; } - if (cancelCheck.getAsBoolean()) { + TaskStatus cancelStatus = cancelCheck.get(); + if (cancelStatus != null) { performCancel(executor); - return false; + return cancelStatus; } setProgress(calcProgress(executor.getCompletedTaskCount())); Thread.sleep(1000); } } catch (InterruptedException e) { LOG.debug("Task wait interrupted"); - status = TaskStatus.CANCEL_BY_USER; performCancel(executor); - return false; + return TaskStatus.CANCEL_BY_USER; } catch (Exception e) { LOG.error("Task wait aborted by exception", e); performCancel(executor); - return false; + return TaskStatus.ERROR; } } @@ -146,37 +147,23 @@ public class BackgroundExecutor { 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; - } + private Supplier buildCancelCheck() { 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; + LOG.error("Task '{}' execution timeout, force cancel", task.getTitle()); + return TaskStatus.CANCEL_BY_TIMEOUT; } if (checkMemoryUsage && !UiUtils.isFreeMemoryAvailable()) { - LOG.debug("Task '{}' memory limit reached, force cancel", task.getTitle()); - status = TaskStatus.CANCEL_BY_MEMORY; - return true; + LOG.error("Task '{}' memory limit reached, force cancel", task.getTitle()); + return TaskStatus.CANCEL_BY_MEMORY; } - return simpleCancelCheck(); + if (isCancelled() || Thread.currentThread().isInterrupted()) { + LOG.warn("Task '{}' canceled", task.getTitle()); + return TaskStatus.CANCEL_BY_USER; + } + return null; }; } @@ -187,25 +174,21 @@ public class BackgroundExecutor { @Override protected void done() { progressPane.setVisible(false); - task.onFinish(status); + task.onFinish(status, jobsCount - jobsComplete); } } private static final class SimpleTask implements IBackgroundTask { private final String title; private final List jobs; - private final Runnable onFinish; + private final Consumer onFinish; - public SimpleTask(String title, List jobs, @Nullable Runnable onFinish) { + public SimpleTask(String title, List jobs, @Nullable Consumer onFinish) { this.title = title; this.jobs = jobs; this.onFinish = onFinish; } - public SimpleTask(String title, Runnable job, @Nullable Runnable onFinish) { - this(title, Collections.singletonList(job), onFinish); - } - @Override public String getTitle() { return title; @@ -217,10 +200,15 @@ public class BackgroundExecutor { } @Override - public void onFinish(TaskStatus status) { + public void onFinish(TaskStatus status, long l) { if (onFinish != null) { - onFinish.run(); + onFinish.accept(status); } } + + @Override + public boolean checkMemoryUsage() { + return true; + } } } diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java index 9d3bff3f9..3a058bce0 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java @@ -20,6 +20,10 @@ public class DecompileTask implements IBackgroundTask { private static final int CLS_LIMIT = Integer.parseInt(UiUtils.getEnvVar("JADX_CLS_PROCESS_LIMIT", "50")); + public static int calcDecompileTimeLimit(int classCount) { + return classCount * CLS_LIMIT + 5000; + } + private final MainWindow mainWindow; private final JadxWrapper wrapper; private final AtomicInteger complete = new AtomicInteger(0); @@ -59,7 +63,7 @@ public class DecompileTask implements IBackgroundTask { } @Override - public void onFinish(TaskStatus status) { + public void onFinish(TaskStatus status, long skippedJobs) { long taskTime = System.currentTimeMillis() - startTime; long avgPerCls = taskTime / expectedCompleteCount; LOG.info("Decompile task complete in {} ms (avg {} ms per class), classes: {}," @@ -68,29 +72,28 @@ public class DecompileTask implements IBackgroundTask { IndexService indexService = mainWindow.getCacheObject().getIndexService(); indexService.setComplete(true); - - int complete = this.complete.get(); - int skipped = expectedCompleteCount - complete; - if (skipped == 0) { + if (skippedJobs == 0) { return; } - LOG.warn("Decompile and indexing of some classes skipped: {}, status: {}", skipped, status); + + int skippedCls = expectedCompleteCount - complete.get(); + LOG.warn("Decompile and indexing of some classes skipped: {}, status: {}", skippedCls, status); switch (status) { case CANCEL_BY_USER: { String reason = NLS.str("message.userCancelTask"); - String message = NLS.str("message.indexIncomplete", reason, skipped); + String message = NLS.str("message.indexIncomplete", reason, skippedCls); JOptionPane.showMessageDialog(mainWindow, message); break; } case CANCEL_BY_TIMEOUT: { String reason = NLS.str("message.taskTimeout", timeLimit()); - String message = NLS.str("message.indexIncomplete", reason, skipped); + String message = NLS.str("message.indexIncomplete", reason, skippedCls); JOptionPane.showMessageDialog(mainWindow, message); break; } case CANCEL_BY_MEMORY: { mainWindow.showHeapUsageBar(); - JOptionPane.showMessageDialog(mainWindow, NLS.str("message.indexingClassesSkipped", skipped)); + JOptionPane.showMessageDialog(mainWindow, NLS.str("message.indexingClassesSkipped", skippedCls)); break; } } @@ -103,7 +106,7 @@ public class DecompileTask implements IBackgroundTask { @Override public int timeLimit() { - return expectedCompleteCount * CLS_LIMIT + 5000; + return calcDecompileTimeLimit(expectedCompleteCount); } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/ExportTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/ExportTask.java new file mode 100644 index 000000000..6a930b387 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/jobs/ExportTask.java @@ -0,0 +1,99 @@ +package jadx.gui.jobs; + +import java.io.File; +import java.util.List; + +import javax.swing.JOptionPane; + +import jadx.api.ICodeCache; +import jadx.api.JadxDecompiler; +import jadx.gui.JadxWrapper; +import jadx.gui.ui.MainWindow; +import jadx.gui.utils.FixedCodeCache; +import jadx.gui.utils.NLS; + +public class ExportTask implements IBackgroundTask { + + private final MainWindow mainWindow; + private final JadxWrapper wrapper; + private final File saveDir; + + private int timeLimit; + private ICodeCache uiCodeCache; + + public ExportTask(MainWindow mainWindow, JadxWrapper wrapper, File saveDir) { + this.mainWindow = mainWindow; + this.wrapper = wrapper; + this.saveDir = saveDir; + } + + @Override + public String getTitle() { + return NLS.str("msg.saving_sources"); + } + + @Override + public List scheduleJobs() { + wrapCodeCache(); + JadxDecompiler decompiler = wrapper.getDecompiler(); + decompiler.getArgs().setRootDir(saveDir); + List saveTasks = decompiler.getSaveTasks(); + this.timeLimit = DecompileTask.calcDecompileTimeLimit(saveTasks.size()); + return saveTasks; + } + + private void wrapCodeCache() { + uiCodeCache = wrapper.getArgs().getCodeCache(); + // do not save newly decompiled code in cache to not increase memory usage + // TODO: maybe make memory limited cache? + wrapper.getArgs().setCodeCache(new FixedCodeCache(uiCodeCache)); + } + + @Override + public void onFinish(TaskStatus status, long skipped) { + // restore initial code cache + wrapper.getArgs().setCodeCache(uiCodeCache); + if (skipped == 0) { + return; + } + String reason = getIncompleteReason(status); + if (reason != null) { + JOptionPane.showMessageDialog(mainWindow, + NLS.str("message.saveIncomplete", reason, skipped), + NLS.str("message.errorTitle"), JOptionPane.ERROR_MESSAGE); + } + } + + private String getIncompleteReason(TaskStatus status) { + switch (status) { + case CANCEL_BY_USER: + return NLS.str("message.userCancelTask"); + + case CANCEL_BY_TIMEOUT: + return NLS.str("message.taskTimeout", timeLimit()); + + case CANCEL_BY_MEMORY: + mainWindow.showHeapUsageBar(); + return NLS.str("message.memoryLow"); + + case ERROR: + return NLS.str("message.taskError"); + } + return null; + } + + @Override + public int timeLimit() { + return timeLimit; + } + + @Override + public boolean canBeCanceled() { + return true; + } + + @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 b82f2cbdb..ee11c3363 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,7 @@ public interface IBackgroundTask { List scheduleJobs(); - void onFinish(TaskStatus status); + void onFinish(TaskStatus status, long skipped); default boolean canBeCanceled() { return false; diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/IndexService.java b/jadx-gui/src/main/java/jadx/gui/jobs/IndexService.java index 5e1cf76a5..3a538bf8a 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/IndexService.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/IndexService.java @@ -11,7 +11,6 @@ import jadx.api.JavaClass; import jadx.gui.utils.CacheObject; import jadx.gui.utils.CodeLinesInfo; import jadx.gui.utils.CodeUsageInfo; -import jadx.gui.utils.UiUtils; import jadx.gui.utils.search.StringRef; import jadx.gui.utils.search.TextSearchIndex; @@ -59,9 +58,7 @@ public class IndexService { } index.remove(cls); usageInfo.remove(cls); - if (UiUtils.isFreeMemoryAvailable()) { - indexCls(cls); - } + indexCls(cls); } @NotNull diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/TaskStatus.java b/jadx-gui/src/main/java/jadx/gui/jobs/TaskStatus.java index ecfbf2a66..defcd7196 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/TaskStatus.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/TaskStatus.java @@ -6,5 +6,6 @@ public enum TaskStatus { COMPLETE, CANCEL_BY_USER, CANCEL_BY_TIMEOUT, - CANCEL_BY_MEMORY + CANCEL_BY_MEMORY, + ERROR } 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 9d87781ba..99625748c 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -59,7 +59,6 @@ import javax.swing.JSplitPane; import javax.swing.JToggleButton; import javax.swing.JToolBar; import javax.swing.JTree; -import javax.swing.ProgressMonitor; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import javax.swing.event.MenuEvent; @@ -91,6 +90,7 @@ import jadx.gui.JadxWrapper; import jadx.gui.device.debugger.BreakpointManager; import jadx.gui.jobs.BackgroundExecutor; import jadx.gui.jobs.DecompileTask; +import jadx.gui.jobs.ExportTask; import jadx.gui.jobs.IndexService; import jadx.gui.jobs.TaskStatus; import jadx.gui.settings.JadxProject; @@ -392,7 +392,12 @@ public class MainWindow extends JFrame { } backgroundExecutor.execute(NLS.str("progress.load"), () -> wrapper.openFile(paths), - () -> { + (status) -> { + if (status == TaskStatus.CANCEL_BY_MEMORY) { + showHeapUsageBar(); + UiUtils.errorMessage(this, NLS.str("message.memoryLow")); + return; + } deobfToggleBtn.setSelected(settings.isDeobfuscationOn()); initTree(); update(); @@ -581,9 +586,7 @@ public class MainWindow extends JFrame { decompilerArgs.setSkipResources(settings.isSkipResources()); } settings.setLastSaveFilePath(fileChooser.getCurrentDirectory().toPath()); - ProgressMonitor progressMonitor = new ProgressMonitor(mainPanel, NLS.str("msg.saving_sources"), "", 0, 100); - progressMonitor.setMillisToPopup(0); - wrapper.saveAll(fileChooser.getSelectedFile(), progressMonitor); + backgroundExecutor.execute(new ExportTask(this, wrapper, fileChooser.getSelectedFile())); } } 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 41b683a2e..ad735693b 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java @@ -41,6 +41,7 @@ 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.TaskStatus; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JField; @@ -215,7 +216,11 @@ public class RenameDialog extends JDialog { if (!updatedTopClasses.isEmpty()) { mainWindow.getBackgroundExecutor().execute("Refreshing", Utils.collectionMap(updatedTopClasses, cls -> () -> refreshJClass(cls)), - () -> { + (status) -> { + if (status == TaskStatus.CANCEL_BY_MEMORY) { + mainWindow.showHeapUsageBar(); + UiUtils.errorMessage(this, NLS.str("message.memoryLow")); + } if (node instanceof JPackage) { // reinit tree mainWindow.initTree(); diff --git a/jadx-gui/src/main/java/jadx/gui/utils/FixedCodeCache.java b/jadx-gui/src/main/java/jadx/gui/utils/FixedCodeCache.java new file mode 100644 index 000000000..cd2c61712 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/FixedCodeCache.java @@ -0,0 +1,33 @@ +package jadx.gui.utils; + +import org.jetbrains.annotations.Nullable; + +import jadx.api.ICodeCache; +import jadx.api.ICodeInfo; + +/** + * Code cache with fixed size of wrapper code cache ('remove' and 'add' methods will do nothing). + */ +public class FixedCodeCache implements ICodeCache { + + private final ICodeCache codeCache; + + public FixedCodeCache(ICodeCache codeCache) { + this.codeCache = codeCache; + } + + @Override + public @Nullable ICodeInfo get(String clsFullName) { + return this.codeCache.get(clsFullName); + } + + @Override + public void remove(String clsFullName) { + // no op + } + + @Override + public void add(String clsFullName, ICodeInfo codeInfo) { + // no op + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/NLS.java b/jadx-gui/src/main/java/jadx/gui/utils/NLS.java index 8fcaf9d52..94b89d07a 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/NLS.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/NLS.java @@ -72,13 +72,7 @@ public class NLS { } public static String str(String key, Object... parameters) { - String value; - try { - value = localizedMessagesMap.getString(key); - } catch (MissingResourceException e) { - value = FALLBACK_MESSAGES_MAP.getString(key); // definitely exists - } - return String.format(value, parameters); + return String.format(str(key), parameters); } public static String str(String key, LangLocale locale) { 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 870467e6b..0786e7cf1 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java @@ -258,4 +258,9 @@ public class UiUtils { } return envVal; } + + public static void errorMessage(Component parent, String message) { + JOptionPane.showMessageDialog(parent, message, + NLS.str("message.errorTitle"), JOptionPane.ERROR_MESSAGE); + } } 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 c85a83aae..44b0f65e5 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -60,6 +60,11 @@ nav.forward=Vorwärts #message.taskTimeout=Task exceeded time limit of %d ms. #message.userCancelTask=Task was canceled by user. +#message.memoryLow=Jadx is running low on memory. Please restart with increased maximum heap size. +#message.taskError=Task failed with error (check log for details). +#message.errorTitle=Error + +#message.saveIncomplete=Save incomplete.
%s
%d classes or resources were not saved! #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. @@ -160,7 +165,7 @@ preferences.rename_printable=Ist druckbar #preferences.res_skip_file= msg.open_file=Bitte Datei öffnen -msg.saving_sources=Quellen speichern… +msg.saving_sources=Quellen speichern msg.language_changed_title=Sprache speichern msg.language_changed=Neue Sprache wird beim nächsten Start der Anwendung angezeigt. msg.index_not_initialized=Index nicht initialisiert, Suche wird deaktiviert! 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 da06b442a..d72dbdc2c 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -60,6 +60,11 @@ nav.forward=Forward message.taskTimeout=Task exceeded time limit of %d ms. message.userCancelTask=Task was canceled by user. +message.memoryLow=Jadx is running low on memory. Please restart with increased maximum heap size. +message.taskError=Task failed with error (check log for details). +message.errorTitle=Error + +message.saveIncomplete=Save incomplete.
%s
%d classes or resources were not saved! 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. @@ -160,7 +165,7 @@ preferences.res_file_ext=File Extensions (e.g. .xml|.html), * means all preferences.res_skip_file=Skip files exceed (MB) msg.open_file=Please open file -msg.saving_sources=Saving sources... +msg.saving_sources=Saving sources msg.language_changed_title=Language changed msg.language_changed=New language will be displayed the next time application starts. msg.index_not_initialized=Index not initialized, search will be disabled! 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 a7aa1c096..a3b2e8d6a 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -60,6 +60,11 @@ nav.forward=Adelante #message.taskTimeout=Task exceeded time limit of %d ms. #message.userCancelTask=Task was canceled by user. +#message.memoryLow=Jadx is running low on memory. Please restart with increased maximum heap size. +#message.taskError=Task failed with error (check log for details). +#message.errorTitle=Error + +#message.saveIncomplete=Save incomplete.
%s
%d classes or resources were not saved! #message.indexIncomplete=Index of some classes skipped.
%s
%d classes were not indexed and will not appear in search results! #message.indexingClassesSkipped= @@ -160,7 +165,7 @@ preferences.reset_title=Reestablecer preferencias #preferences.res_skip_file= msg.open_file=Por favor, abra un archivo -msg.saving_sources=Guardando fuente... +msg.saving_sources=Guardando fuente msg.language_changed_title=Idioma cambiado msg.language_changed=El nuevo idioma se mostrará la próxima vez que la aplicación se inicie. msg.index_not_initialized=Índice no inicializado, ¡la bósqueda se desactivará! 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 3542a6c00..d9db1b9c0 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -60,6 +60,11 @@ nav.forward=앞으로 #message.taskTimeout=Task exceeded time limit of %d ms. #message.userCancelTask=Task was canceled by user. +#message.memoryLow=Jadx is running low on memory. Please restart with increased maximum heap size. +#message.taskError=Task failed with error (check log for details). +#message.errorTitle=Error + +#message.saveIncomplete=Save incomplete.
%s
%d classes or resources were not saved! #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를 다시 시작하십시오. @@ -160,7 +165,7 @@ preferences.res_file_ext=파일 확장자 (예: .xml|.html) (* 은 전체를 의 preferences.res_skip_file=이 옵션보다 큰 파일 건너 뛰기 (MB) msg.open_file=파일을 여십시오 -msg.saving_sources=소스 저장 중 ... +msg.saving_sources=소스 저장 중 msg.language_changed_title=언어 변경됨 msg.language_changed=다음에 응용 프로그램이 시작되면 새 언어가 표시됩니다. msg.index_not_initialized=인덱스가 초기화되지 않았습니다. 검색이 비활성화됩니다! 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 457ae0854..fa634b29e 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -60,6 +60,11 @@ nav.forward=前进 #message.taskTimeout=Task exceeded time limit of %d ms. #message.userCancelTask=Task was canceled by user. +#message.memoryLow=Jadx is running low on memory. Please restart with increased maximum heap size. +#message.taskError=Task failed with error (check log for details). +#message.errorTitle=Error + +#message.saveIncomplete=Save incomplete.
%s
%d classes or resources were not saved! #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。 @@ -160,7 +165,7 @@ preferences.rename_printable=是可打印 #preferences.res_skip_file= msg.open_file=请打开文件 -msg.saving_sources=正在导出源代码... +msg.saving_sources=正在导出源代码 msg.language_changed_title=语言已更改 msg.language_changed=在下次启动时将会显示新的语言。 msg.index_not_initialized=索引尚未初始化,无法进行搜索!