GP-5585 non blocking project index for the FileSystemBrowser

This commit is contained in:
dev747368 2025-04-17 10:57:56 -04:00
parent 1ef5566219
commit a3a0870b66
4 changed files with 125 additions and 69 deletions

View file

@ -23,21 +23,59 @@ import ghidra.formats.gfilesystem.FSRL;
import ghidra.framework.main.datatable.ProjectDataTablePanel;
import ghidra.framework.model.*;
import ghidra.util.Swing;
import ghidra.util.task.TaskMonitor;
import ghidra.util.datastruct.ListenerSet;
/**
* An in-memory index of FSRL-to-domainfile in the current project.
* An in-memory index of FSRL-to-domainfiles.
*/
public class ProjectIndexService implements DomainFolderChangeListener {
public class ProjectIndexService implements DomainFolderChangeListener, AutoCloseable {
public static ProjectIndexService getInstance() {
return SingletonHolder.instance;
public static final ProjectIndexService DUMMY = new ProjectIndexService(null);
public interface ProjectIndexListener {
void indexUpdated();
}
private static class SingletonHolder {
private static final ProjectIndexService instance = new ProjectIndexService();
/**
* Returns an index for a Project. Instances returned by this method should not be
* {@link #close() closed} by the caller.
*
* @param project {@link Project} to get index for, or {@code null} for a DUMMY placeholder
* @return {@link ProjectIndexService} instance, never null
*/
public static synchronized ProjectIndexService getIndexFor(Project project) {
if (project == null || project.isClosed()) {
return DUMMY;
}
ProjectIndexService result = instances.get(project.getProjectLocator());
if (result == null) {
result = new ProjectIndexService(project);
instances.put(project.getProjectLocator(), result);
}
return result;
}
/**
* Notify the index instance management that a Project has been closed. Users of this service
* need to do this because notification of Project closure is only available to GUI Plugin
* classes.
*
* @param project {@link Project} that was closed
*/
public static synchronized void projectClosed(Project project) {
if (project != null) {
ProjectLocator projectLocator = project.getProjectLocator();
ProjectIndexService result = instances.get(projectLocator);
if (result != null) {
instances.remove(projectLocator);
result.close();
}
}
}
private static Map<ProjectLocator, ProjectIndexService> instances = new HashMap<>();
public enum IndexType {
MD5("Executable MD5"), FSRL("FSRL");
@ -68,72 +106,85 @@ public class ProjectIndexService implements DomainFolderChangeListener {
}
private Project project;
private List<IndexInfo> indexes;
private List<IndexInfo> indexes = List.of(new IndexInfo(IndexType.MD5, this::getMD5),
new IndexInfo(IndexType.FSRL, this::getFSRL));
private Thread indexThread;
private ListenerSet<ProjectIndexListener> indexListeners =
new ListenerSet<>(ProjectIndexListener.class, false);
private ProjectIndexService() {
this.indexes = List.of(new IndexInfo(IndexType.MD5, this::getMD5),
new IndexInfo(IndexType.FSRL, this::getFSRL));
public ProjectIndexService(Project project) {
this.project = project;
if (project != null) {
ProjectData projectData = project.getProjectData();
projectData.addDomainFolderChangeListener(this);
indexProject(projectData);
}
}
public synchronized void clearProject() {
@Override
public synchronized void close() {
if (project != null) {
project.getProjectData().removeDomainFolderChangeListener(this);
for (IndexInfo index : indexes) {
index.indexedFiles.clear();
}
project = null;
indexListeners.clear();
Thread localIndexThread = indexThread;
if (localIndexThread != null && localIndexThread.isAlive()) {
localIndexThread.interrupt();
}
}
}
public void setProject(Project newProject, TaskMonitor monitor) {
synchronized (this) {
if (newProject == project) {
return;
}
clearProject();
project = newProject;
if (project != null) {
indexes = List.of(new IndexInfo(IndexType.MD5, this::getMD5),
new IndexInfo(IndexType.FSRL, this::getFSRL));
ProjectData projectData = project.getProjectData();
projectData.removeDomainFolderChangeListener(this);
projectData.addDomainFolderChangeListener(this);
}
public synchronized void addIndexListener(ProjectIndexListener listener) {
if (project != null) {
indexListeners.add(listener);
}
}
if (newProject != null) {
// index outside of sync lock to allow concurrent lookups
indexProject(newProject.getProjectData(), monitor);
}
public synchronized void removeIndexListener(ProjectIndexListener listener) {
indexListeners.remove(listener);
}
@Override
public void domainFileAdded(DomainFile file) {
indexFile(file);
indexListeners.invoke().indexUpdated();
}
@Override
public void domainFileRemoved(DomainFolder parent, String name, String fileID) {
removeFile(fileID);
indexListeners.invoke().indexUpdated();
}
private void indexProject(ProjectData projectData, TaskMonitor monitor) {
private void indexProject(ProjectData projectData) {
int fileCount = projectData.getFileCount();
if (fileCount < 0 || fileCount > ProjectDataTablePanel.MAX_FILE_COUNT) {
return;
}
monitor.initialize(fileCount, "Indexing Project Metadata");
for (DomainFile df : ProjectDataUtils.descendantFiles(projectData.getRootFolder())) {
monitor.incrementProgress();
if (monitor.isCancelled()) {
break;
indexThread = new Thread(() -> {
int count = 0;
for (DomainFile df : ProjectDataUtils.descendantFiles(projectData.getRootFolder())) {
if (indexThread.isInterrupted()) {
break;
}
indexFile(df);
if (count++ % 10 == 0) {
indexListeners.invoke().indexUpdated();
Swing.allowSwingToProcessEvents();
}
}
indexFile(df);
if (monitor.getProgress() % 10 == 0) {
Swing.allowSwingToProcessEvents();
}
}
indexThread = null;
indexListeners.invoke().indexUpdated();
}, "Project Indexing Thread");
indexThread.setDaemon(true);
indexThread.start();
}
private String getMD5(DomainFile file, Map<String, String> metadata) {
@ -176,19 +227,23 @@ public class ProjectIndexService implements DomainFolderChangeListener {
}
private synchronized void indexFile(DomainFile file) {
String newFileId = file.getFileID();
if (newFileId == null) {
return;
}
Map<String, String> metadata = file.getMetadata();
for (IndexInfo index : indexes) {
Object indexedValue = index.mappingFunc.apply(file, metadata);
if (indexedValue != null) {
Object fileInfo = index.indexedFiles.get(indexedValue);
if (fileInfo == null) {
index.indexedFiles.put(indexedValue, file.getFileID());
index.indexedFiles.put(indexedValue, newFileId);
}
else if (fileInfo instanceof List<?> fileInfoList) {
((List<String>) fileInfoList).add(file.getFileID());
((List<String>) fileInfoList).add(newFileId);
}
else if (fileInfo instanceof String prevFileId) {
String newFileId = file.getFileID();
if (newFileId.equals(prevFileId)) {
// don't need to do anything
continue;
@ -205,8 +260,7 @@ public class ProjectIndexService implements DomainFolderChangeListener {
private synchronized void removeFile(String fileId) {
// brute force search through all entries to remove the file
for (IndexInfo index : indexes) {
for (Iterator<Object> it = index.indexedFiles.values().iterator(); it
.hasNext();) {
for (Iterator<Object> it = index.indexedFiles.values().iterator(); it.hasNext();) {
Object fileInfo = it.next();
if (fileInfo instanceof String fileIdStr && fileIdStr.equals(fileId)) {
it.remove();

View file

@ -46,6 +46,7 @@ import ghidra.framework.model.*;
import ghidra.framework.plugintool.ComponentProviderAdapter;
import ghidra.plugin.importer.ImporterUtilities;
import ghidra.plugin.importer.ProjectIndexService;
import ghidra.plugin.importer.ProjectIndexService.ProjectIndexListener;
import ghidra.program.model.listing.Program;
import ghidra.util.*;
import ghidra.util.classfinder.ClassSearcher;
@ -62,12 +63,12 @@ import ghidra.util.task.TaskMonitor;
* See the {@link FSBFileHandler} interface for how to add actions to this component.
*/
public class FSBComponentProvider extends ComponentProviderAdapter
implements FileSystemEventListener, PopupActionProvider {
implements FileSystemEventListener, PopupActionProvider, ProjectIndexListener {
private static final String TITLE = "Filesystem Viewer";
private FSBIcons fsbIcons = FSBIcons.getInstance();
private FileSystemService fsService = FileSystemService.getInstance();
private ProjectIndexService projectIndex = ProjectIndexService.getInstance();
private ProjectIndexService projectIndex = ProjectIndexService.DUMMY;
private FileSystemBrowserPlugin plugin;
private GTree gTree;
@ -169,7 +170,7 @@ public class FSBComponentProvider extends ComponentProviderAdapter
if (df != null) {
overlays.add(FSBIcons.IMPORTED_OVERLAY_ICON);
if (plugin.isOpen(df)) {
if (df.isOpen()) {
// TODO: change this to a OVERLAY_OPEN option when fetching icon
setForeground(selected ? Palette.CYAN : Palette.MAGENTA);
}
@ -222,6 +223,7 @@ public class FSBComponentProvider extends ComponentProviderAdapter
void dispose() {
plugin.getTool().removePopupActionProvider(this);
projectIndex.removeIndexListener(this);
if (rootNode != null && rootNode.getFSRef() != null && !rootNode.getFSRef().isClosed()) {
rootNode.getFSRef().getFilesystem().getRefManager().removeListener(this);
@ -235,6 +237,7 @@ public class FSBComponentProvider extends ComponentProviderAdapter
rootNode = null;
plugin = null;
gTree = null;
projectIndex = null;
}
@Override
@ -265,12 +268,18 @@ public class FSBComponentProvider extends ComponentProviderAdapter
}
public void setProject(Project project) {
gTree.runTask(monitor -> {
projectIndex.setProject(project, monitor);
Swing.runLater(() -> {
projectIndex = ProjectIndexService.getIndexFor(project);
projectIndex.addIndexListener(this);
}
@Override
public void indexUpdated() {
// icons might need repainting after new info is available
Swing.runLater(() -> {
if (gTree != null) {
contextChanged();
gTree.repaint();
}); // icons might need repainting after new info is available
}
});
}

View file

@ -36,7 +36,8 @@ import ghidra.app.services.FileSystemBrowserService;
import ghidra.formats.gfilesystem.*;
import ghidra.framework.main.ApplicationLevelPlugin;
import ghidra.framework.main.FrontEndService;
import ghidra.framework.model.*;
import ghidra.framework.model.Project;
import ghidra.framework.model.ProjectListener;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.plugin.importer.ImporterUtilities;
@ -295,7 +296,7 @@ public class FileSystemBrowserPlugin extends Plugin
if (FileSystemService.isInitialized()) {
fsService().closeUnusedFileSystems();
}
ProjectIndexService.getInstance().clearProject();
ProjectIndexService.projectClosed(project);
}
@Override
@ -305,16 +306,6 @@ public class FileSystemBrowserPlugin extends Plugin
// to tell them about the new project
}
public boolean isOpen(DomainFile df) {
Object tmp = new Object();
DomainObject openDF = df.getOpenedDomainObject(tmp);
if (openDF != null) {
openDF.release(tmp);
return true;
}
return false;
}
public File getLastExportDirectory() {
return lastExportDirectory != null
? lastExportDirectory

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -36,10 +36,12 @@ import ghidra.util.task.TaskMonitor;
public class GFileSystemLoadKernelTask extends Task {
private List<FSRL> fileList;
private ProgramManager programManager;
private Project project;
public GFileSystemLoadKernelTask(Plugin plugin, ProgramManager programManager,
List<FSRL> fileList) {
super("Loading iOS kernel...", true, true, true);
this.project = plugin.getTool().getProject();
this.programManager = programManager;
this.fileList = fileList;
}
@ -114,7 +116,7 @@ public class GFileSystemLoadKernelTask extends Task {
}
monitor.setMessage("Opening " + file.getName());
ProjectIndexService projectIndex = ProjectIndexService.getInstance();
ProjectIndexService projectIndex = ProjectIndexService.getIndexFor(project);
DomainFile existingDF = projectIndex.findFirstByFSRL(file.getFSRL());
if ( existingDF != null && programManager != null ) {
programManager.openProgram(existingDF);