未验证 提交 65f7c802 编写于 作者: S Skylot

feat(gui): add reload and live reload actions (#1537)

上级 d2e6bb23
......@@ -269,6 +269,7 @@ public class RootNode {
public void runPreDecompileStage() {
boolean debugEnabled = LOG.isDebugEnabled();
for (IDexTreeVisitor pass : preDecompilePasses) {
Utils.checkThreadInterrupt();
long start = debugEnabled ? System.currentTimeMillis() : 0;
try {
pass.init(this);
......
......@@ -428,7 +428,7 @@ public class Utils {
}
public static void checkThreadInterrupt() {
if (Thread.interrupted()) {
if (Thread.currentThread().isInterrupted()) {
throw new JadxRuntimeException("Thread interrupted");
}
}
......
......@@ -3,6 +3,7 @@ package jadx.gui.jobs;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
......@@ -11,6 +12,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
......@@ -19,6 +21,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.settings.JadxSettings;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.panel.ProgressPanel;
import jadx.gui.utils.NLS;
......@@ -33,16 +36,16 @@ import static jadx.gui.utils.UiUtils.calcProgress;
public class BackgroundExecutor {
private static final Logger LOG = LoggerFactory.getLogger(BackgroundExecutor.class);
private final MainWindow mainWindow;
private final JadxSettings settings;
private final ProgressPanel progressPane;
private ThreadPoolExecutor taskQueueExecutor;
private final Map<Long, IBackgroundTask> taskRunning = new ConcurrentHashMap<>();
private final AtomicLong idSupplier = new AtomicLong(0);
public BackgroundExecutor(MainWindow mainWindow) {
this.mainWindow = mainWindow;
this.progressPane = mainWindow.getProgressPane();
public BackgroundExecutor(JadxSettings settings, ProgressPanel progressPane) {
this.settings = Objects.requireNonNull(settings);
this.progressPane = Objects.requireNonNull(progressPane);
reset();
}
......@@ -68,9 +71,16 @@ public class BackgroundExecutor {
public synchronized void cancelAll() {
try {
taskRunning.values().forEach(Cancelable::cancel);
taskQueueExecutor.shutdown();
boolean complete = taskQueueExecutor.awaitTermination(30, TimeUnit.SECONDS);
LOG.debug("Background task executor terminated with status: {}", complete ? "complete" : "interrupted");
taskQueueExecutor.shutdownNow();
boolean complete = taskQueueExecutor.awaitTermination(3, TimeUnit.SECONDS);
if (complete) {
LOG.debug("Background task executor canceled successfully");
} else {
String taskNames = taskRunning.values().stream()
.map(IBackgroundTask::getTitle)
.collect(Collectors.joining(", "));
LOG.debug("Background task executor cancel failed. Running tasks: {}", taskNames);
}
} catch (Exception e) {
LOG.error("Error terminating task executor", e);
} finally {
......@@ -131,8 +141,16 @@ public class BackgroundExecutor {
try {
runJobs();
} finally {
taskComplete(id);
task.onDone(this);
try {
task.onDone(this);
// treat UI task operations as part of the task to not mix with others
UiUtils.uiRunAndWait(() -> {
progressPane.setVisible(false);
task.onFinish(this);
});
} finally {
taskComplete(id);
}
}
return status;
}
......@@ -146,7 +164,7 @@ public class BackgroundExecutor {
progressPane.changeVisibility(this, true);
}
status = TaskStatus.STARTED;
int threadsCount = mainWindow.getSettings().getThreadsCount();
int threadsCount = settings.getThreadsCount();
executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadsCount);
for (Runnable job : jobs) {
executor.execute(job);
......@@ -258,12 +276,6 @@ public class BackgroundExecutor {
};
}
@Override
protected void done() {
progressPane.setVisible(false);
task.onFinish(this);
}
@Override
public TaskStatus getStatus() {
return status;
......
......@@ -169,6 +169,17 @@ public class JadxProject {
throw new JadxRuntimeException("Can't get working dir");
}
public boolean isEnableLiveReload() {
return data.isEnableLiveReload();
}
public void setEnableLiveReload(boolean newValue) {
if (newValue != data.isEnableLiveReload()) {
data.setEnableLiveReload(newValue);
changed();
}
}
private void changed() {
JadxSettings settings = mainWindow.getSettings();
if (settings != null && settings.isAutoSaveProject()) {
......
......@@ -19,6 +19,7 @@ public class ProjectData {
private List<TabViewState> openTabs = Collections.emptyList();
private int activeTab = -1;
private @Nullable Path cacheDir;
private boolean enableLiveReload = false;
public List<Path> getFiles() {
return files;
......@@ -94,4 +95,12 @@ public class ProjectData {
public void setCacheDir(Path cacheDir) {
this.cacheDir = cacheDir;
}
public boolean isEnableLiveReload() {
return enableLiveReload;
}
public void setEnableLiveReload(boolean enableLiveReload) {
this.enableLiveReload = enableLiveReload;
}
}
......@@ -16,6 +16,7 @@ import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
......@@ -135,7 +136,9 @@ import jadx.gui.utils.Link;
import jadx.gui.utils.NLS;
import jadx.gui.utils.SystemInfo;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.fileswatcher.LiveReloadWorker;
import jadx.gui.utils.logs.LogCollector;
import jadx.gui.utils.ui.ActionHandler;
import static io.reactivex.internal.functions.Functions.EMPTY_RUNNABLE;
import static javax.swing.KeyStroke.getKeyStroke;
......@@ -152,6 +155,7 @@ public class MainWindow extends JFrame {
private static final ImageIcon ICON_OPEN = UiUtils.openSvgIcon("ui/openDisk");
private static final ImageIcon ICON_ADD_FILES = UiUtils.openSvgIcon("ui/addFile");
private static final ImageIcon ICON_SAVE_ALL = UiUtils.openSvgIcon("ui/menu-saveall");
private static final ImageIcon ICON_RELOAD = UiUtils.openSvgIcon("ui/refresh");
private static final ImageIcon ICON_EXPORT = UiUtils.openSvgIcon("ui/export");
private static final ImageIcon ICON_EXIT = UiUtils.openSvgIcon("ui/exit");
private static final ImageIcon ICON_SYNC = UiUtils.openSvgIcon("ui/pagination");
......@@ -196,6 +200,9 @@ public class MainWindow extends JFrame {
private JToggleButton deobfToggleBtn;
private JCheckBoxMenuItem deobfMenuItem;
private JCheckBoxMenuItem liveReloadMenuItem;
private final LiveReloadWorker liveReloadWorker;
private transient Link updateLink;
private transient ProgressPanel progressPane;
private transient Theme editorTheme;
......@@ -208,18 +215,18 @@ public class MainWindow extends JFrame {
this.cacheObject = new CacheObject();
this.project = new JadxProject(this);
this.wrapper = new JadxWrapper(this);
this.liveReloadWorker = new LiveReloadWorker(this);
resetCache();
FontUtils.registerBundledFonts();
initUI();
this.backgroundExecutor = new BackgroundExecutor(settings, progressPane);
initMenuAndToolbar();
registerMouseNavigationButtons();
UiUtils.setWindowIcons(this);
loadSettings();
update();
this.backgroundExecutor = new BackgroundExecutor(this);
update();
checkForUpdate();
}
......@@ -405,7 +412,7 @@ public class MainWindow extends JFrame {
return loadedFile.resolveSibling(fileName);
}
public void reopen() {
public synchronized void reopen() {
saveAll();
closeAll();
loadFiles(EMPTY_RUNNABLE);
......@@ -439,6 +446,10 @@ public class MainWindow extends JFrame {
UiUtils.errorMessage(this, NLS.str("message.memoryLow"));
return;
}
if (status != TaskStatus.COMPLETE) {
LOG.warn("Loading task incomplete, status: {}", status);
return;
}
checkLoadedStatus();
onOpen();
exportMappingsMenu.setEnabled(true);
......@@ -485,6 +496,7 @@ public class MainWindow extends JFrame {
deobfToggleBtn.setSelected(settings.isDeobfuscationOn());
initTree();
update();
updateLiveReload(project.isEnableLiveReload());
BreakpointManager.init(project.getFilePaths().get(0).toAbsolutePath().getParent());
backgroundExecutor.execute(NLS.str("progress.load"),
......@@ -492,6 +504,21 @@ public class MainWindow extends JFrame {
status -> runInitialBackgroundJobs());
}
public void updateLiveReload(boolean state) {
if (liveReloadWorker.isStarted() == state) {
return;
}
project.setEnableLiveReload(state);
liveReloadMenuItem.setEnabled(false);
backgroundExecutor.execute(
(state ? "Starting" : "Stopping") + " live reload",
() -> liveReloadWorker.updateState(state),
s -> {
liveReloadMenuItem.setState(state);
liveReloadMenuItem.setEnabled(true);
});
}
private void addTreeCustomNodes() {
treeRoot.replaceCustomNode(ApkSignature.getApkSignature(wrapper));
treeRoot.replaceCustomNode(new SummaryNode(this));
......@@ -829,6 +856,19 @@ public class MainWindow extends JFrame {
};
saveProjectAsAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.save_project_as"));
ActionHandler reload = new ActionHandler(ev -> UiUtils.uiRun(this::reopen));
reload.setNameAndDesc(NLS.str("file.reload"));
reload.setIcon(ICON_RELOAD);
reload.setKeyBinding(getKeyStroke(KeyEvent.VK_F5, 0));
ActionHandler liveReload = new ActionHandler(ev -> updateLiveReload(!project.isEnableLiveReload()));
liveReload.setName(NLS.str("file.live_reload"));
liveReload.setShortDescription(NLS.str("file.live_reload_desc"));
liveReload.setKeyBinding(getKeyStroke(KeyEvent.VK_F5, InputEvent.SHIFT_DOWN_MASK));
liveReloadMenuItem = new JCheckBoxMenuItem(liveReload);
liveReloadMenuItem.setState(project.isEnableLiveReload());
Action exportMappingsAsTiny2 = new AbstractAction("Tiny v2 file") {
@Override
public void actionPerformed(ActionEvent e) {
......@@ -1045,6 +1085,9 @@ public class MainWindow extends JFrame {
file.add(saveProjectAction);
file.add(saveProjectAsAction);
file.addSeparator();
file.add(reload);
file.add(liveReloadMenuItem);
file.addSeparator();
file.add(exportMappingsMenu);
file.addSeparator();
file.add(saveAllAction);
......@@ -1114,6 +1157,8 @@ public class MainWindow extends JFrame {
toolbar.add(openAction);
toolbar.add(addFilesAction);
toolbar.addSeparator();
toolbar.add(reload);
toolbar.addSeparator();
toolbar.add(saveAllAction);
toolbar.add(exportAction);
toolbar.addSeparator();
......@@ -1452,10 +1497,6 @@ public class MainWindow extends JFrame {
return backgroundExecutor;
}
public ProgressPanel getProgressPane() {
return progressPane;
}
public JRoot getTreeRoot() {
return treeRoot;
}
......
......@@ -11,6 +11,7 @@ import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.Objects;
import javax.swing.AbstractAction;
import javax.swing.Action;
......@@ -75,7 +76,7 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
public AbstractCodeArea(ContentPanel contentPanel, JNode node) {
this.contentPanel = contentPanel;
this.node = node;
this.node = Objects.requireNonNull(node);
setMarkOccurrences(false);
setEditable(false);
......
package jadx.gui.utils.fileswatcher;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.Utils;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
public class FilesWatcher {
private static final Logger LOG = LoggerFactory.getLogger(FilesWatcher.class);
private final WatchService watcher = FileSystems.getDefault().newWatchService();
private final Map<WatchKey, Path> keys = new HashMap<>();
private final Map<Path, Set<Path>> files = new HashMap<>();
private final AtomicBoolean cancel = new AtomicBoolean(false);
private final BiConsumer<Path, WatchEvent.Kind<Path>> listener;
public FilesWatcher(List<Path> paths, BiConsumer<Path, WatchEvent.Kind<Path>> listener) throws IOException {
this.listener = listener;
for (Path path : paths) {
if (Files.isDirectory(path, NOFOLLOW_LINKS)) {
registerDirs(path);
} else {
Path parentDir = path.toAbsolutePath().getParent();
register(parentDir);
files.merge(parentDir, Collections.singleton(path), Utils::mergeSets);
}
}
}
public void cancel() {
cancel.set(true);
}
@SuppressWarnings("unchecked")
public void watch() {
cancel.set(false);
LOG.debug("File watcher started for {} dirs", keys.size());
while (!cancel.get()) {
WatchKey key;
try {
key = watcher.take();
} catch (InterruptedException e) {
LOG.debug("File watcher interrupted");
return;
}
Path dir = keys.get(key);
if (dir == null) {
LOG.warn("Unknown directory key: {}", key);
continue;
}
for (WatchEvent<?> event : key.pollEvents()) {
if (cancel.get() || Thread.interrupted()) {
return;
}
WatchEvent.Kind<?> kind = event.kind();
if (kind == OVERFLOW) {
continue;
}
Path fileName = ((WatchEvent<Path>) event).context();
Path path = dir.resolve(fileName);
Set<Path> files = this.files.get(dir);
if (files == null || files.contains(path)) {
listener.accept(path, (WatchEvent.Kind<Path>) kind);
}
if (kind == ENTRY_CREATE) {
try {
if (Files.isDirectory(path, NOFOLLOW_LINKS)) {
registerDirs(path);
}
} catch (Exception e) {
LOG.warn("Failed to update directory watch: {}", path, e);
}
}
}
boolean valid = key.reset();
if (!valid) {
keys.remove(key);
if (keys.isEmpty()) {
LOG.debug("File watcher stopped: all watch keys removed");
return;
}
}
}
}
private void registerDirs(Path start) throws IOException {
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
register(dir);
return FileVisitResult.CONTINUE;
}
});
}
private void register(Path dir) throws IOException {
WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
keys.put(key, dir);
}
}
package jadx.gui.utils.fileswatcher;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.reactivex.processors.PublishProcessor;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.UiUtils;
public class LiveReloadWorker {
private static final Logger LOG = LoggerFactory.getLogger(LiveReloadWorker.class);
private final MainWindow mainWindow;
private final PublishProcessor<Path> processor;
private volatile boolean started = false;
private ExecutorService executor;
private FilesWatcher watcher;
@SuppressWarnings("ResultOfMethodCallIgnored")
public LiveReloadWorker(MainWindow mainWindow) {
this.mainWindow = mainWindow;
this.processor = PublishProcessor.create();
this.processor
.debounce(1, TimeUnit.SECONDS)
.subscribe(path -> {
LOG.debug("Reload triggered");
UiUtils.uiRun(mainWindow::reopen);
});
}
public boolean isStarted() {
return started;
}
public synchronized void updateState(boolean enabled) {
if (this.started == enabled) {
return;
}
if (enabled) {
LOG.debug("Starting live reload worker");
start();
} else {
LOG.debug("Stopping live reload worker");
stop();
}
}
private void onUpdate(Path path, WatchEvent.Kind<Path> pathKind) {
LOG.debug("Path updated: {}", path);
processor.onNext(path);
}
private synchronized void start() {
try {
watcher = new FilesWatcher(mainWindow.getProject().getFilePaths(), this::onUpdate);
executor = Executors.newSingleThreadExecutor();
started = true;
executor.submit(watcher::watch);
} catch (Exception e) {
LOG.warn("Failed to start live reload worker", e);
resetState();
}
}
private synchronized void stop() {
try {
watcher.cancel();
executor.shutdownNow();
boolean canceled = executor.awaitTermination(5, TimeUnit.SECONDS);
if (!canceled) {
LOG.warn("Failed to cancel live reload worker");
}
} catch (Exception e) {
LOG.warn("Failed to stop live reload worker", e);
} finally {
resetState();
}
}
private void resetState() {
started = false;
executor = null;
watcher = null;
}
}
......@@ -4,6 +4,8 @@ import java.awt.event.ActionEvent;
import java.util.function.Consumer;
import javax.swing.AbstractAction;
import javax.swing.ImageIcon;
import javax.swing.KeyStroke;
public class ActionHandler extends AbstractAction {
......@@ -13,6 +15,27 @@ public class ActionHandler extends AbstractAction {
this.consumer = consumer;
}
public void setName(String name) {
putValue(NAME, name);
}
public void setNameAndDesc(String name) {
setName(name);
setShortDescription(name);
}
public void setShortDescription(String desc) {
putValue(SHORT_DESCRIPTION, desc);
}
public void setIcon(ImageIcon icon) {
putValue(SMALL_ICON, icon);
}
public void setKeyBinding(KeyStroke keyStroke) {
putValue(ACCELERATOR_KEY, keyStroke);
}
@Override
public void actionPerformed(ActionEvent e) {
consumer.accept(e);
......
......@@ -26,6 +26,9 @@ file.open_title=Datei öffnen
file.new_project=Neues Projekt
file.save_project=Projekt speichern
file.save_project_as=Projekt speichern als…
#file.reload=Reload files
#file.live_reload=Live reload
#file.live_reload_desc=Auto reload files on changes
#file.export_mappings_as=
file.save_all=Alles speichern
file.export_gradle=Als Gradle-Projekt speichern
......
......@@ -26,6 +26,9 @@ file.open_title=Open file
file.new_project=New project
file.save_project=Save project
file.save_project_as=Save project as...
file.reload=Reload files
file.live_reload=Live reload
file.live_reload_desc=Auto reload files on changes
file.export_mappings_as=Export mappings as...
file.save_all=Save all
file.export_gradle=Save as gradle project
......
......@@ -26,6 +26,9 @@ file.open_title=Abrir archivo
#file.new_project=
#file.save_project=
#file.save_project_as=
#file.reload=Reload files
#file.live_reload=Live reload
#file.live_reload_desc=Auto reload files on changes
#file.export_mappings_as=
file.save_all=Guardar todo
file.export_gradle=Guardar como proyecto Gradle
......
......@@ -26,6 +26,9 @@ file.open_title=파일 열기
file.new_project=새 프로젝트
file.save_project=프로젝트 저장
file.save_project_as=다른 이름으로 프로젝트 저장...
#file.reload=Reload files
#file.live_reload=Live reload
#file.live_reload_desc=Auto reload files on changes
#file.export_mappings_as=
file.save_all=모두 저장
file.export_gradle=Gradle 프로젝트로 저장
......
......@@ -26,6 +26,9 @@ file.open_title=打开文件
file.new_project=新建项目
file.save_project=保存项目
file.save_project_as=另存项目为...
#file.reload=Reload files
#file.live_reload=Live reload
#file.live_reload_desc=Auto reload files on changes
#file.export_mappings_as=
file.save_all=全部保存
file.export_gradle=另存为 Gradle 项目
......
......@@ -26,6 +26,9 @@ file.open_title=開啟檔案
file.new_project=新建專案
file.save_project=儲存專案
file.save_project_as=另存專案...
#file.reload=Reload files
#file.live_reload=Live reload
#file.live_reload_desc=Auto reload files on changes
#file.export_mappings_as=
file.save_all=全部儲存
file.export_gradle=另存為 gradle 專案
......
<!-- Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#6E6E6E" fill-rule="evenodd" d="M12.5747152,11.8852806 C11.4741474,13.1817355 9.83247882,14.0044386 7.99865879,14.0044386 C5.03907292,14.0044386 2.57997332,11.8615894 2.08820756,9.0427473 L3.94774327,9.10768372 C4.43372186,10.8898575 6.06393114,12.2000519 8.00015362,12.2000519 C9.30149237,12.2000519 10.4645985,11.6082097 11.2349873,10.6790094 L9.05000019,8.71167959 L14.0431479,8.44999981 L14.3048222,13.4430431 L12.5747152,11.8852806 Z M3.42785637,4.11741586 C4.52839138,2.82452748 6.16775464,2.00443857 7.99865879,2.00443857 C10.918604,2.00443857 13.3513802,4.09026967 13.8882946,6.8532307 L12.0226389,6.78808057 C11.5024872,5.05935553 9.89838095,3.8000774 8.00015362,3.8000774 C6.69867367,3.8000774 5.53545628,4.39204806 4.76506921,5.32142241 L6.95482203,7.29304326 L1.96167436,7.55472304 L1.70000005,2.56167973 L3.42785637,4.11741586 Z" transform="rotate(3 8.002 8.004)"/>
</svg>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册