diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ProjectIndexService.java b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ProjectIndexService.java index b95b0181ec..95c5b0ea9d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ProjectIndexService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ProjectIndexService.java @@ -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 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 indexes; + private List indexes = List.of(new IndexInfo(IndexType.MD5, this::getMD5), + new IndexInfo(IndexType.FSRL, this::getFSRL)); + private Thread indexThread; + private ListenerSet 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 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 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) fileInfoList).add(file.getFileID()); + ((List) 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 it = index.indexedFiles.values().iterator(); it - .hasNext();) { + for (Iterator it = index.indexedFiles.values().iterator(); it.hasNext();) { Object fileInfo = it.next(); if (fileInfo instanceof String fileIdStr && fileIdStr.equals(fileId)) { it.remove(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBComponentProvider.java index 041fae54e1..fd80dfdf8c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBComponentProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBComponentProvider.java @@ -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 + } }); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserPlugin.java index 3ec96c6ba2..3ff4a20cbb 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserPlugin.java @@ -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 diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/GFileSystemLoadKernelTask.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/GFileSystemLoadKernelTask.java index c2ab384bc8..373e554732 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/GFileSystemLoadKernelTask.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/GFileSystemLoadKernelTask.java @@ -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 fileList; private ProgramManager programManager; + private Project project; public GFileSystemLoadKernelTask(Plugin plugin, ProgramManager programManager, List 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);