diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLI.java b/jadx-cli/src/main/java/jadx/cli/JadxCLI.java index dfe3a51de..0911f893a 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLI.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLI.java @@ -102,6 +102,7 @@ public class JadxCLI { if (LogHelper.getLogLevel() == LogLevelEnum.QUIET) { jadx.save(); } else { + LOG.info("processing ..."); jadx.save(500, (done, total) -> { int progress = (int) (done * 100.0 / total); System.out.printf("INFO - progress: %d of %d (%d%%)\r", done, total, progress); diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index d13bbff6f..fc0d4de7f 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -12,9 +12,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -36,6 +33,7 @@ import jadx.api.plugins.input.JadxCodeInput; import jadx.api.plugins.pass.JadxPass; import jadx.api.plugins.pass.types.JadxAfterLoadPass; import jadx.api.plugins.pass.types.JadxPassType; +import jadx.api.utils.tasks.ITaskExecutor; import jadx.core.Jadx; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.ClassNode; @@ -52,6 +50,7 @@ import jadx.core.utils.DecompilerScheduler; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; +import jadx.core.utils.tasks.TaskExecutor; import jadx.core.xmlgen.BinaryXMLParser; import jadx.core.xmlgen.ProtoXMLParser; import jadx.core.xmlgen.ResourcesSaver; @@ -248,13 +247,12 @@ public final class JadxDecompiler implements Closeable { @SuppressWarnings("BusyWait") public void save(int intervalInMillis, ProgressListener listener) { - ThreadPoolExecutor ex = (ThreadPoolExecutor) getSaveExecutor(); - ex.shutdown(); try { - long total = ex.getTaskCount(); - while (ex.isTerminating()) { - long done = ex.getCompletedTaskCount(); - listener.progress(done, total); + ITaskExecutor tasks = getSaveTaskExecutor(); + tasks.execute(); + long total = tasks.getTasksCount(); + while (tasks.isRunning()) { + listener.progress(tasks.getProgress(), total); Thread.sleep(intervalInMillis); } } catch (InterruptedException e) { @@ -271,72 +269,64 @@ public final class JadxDecompiler implements Closeable { save(false, true); } - @SuppressWarnings("ResultOfMethodCallIgnored") private void save(boolean saveSources, boolean saveResources) { - ExecutorService ex = getSaveExecutor(saveSources, saveResources); - ex.shutdown(); - try { - ex.awaitTermination(1, TimeUnit.DAYS); - } catch (InterruptedException e) { - LOG.error("Save interrupted", e); - Thread.currentThread().interrupt(); - } + ITaskExecutor executor = getSaveTasks(saveSources, saveResources); + executor.execute(); + executor.awaitTermination(); } - public ExecutorService getSaveExecutor() { - return getSaveExecutor(!args.isSkipSources(), !args.isSkipResources()); - } - - public List getSaveTasks() { + public ITaskExecutor getSaveTaskExecutor() { 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; + @Deprecated(forRemoval = true) + public ExecutorService getSaveExecutor() { + ITaskExecutor executor = getSaveTaskExecutor(); + executor.execute(); + return executor.getInternalExecutor(); } - private List getSaveTasks(boolean saveSources, boolean saveResources) { + @Deprecated(forRemoval = true) + public List getSaveTasks() { + return Collections.singletonList(this::save); + } + + private TaskExecutor getSaveTasks(boolean saveSources, boolean saveResources) { if (root == null) { throw new JadxRuntimeException("No loaded files"); } File sourcesOutDir; File resOutDir; - List tasks = new ArrayList<>(); - TaskBarrier barrier = new TaskBarrier(); + ExportGradleTask gradleExportTask; if (args.isExportAsGradleProject()) { - ExportGradleTask gradleExportTask = new ExportGradleTask(resources, root, args.getOutDir(), barrier); + gradleExportTask = new ExportGradleTask(resources, root, args.getOutDir()); gradleExportTask.init(); sourcesOutDir = gradleExportTask.getSrcOutDir(); resOutDir = gradleExportTask.getResOutDir(); - tasks.add(gradleExportTask); } else { sourcesOutDir = args.getOutDirSrc(); resOutDir = args.getOutDirRes(); + gradleExportTask = null; } - int taskCount = 0; - // save resources first because decompilation can hang or fail + TaskExecutor executor = new TaskExecutor(); + executor.setThreadsCount(args.getThreadsCount()); if (saveResources) { - taskCount = appendResourcesSaveTasks(tasks, resOutDir, barrier); + // save resources first because decompilation can stop or fail + appendResourcesSaveTasks(executor, resOutDir); } if (saveSources) { - taskCount += appendSourcesSave(tasks, sourcesOutDir, barrier); + appendSourcesSave(executor, sourcesOutDir); } - barrier.setUpBarrier(taskCount); - - return tasks; + if (gradleExportTask != null) { + executor.addSequentialTask(gradleExportTask); + } + return executor; } - private int appendResourcesSaveTasks(List tasks, File outDir, TaskBarrier barrier) { - int numResourceTasks = 0; + private void appendResourcesSaveTasks(ITaskExecutor executor, File outDir) { if (args.isSkipFilesSave()) { - return 0; + return; } // process AndroidManifest.xml first to load complete resource ids table for (ResourceFile resourceFile : getResources()) { @@ -345,8 +335,10 @@ public final class JadxDecompiler implements Closeable { break; } } - - Set inputFileNames = args.getInputFiles().stream().map(File::getAbsolutePath).collect(Collectors.toSet()); + Set inputFileNames = args.getInputFiles().stream() + .map(File::getAbsolutePath) + .collect(Collectors.toSet()); + List tasks = new ArrayList<>(); for (ResourceFile resourceFile : getResources()) { ResourceType resType = resourceFile.getType(); if (resType == ResourceType.MANIFEST) { @@ -358,17 +350,41 @@ public final class JadxDecompiler implements Closeable { // ignore resource made from input file continue; } - tasks.add(new ResourcesSaver(outDir, resourceFile, barrier)); - numResourceTasks++; + tasks.add(new ResourcesSaver(outDir, resourceFile)); } - return numResourceTasks; + executor.addParallelTasks(tasks); } - private int appendSourcesSave(List tasks, File outDir, TaskBarrier barrier) { - int numSourceTasks = 0; - Predicate classFilter = args.getClassFilter(); + private void appendSourcesSave(ITaskExecutor executor, File outDir) { List classes = getClasses(); - List processQueue = new ArrayList<>(classes.size()); + List processQueue = filterClasses(classes); + List> batches; + try { + batches = decompileScheduler.buildBatches(processQueue); + } catch (Exception e) { + throw new JadxRuntimeException("Decompilation batches build failed", e); + } + List decompileTasks = new ArrayList<>(batches.size()); + for (List decompileBatch : batches) { + decompileTasks.add(() -> { + for (JavaClass cls : decompileBatch) { + try { + ClassNode clsNode = cls.getClassNode(); + ICodeInfo code = clsNode.getCode(); + SaveCode.save(outDir, clsNode, code); + } catch (Exception e) { + LOG.error("Error saving class: {}", cls, e); + } + } + + }); + } + executor.addParallelTasks(decompileTasks); + } + + private List filterClasses(List classes) { + Predicate classFilter = args.getClassFilter(); + List list = new ArrayList<>(classes.size()); for (JavaClass cls : classes) { ClassNode clsNode = cls.getClassNode(); if (clsNode.contains(AFlag.DONT_GENERATE)) { @@ -380,35 +396,9 @@ public final class JadxDecompiler implements Closeable { } continue; } - processQueue.add(cls); + list.add(cls); } - List> batches; - try { - batches = decompileScheduler.buildBatches(processQueue); - } catch (Exception e) { - throw new JadxRuntimeException("Decompilation batches build failed", e); - } - for (List decompileBatch : batches) { - tasks.add(() -> { - try { - for (JavaClass cls : decompileBatch) { - try { - ClassNode clsNode = cls.getClassNode(); - ICodeInfo code = clsNode.getCode(); - SaveCode.save(outDir, clsNode, code); - } catch (Exception e) { - LOG.error("Error saving class: {}", cls, e); - } - } - } finally { - if (barrier != null) { - barrier.finishTask(); - } - } - }); - numSourceTasks++; - } - return numSourceTasks; + return list; } public List getClasses() { diff --git a/jadx-core/src/main/java/jadx/api/TaskBarrier.java b/jadx-core/src/main/java/jadx/api/TaskBarrier.java deleted file mode 100644 index 3db8dc8a2..000000000 --- a/jadx-core/src/main/java/jadx/api/TaskBarrier.java +++ /dev/null @@ -1,22 +0,0 @@ -package jadx.api; - -import java.util.concurrent.CountDownLatch; - -public class TaskBarrier { - - private CountDownLatch taskCountDown = null; - - public void setUpBarrier(final int numTasks) { - taskCountDown = new CountDownLatch(numTasks); - } - - public CountDownLatch getTaskCountDown() { - return taskCountDown; - } - - public void finishTask() { - if (taskCountDown != null) { - taskCountDown.countDown(); - } - } -} diff --git a/jadx-core/src/main/java/jadx/api/utils/tasks/ITaskExecutor.java b/jadx-core/src/main/java/jadx/api/utils/tasks/ITaskExecutor.java new file mode 100644 index 000000000..5aafd513e --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/utils/tasks/ITaskExecutor.java @@ -0,0 +1,69 @@ +package jadx.api.utils.tasks; + +import java.util.List; +import java.util.concurrent.ExecutorService; + +import org.jetbrains.annotations.Nullable; + +/** + * Schedule and execute tasks combined into stages + * with parallel or sequential execution (similar to the fork-join pattern). + */ +public interface ITaskExecutor { + + /** + * Add parallel stage with provided tasks + */ + void addParallelTasks(List parallelTasks); + + /** + * Add sequential stage with provided tasks + */ + void addSequentialTasks(List seqTasks); + + /** + * Add sequential stage with a single task + */ + void addSequentialTask(Runnable task); + + /** + * Scheduled tasks count + */ + int getTasksCount(); + + /** + * Set threads count for parallel stage. + * Can be changed during execution. + * Defaults to half of processors count. + */ + void setThreadsCount(int threadsCount); + + int getThreadsCount(); + + /** + * Start tasks execution. + */ + void execute(); + + int getProgress(); + + /** + * Not started tasks will be not executed after this method invocation. + */ + void terminate(); + + boolean isTerminating(); + + boolean isRunning(); + + /** + * Block until execution is finished + */ + void awaitTermination(); + + /** + * Return internal executor service. + */ + @Nullable + ExecutorService getInternalExecutor(); +} diff --git a/jadx-core/src/main/java/jadx/core/export/ExportGradleTask.java b/jadx-core/src/main/java/jadx/core/export/ExportGradleTask.java index 72e8e5e45..0409dc97c 100644 --- a/jadx-core/src/main/java/jadx/core/export/ExportGradleTask.java +++ b/jadx-core/src/main/java/jadx/core/export/ExportGradleTask.java @@ -5,10 +5,8 @@ import java.util.List; import jadx.api.ResourceFile; import jadx.api.ResourceType; -import jadx.api.TaskBarrier; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.android.AndroidManifestParser; -import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; import jadx.core.xmlgen.ResContainer; @@ -18,20 +16,21 @@ public class ExportGradleTask implements Runnable { private final RootNode root; private final File projectDir; - private final File appDir; private final File srcOutDir; private final File resOutDir; - private final TaskBarrier barrier; - - public ExportGradleTask(List resources, RootNode root, File projectDir, TaskBarrier barrier) { + public ExportGradleTask(List resources, RootNode root, File projectDir) { this.resources = resources; this.projectDir = projectDir; this.root = root; - this.appDir = new File(projectDir, "app"); + File appDir = new File(projectDir, "app"); this.srcOutDir = new File(appDir, "src/main/java"); this.resOutDir = new File(appDir, "src/main"); - this.barrier = barrier; + } + + public void init() { + FileUtils.makeDirs(srcOutDir); + FileUtils.makeDirs(resOutDir); } @Override @@ -58,23 +57,9 @@ public class ExportGradleTask implements Runnable { .orElse(null)); ExportGradleProject export = new ExportGradleProject(root, projectDir, androidManifest, strings); - - // wait until all sources and resources are exported and all necessary info for gradle export are - // collected - try { - barrier.getTaskCountDown().await(); - } catch (InterruptedException e) { - throw new JadxRuntimeException("Gradle export failed", e); - } - export.generateGradleFiles(); } - public void init() { - FileUtils.makeDirs(srcOutDir); - FileUtils.makeDirs(resOutDir); - } - public File getSrcOutDir() { return srcOutDir; } diff --git a/jadx-core/src/main/java/jadx/core/utils/tasks/TaskExecutor.java b/jadx-core/src/main/java/jadx/core/utils/tasks/TaskExecutor.java new file mode 100644 index 000000000..8b850d91c --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/utils/tasks/TaskExecutor.java @@ -0,0 +1,180 @@ +package jadx.core.utils.tasks; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.jetbrains.annotations.Nullable; + +import jadx.api.JadxArgs; +import jadx.api.utils.tasks.ITaskExecutor; +import jadx.core.utils.exceptions.JadxRuntimeException; + +public class TaskExecutor implements ITaskExecutor { + + private enum ExecType { + PARALLEL, + SEQUENTIAL, + } + + private static final class ExecStage { + private final ExecType type; + private final List tasks; + + private ExecStage(ExecType type, List tasks) { + this.type = type; + this.tasks = tasks; + } + + public ExecType getType() { + return type; + } + + public List getTasks() { + return tasks; + } + } + + private final List stages = new ArrayList<>(); + private final AtomicInteger threadsCount = new AtomicInteger(JadxArgs.DEFAULT_THREADS_COUNT); + private final AtomicInteger progress = new AtomicInteger(0); + private final AtomicBoolean running = new AtomicBoolean(false); + private final AtomicBoolean terminating = new AtomicBoolean(false); + private int tasksCount = 0; + private @Nullable ExecutorService executor; + + @Override + public void addParallelTasks(List parallelTasks) { + if (parallelTasks.isEmpty()) { + return; + } + tasksCount += parallelTasks.size(); + stages.add(new ExecStage(ExecType.PARALLEL, parallelTasks)); + } + + @Override + public void addSequentialTasks(List seqTasks) { + if (seqTasks.isEmpty()) { + return; + } + tasksCount += seqTasks.size(); + stages.add(new ExecStage(ExecType.SEQUENTIAL, seqTasks)); + } + + @Override + public void addSequentialTask(Runnable seqTask) { + addSequentialTasks(Collections.singletonList(seqTask)); + } + + @Override + public int getThreadsCount() { + return threadsCount.get(); + } + + @Override + public void setThreadsCount(int count) { + threadsCount.set(count); + } + + @Override + public int getTasksCount() { + return tasksCount; + } + + @Override + public int getProgress() { + return progress.get(); + } + + @Override + public void execute() { + if (running.get() || executor != null) { + throw new IllegalStateException("Already executing"); + } + running.set(true); + progress.set(0); + terminating.set(false); + executor = Executors.newFixedThreadPool(1); + executor.execute(this::runStages); + executor.shutdown(); + } + + @Override + public void terminate() { + terminating.set(true); + } + + @Override + public boolean isTerminating() { + return terminating.get(); + } + + @Override + public boolean isRunning() { + return running.get(); + } + + @Override + public @Nullable ExecutorService getInternalExecutor() { + return executor; + } + + @Override + public void awaitTermination() { + if (executor == null || !running.get()) { + // already terminated + return; + } + awaitExecutorTermination(executor); + } + + private void runStages() { + try { + for (ExecStage stage : stages) { + int threads = Math.min(stage.getTasks().size(), threadsCount.get()); + if (stage.getType() == ExecType.SEQUENTIAL || threads == 1) { + for (Runnable task : stage.getTasks()) { + wrapTask(task); + } + } else { + ExecutorService parallelExecutor = Executors.newFixedThreadPool(threads); + for (Runnable task : stage.getTasks()) { + parallelExecutor.execute(() -> wrapTask(task)); + } + parallelExecutor.shutdown(); + awaitExecutorTermination(parallelExecutor); + } + if (terminating.get()) { + break; + } + } + } finally { + running.set(false); + executor = null; + } + } + + private void wrapTask(Runnable task) { + if (terminating.get()) { + return; + } + task.run(); + progress.incrementAndGet(); + } + + public static void awaitExecutorTermination(ExecutorService executor) { + try { + boolean complete = executor.awaitTermination(10, TimeUnit.DAYS); + if (!complete) { + throw new JadxRuntimeException("Executor timeout"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java index 9845c2cf6..5e0d3b2e1 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java @@ -10,7 +10,6 @@ import org.slf4j.LoggerFactory; import jadx.api.ResourceFile; import jadx.api.ResourcesLoader; -import jadx.api.TaskBarrier; import jadx.api.plugins.utils.ZipSecurity; import jadx.core.dex.visitors.SaveCode; import jadx.core.utils.exceptions.JadxException; @@ -23,29 +22,17 @@ public class ResourcesSaver implements Runnable { private final ResourceFile resourceFile; private final File outDir; - private TaskBarrier barrier = null; - public ResourcesSaver(File outDir, ResourceFile resourceFile) { this.resourceFile = resourceFile; this.outDir = outDir; } - public ResourcesSaver(File outDir, ResourceFile resourceFile, TaskBarrier barrier) { - this.resourceFile = resourceFile; - this.outDir = outDir; - this.barrier = barrier; - } - @Override public void run() { try { saveResources(resourceFile.loadContent()); } catch (Throwable e) { LOG.warn("Failed to save resource: {}", resourceFile.getOriginalName(), e); - } finally { - if (barrier != null) { - barrier.finishTask(); - } } } diff --git a/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java b/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java index b84fc5d6d..1b513044a 100644 --- a/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java @@ -13,7 +13,6 @@ import jadx.api.ICodeInfo; import jadx.api.JadxDecompiler; import jadx.api.JadxDecompilerTestUtils; import jadx.api.ResourceFile; -import jadx.api.TaskBarrier; import jadx.core.dex.nodes.RootNode; import jadx.core.export.ExportGradleProject; import jadx.core.export.ExportGradleTask; @@ -66,12 +65,11 @@ public abstract class ExportGradleTest { final ResContainer androidManifestContainer = createResourceContainer(manifestFilename); when(androidManifest.loadContent()).thenReturn(androidManifestContainer); final ResContainer strings = createResourceContainer(stringsFileName); - TaskBarrier taskBarrier = new TaskBarrier(); - final ExportGradleTask exportGradleTask = new ExportGradleTask(List.of(androidManifest), root, exportDir, taskBarrier); + final ExportGradleTask exportGradleTask = new ExportGradleTask(List.of(androidManifest), root, exportDir); exportGradleTask.init(); - assertThat(exportGradleTask.getSrcOutDir().exists()); - assertThat(exportGradleTask.getResOutDir().exists()); + assertThat(exportGradleTask.getSrcOutDir()).exists(); + assertThat(exportGradleTask.getResOutDir()).exists(); final ExportGradleProject export = new ExportGradleProject(root, exportDir, androidManifest, strings); @@ -80,13 +78,13 @@ public abstract class ExportGradleTest { protected String getAppGradleBuild() { File appBuildGradle = new File(exportDir, "app/build.gradle"); - assertThat(appBuildGradle.exists()); + assertThat(appBuildGradle).exists(); return loadFileContent(appBuildGradle); } protected String getSettingsGradle() { File settingsGradle = new File(exportDir, "settings.gradle"); - assertThat(settingsGradle.exists()); + assertThat(settingsGradle).exists(); return loadFileContent(settingsGradle); } } diff --git a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java index e199d198d..11c08c726 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java @@ -266,10 +266,6 @@ public class JadxWrapper { return getDecompiler().getEnclosingNode(codeInfo, pos); } - public List getSaveTasks() { - return getDecompiler().getSaveTasks(); - } - public List getPackages() { return getDecompiler().getPackages(); } 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 e20bf2a9b..bf887198c 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; @@ -20,6 +21,7 @@ import javax.swing.SwingWorker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.api.utils.tasks.ITaskExecutor; import jadx.gui.settings.JadxSettings; import jadx.gui.ui.MainWindow; import jadx.gui.ui.panel.ProgressPanel; @@ -104,7 +106,7 @@ public class BackgroundExecutor { private final class TaskWorker extends SwingWorker implements ITaskInfo { private final long id; private final IBackgroundTask task; - private ThreadPoolExecutor executor; + private ITaskExecutor taskExecutor; private TaskStatus status = TaskStatus.WAIT; private long jobsCount; private long jobsComplete; @@ -148,47 +150,43 @@ public class BackgroundExecutor { } private void runJobs() throws InterruptedException { - List jobs = task.scheduleJobs(); - jobsCount = jobs.size(); + taskExecutor = task.scheduleTasks(); + jobsCount = taskExecutor.getTasksCount(); LOG.debug("Starting background task '{}', jobs count: {}, time limit: {} ms, memory check: {}", task.getTitle(), jobsCount, task.timeLimit(), task.checkMemoryUsage()); if (jobsCount != 1) { progressPane.changeVisibility(this, true); } status = TaskStatus.STARTED; - int threadsCount = settings.getThreadsCount(); - executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadsCount); - for (Runnable job : jobs) { - executor.execute(job); - } - executor.shutdown(); + taskExecutor.setThreadsCount(settings.getThreadsCount()); + taskExecutor.execute(); long startTime = System.currentTimeMillis(); - status = waitTermination(executor, buildCancelCheck(startTime)); + status = waitTermination(buildCancelCheck(startTime)); time = System.currentTimeMillis() - startTime; - jobsComplete = executor.getCompletedTaskCount(); + jobsComplete = taskExecutor.getProgress(); } @SuppressWarnings("BusyWait") - private TaskStatus waitTermination(ThreadPoolExecutor executor, Supplier cancelCheck) throws InterruptedException { + private TaskStatus waitTermination(Supplier cancelCheck) throws InterruptedException { try { int k = 0; while (true) { - if (executor.isTerminated()) { + if (!taskExecutor.isRunning()) { return TaskStatus.COMPLETE; } TaskStatus cancelStatus = cancelCheck.get(); if (cancelStatus != null) { - performCancel(executor); + performCancel(); return cancelStatus; } if (k < 10) { // faster update for short tasks Thread.sleep(200); if (k == 5) { - updateProgress(executor); + updateProgress(); } } else { - updateProgress(executor); + updateProgress(); Thread.sleep(1000); } if (jobsCount == 1 && k == 5) { @@ -199,22 +197,22 @@ public class BackgroundExecutor { } } catch (InterruptedException e) { LOG.debug("Task wait interrupted"); - performCancel(executor); + performCancel(); return TaskStatus.CANCEL_BY_USER; } catch (Exception e) { LOG.error("Task wait aborted by exception", e); - performCancel(executor); + performCancel(); return TaskStatus.ERROR; } } - private void updateProgress(ThreadPoolExecutor executor) { + private void updateProgress() { Consumer onProgressListener = task.getProgressListener(); ITaskProgress taskProgress = task.getTaskProgress(); if (taskProgress == null) { - setProgress(calcProgress(executor.getCompletedTaskCount(), jobsCount)); + setProgress(calcProgress(taskExecutor.getProgress(), jobsCount)); if (onProgressListener != null) { - onProgressListener.accept(new TaskProgress(executor.getCompletedTaskCount(), jobsCount)); + onProgressListener.accept(new TaskProgress(taskExecutor.getProgress(), jobsCount)); } } else { setProgress(calcProgress(taskProgress)); @@ -224,12 +222,16 @@ public class BackgroundExecutor { } } - private void performCancel(ThreadPoolExecutor executor) throws InterruptedException { + private void performCancel() throws InterruptedException { progressPane.changeLabel(this, task.getTitle() + " (" + NLS.str("progress.canceling") + ")… "); progressPane.changeIndeterminate(this, true); // force termination + ExecutorService executor = taskExecutor.getInternalExecutor(); + if (executor == null) { + return; + } + taskExecutor.terminate(); task.cancel(); - executor.shutdown(); int cancelTimeout = task.getCancelTimeoutMS(); if (cancelTimeout != 0) { if (executor.awaitTermination(cancelTimeout, TimeUnit.MILLISECONDS)) { @@ -240,8 +242,9 @@ public class BackgroundExecutor { LOG.debug("Forcing tasks cancel"); executor.shutdownNow(); boolean complete = executor.awaitTermination(task.getShutdownTimeoutMS(), TimeUnit.MILLISECONDS); - LOG.debug("Forced task cancel status: {}", - complete ? "success" : "fail, still active: " + executor.getActiveCount()); + LOG.debug("Forced task cancel status: {}", complete + ? "success" + : "fail, still active: " + (taskExecutor.getTasksCount() - taskExecutor.getProgress())); } private Supplier buildCancelCheck(long startTime) { @@ -261,13 +264,13 @@ public class BackgroundExecutor { } if (checkMemoryUsage && !UiUtils.isFreeMemoryAvailable()) { LOG.info("Memory usage: {}", UiUtils.memoryInfo()); - if (executor.getCorePoolSize() == 1) { + if (taskExecutor.getThreadsCount() == 1) { LOG.error("Task '{}' memory limit reached, force cancel", task.getTitle()); return TaskStatus.CANCEL_BY_MEMORY; } LOG.warn("Low memory, reduce processing threads count to 1"); - // reduce thread count and continue - executor.setCorePoolSize(1); + // reduce threads count and continue + taskExecutor.setThreadsCount(1); System.gc(); UiUtils.sleep(1000); // wait GC if (!UiUtils.isFreeMemoryAvailable()) { 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 01bd04d6f..79cd6634a 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java @@ -12,6 +12,8 @@ import org.slf4j.LoggerFactory; import jadx.api.ICodeCache; import jadx.api.JavaClass; +import jadx.api.utils.tasks.ITaskExecutor; +import jadx.core.utils.tasks.TaskExecutor; import jadx.gui.JadxWrapper; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; @@ -44,6 +46,12 @@ public class DecompileTask extends CancelableBackgroundTask { } @Override + public ITaskExecutor scheduleTasks() { + TaskExecutor executor = new TaskExecutor(); + executor.addParallelTasks(scheduleJobs()); + return executor; + } + public List scheduleJobs() { if (mainWindow.getCacheObject().isFullDecompilationFinished()) { return Collections.emptyList(); @@ -60,6 +68,10 @@ public class DecompileTask extends CancelableBackgroundTask { LOG.error("Decompile batches build error", e); return Collections.emptyList(); } + return getJobs(batches); + } + + private List getJobs(List> batches) { ICodeCache codeCache = wrapper.getArgs().getCodeCache(); List jobs = new ArrayList<>(batches.size()); for (List batch : batches) { diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/ExportTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/ExportTask.java index 70b579618..7f3ccf588 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/ExportTask.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/ExportTask.java @@ -1,11 +1,11 @@ package jadx.gui.jobs; import java.io.File; -import java.util.List; import javax.swing.JOptionPane; import jadx.api.ICodeCache; +import jadx.api.utils.tasks.ITaskExecutor; import jadx.gui.JadxWrapper; import jadx.gui.cache.code.FixedCodeCache; import jadx.gui.ui.MainWindow; @@ -32,11 +32,11 @@ public class ExportTask extends CancelableBackgroundTask { } @Override - public List scheduleJobs() { + public ITaskExecutor scheduleTasks() { wrapCodeCache(); wrapper.getArgs().setRootDir(saveDir); - List saveTasks = wrapper.getSaveTasks(); - this.timeLimit = DecompileTask.calcDecompileTimeLimit(saveTasks.size()); + ITaskExecutor saveTasks = wrapper.getDecompiler().getSaveTaskExecutor(); + this.timeLimit = DecompileTask.calcDecompileTimeLimit(saveTasks.getTasksCount()); return saveTasks; } 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 bf26666b9..b1ede9d38 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java @@ -1,18 +1,16 @@ package jadx.gui.jobs; -import java.util.List; import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; +import jadx.api.utils.tasks.ITaskExecutor; + public interface IBackgroundTask extends Cancelable { String getTitle(); - /** - * Jobs to run in parallel - */ - List scheduleJobs(); + ITaskExecutor scheduleTasks(); /** * Called on executor thread after the all jobs finished. 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 7352cbbb0..9acbe073e 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/SimpleTask.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/SimpleTask.java @@ -5,6 +5,9 @@ import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; +import jadx.api.utils.tasks.ITaskExecutor; +import jadx.core.utils.tasks.TaskExecutor; + /** * Simple not cancelable task with memory check */ @@ -29,8 +32,10 @@ public class SimpleTask implements IBackgroundTask { } @Override - public List scheduleJobs() { - return jobs; + public ITaskExecutor scheduleTasks() { + TaskExecutor executor = new TaskExecutor(); + executor.addParallelTasks(jobs); + return executor; } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/search/SearchTask.java b/jadx-gui/src/main/java/jadx/gui/search/SearchTask.java index 02d9fb029..572189f89 100644 --- a/jadx-gui/src/main/java/jadx/gui/search/SearchTask.java +++ b/jadx-gui/src/main/java/jadx/gui/search/SearchTask.java @@ -14,6 +14,8 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.api.utils.tasks.ITaskExecutor; +import jadx.core.utils.tasks.TaskExecutor; import jadx.gui.jobs.BackgroundExecutor; import jadx.gui.jobs.CancelableBackgroundTask; import jadx.gui.jobs.ITaskInfo; @@ -98,8 +100,10 @@ public class SearchTask extends CancelableBackgroundTask { } @Override - public List scheduleJobs() { - return jobs; + public ITaskExecutor scheduleTasks() { + TaskExecutor executor = new TaskExecutor(); + executor.addParallelTasks(jobs); + return executor; } @Override