提交 1c08d854 编写于 作者: S Skylot

fix(gui): add memory limit checks to export and load tasks (#1181)

上级 9c252fb2
......@@ -207,16 +207,24 @@ public final class JadxDecompiler implements Closeable {
return getSaveExecutor(!args.isSkipSources(), !args.isSkipResources());
}
public List<Runnable> getSaveTasks() {
return getSaveTasks(!args.isSkipSources(), !args.isSkipResources());
}
private ExecutorService getSaveExecutor(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);
List<Runnable> tasks = getSaveTasks(saveSources, saveResources);
tasks.forEach(executor::execute);
return executor;
}
private List<Runnable> getSaveTasks(boolean saveSources, boolean saveResources) {
if (root == null) {
throw new JadxRuntimeException("No loaded files");
}
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<Runnable> 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<Runnable> tasks, File outDir) {
Set<String> 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<Runnable> tasks, File outDir) {
Predicate<String> 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);
......
......@@ -29,6 +29,6 @@ public class InMemoryCodeCache implements ICodeCache {
@Override
public String toString() {
return "InMemoryCodeCache";
return "InMemoryCodeCache: size=" + storage.size();
}
}
......@@ -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<ClassInfo, ClassNode> clsMap = new HashMap<>();
private List<ClassNode> 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() {
......
......@@ -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 <T> boolean notEmpty(T[] arr) {
return arr != null && arr.length != 0;
}
public static void checkThreadInterrupt() {
if (Thread.interrupted()) {
throw new JadxRuntimeException("Thread interrupted");
}
}
}
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
*/
......
......@@ -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<Runnable> backgroundJobs, Runnable onFinishUiRunnable) {
public void execute(String title, List<Runnable> backgroundJobs, Consumer<TaskStatus> 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<TaskStatus> 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<TaskStatus, Void> {
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<Runnable> 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<TaskStatus> 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<TaskStatus> 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;
}
if (isCancelled() || Thread.currentThread().isInterrupted()) {
LOG.warn("Task '{}' canceled", task.getTitle());
return TaskStatus.CANCEL_BY_USER;
}
return simpleCancelCheck();
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<Runnable> jobs;
private final Runnable onFinish;
private final Consumer<TaskStatus> onFinish;
public SimpleTask(String title, List<Runnable> jobs, @Nullable Runnable onFinish) {
public SimpleTask(String title, List<Runnable> jobs, @Nullable Consumer<TaskStatus> 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;
}
}
}
......@@ -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
......
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<Runnable> scheduleJobs() {
wrapCodeCache();
JadxDecompiler decompiler = wrapper.getDecompiler();
decompiler.getArgs().setRootDir(saveDir);
List<Runnable> 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;
}
}
......@@ -8,7 +8,7 @@ public interface IBackgroundTask {
List<Runnable> scheduleJobs();
void onFinish(TaskStatus status);
void onFinish(TaskStatus status, long skipped);
default boolean canBeCanceled() {
return false;
......
......@@ -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
......
......@@ -6,5 +6,6 @@ public enum TaskStatus {
COMPLETE,
CANCEL_BY_USER,
CANCEL_BY_TIMEOUT,
CANCEL_BY_MEMORY
CANCEL_BY_MEMORY,
ERROR
}
......@@ -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()));
}
}
......
......@@ -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();
......
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
}
}
......@@ -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) {
......
......@@ -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);
}
}
......@@ -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=<html>Save incomplete.<br> %s<br> %d classes or resources were not saved!</html>
#message.indexIncomplete=<html>Index of some classes skipped.<br> %s<br> %d classes were not indexed and will not appear in search results!</html>
message.indexingClassesSkipped=<html>Jadx hat nur noch wenig Speicherplatz. Daher wurden %d Klassen nicht indiziert.<br>Wenn Sie möchten, dass alle Klassen indiziert werden, Jadx mit erhöhter maximaler Heap-Größe neustarten.</html>
......@@ -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!
......
......@@ -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=<html>Save incomplete.<br> %s<br> %d classes or resources were not saved!</html>
message.indexIncomplete=<html>Index of some classes skipped.<br> %s<br> %d classes were not indexed and will not appear in search results!</html>
message.indexingClassesSkipped=<html>Jadx is running low on memory. Therefore %d classes were not indexed.<br>If you want all classes to be indexed restart Jadx with increased maximum heap size.</html>
......@@ -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!
......
......@@ -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=<html>Save incomplete.<br> %s<br> %d classes or resources were not saved!</html>
#message.indexIncomplete=<html>Index of some classes skipped.<br> %s<br> %d classes were not indexed and will not appear in search results!</html>
#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á!
......
......@@ -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=<html>Save incomplete.<br> %s<br> %d classes or resources were not saved!</html>
#message.indexIncomplete=<html>Index of some classes skipped.<br> %s<br> %d classes were not indexed and will not appear in search results!</html>
message.indexingClassesSkipped=<html>Jadx의 메모리가 부족합니다. 따라서 %d 개의 클래스가 인덱싱되지 않았습니다. <br> 모든 클래스를 인덱싱하려면 최대 힙 크기를 늘린 상태로 Jadx를 다시 시작하십시오.</html>
......@@ -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=인덱스가 초기화되지 않았습니다. 검색이 비활성화됩니다!
......
......@@ -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=<html>Save incomplete.<br> %s<br> %d classes or resources were not saved!</html>
#message.indexIncomplete=<html>Index of some classes skipped.<br> %s<br> %d classes were not indexed and will not appear in search results!</html>
message.indexingClassesSkipped=<html>Jadx 的内存不足。因此,%d 类没有编入索引。<br>如果要将所有类编入索引,请使用增加的最大堆大小重新启动 Jadx。</html>
......@@ -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=索引尚未初始化,无法进行搜索!
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册