list = new ArrayList<>(programMap.values());
+ Collections.sort(list);
+ return list;
+
+ }
+
private void setCurrentProgram(ProgramInfo info) {
if (currentInfo == info) {
return;
@@ -340,7 +334,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
}
public boolean isEmpty() {
- return openPrograms.isEmpty();
+ return programMap.isEmpty();
}
public boolean contains(Program p) {
@@ -392,44 +386,15 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
return programMap.get(p);
}
- Program getOpenProgram(URL ghidraURL) {
- URL normalizedURL = GhidraURL.getNormalizedURL(ghidraURL);
+ Program getOpenProgram(ProgramLocator programLocator) {
for (ProgramInfo info : programMap.values()) {
- URL url = info.ghidraURL;
- if (url != null && url.equals(normalizedURL)) {
+ if (info.getProgramLocator().equals(programLocator)) {
return info.program;
}
}
return null;
}
- Program getOpenProgram(DomainFile domainFile, int version) {
- for (ProgramInfo info : programMap.values()) {
- DomainFile df = info.domainFile;
- if (df != null && filesMatch(domainFile, version, df)) {
- return info.program;
- }
- }
- return null;
- }
-
- private boolean filesMatch(DomainFile file1, int version, DomainFile file2) {
- if (!file1.getPathname().equals(file2.getPathname())) {
- return false;
- }
-
- if (file1.isCheckedOut() != file2.isCheckedOut()) {
- return false;
- }
-
- if (!SystemUtilities.isEqual(file1.getProjectLocator(), file2.getProjectLocator())) {
- return false;
- }
- // TODO: version check is questionable - unclear how proxy file would work
- int openVersion = file2.isReadOnly() ? file2.getVersion() : -1;
- return version == openVersion;
- }
-
/**
* Returns true if there is at least one program that has unsaved changes.
* @return true if there is at least one program that has unsaved changes.
@@ -445,7 +410,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
return true;
}
// look at all the open programs to see if any have changes
- for (ProgramInfo programInfo : openPrograms) {
+ for (ProgramInfo programInfo : programMap.values()) {
if (programInfo.program.isChanged()) {
return true;
}
@@ -497,7 +462,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
// recovering after closing the program during this swap
dataState = tool.saveDataStateToXml(true);
}
- OpenProgramTask openTask = new OpenProgramTask(file, -1, this);
+ OpenProgramTask openTask = new OpenProgramTask(file, DomainFile.DEFAULT_VERSION, this);
openTask.setSilent();
new TaskLauncher(openTask, tool.getToolFrame());
OpenProgramRequest openProgramReq = openTask.getOpenProgram();
@@ -519,66 +484,31 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
private static final AtomicInteger nextAvailableId = new AtomicInteger();
public final Program program;
-
- // NOTE: domainFile and ghidraURL use are mutually exclusive and reflect how program was
- // opened. Supported cases include:
- // 1. Opened via Program file
- // 2. Opened via ProgramLink file
- // 3. Opened via Program URL
-
- private DomainFile domainFile; // may be link file
- private URL ghidraURL;
+ public ProgramLocator programLocator;
private TransientToolState lastState;
private int instance;
private boolean visible = false;
private Object owner;
- private String str; // cached toString
+ private String displayName; // cached displayName
- ProgramInfo(Program p, DomainFile domainFile, boolean visible) {
+ ProgramInfo(Program p, ProgramLocator programLocator, boolean visible) {
this.program = p;
- this.domainFile = domainFile;
- if (domainFile instanceof LinkedDomainFile linkedDomainFile) {
- this.ghidraURL = linkedDomainFile.getSharedProjectURL(null);
- }
- else {
- this.ghidraURL = null;
- }
+ this.programLocator = programLocator;
this.visible = visible;
instance = nextAvailableId.incrementAndGet();
}
- ProgramInfo(Program p, URL ghidraURL, boolean visible) {
- this.program = p;
- this.domainFile = null;
- this.ghidraURL = ghidraURL;
- this.visible = visible;
- instance = nextAvailableId.incrementAndGet();
- }
-
- /**
- * {@return URL used to open program or null if not applicable}
- */
- URL getGhidraUrl() {
- return ghidraURL;
- }
-
- /**
- * Get the {@link DomainFile} which corresponds to this program. If {@link #getGhidraUrl()}
- * return null this file was used to open program.
- * @return {@link DomainFile} which corresponds to program
- */
- DomainFile getDomainFile() {
- return domainFile;
+ ProgramLocator getProgramLocator() {
+ return programLocator;
}
void programSavedAs() {
- domainFile = program.getDomainFile();
- ghidraURL = null;
- str = null;
+ programLocator = new ProgramLocator(program.getDomainFile());
+ displayName = null;
}
-
+
public void setVisible(boolean state) {
visible = state;
fireVisibilityChangeEvent(program, visible);
@@ -591,21 +521,21 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
@Override
public String toString() {
- if (str != null) {
- return str;
+ if (displayName != null) {
+ return displayName;
}
StringBuilder buf = new StringBuilder();
DomainFile df = program.getDomainFile();
- if (domainFile != null && domainFile.isLinkFile()) {
- buf.append(domainFile.getName());
- buf.append("->");
- }
- buf.append(df.toString());
+ buf.append(program.getDomainFile().toString());
if (df.isReadOnly()) {
buf.append(" [Read-Only]");
}
- str = buf.toString();
- return str;
+ displayName = buf.toString();
+ return displayName;
+ }
+
+ public boolean canReopen() {
+ return programLocator.canReopen();
}
}
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramCache.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramCache.java
new file mode 100644
index 0000000000..5cee62e4f8
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramCache.java
@@ -0,0 +1,110 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.progmgr;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+import ghidra.framework.data.DomainObjectFileListener;
+import ghidra.framework.model.DomainObject;
+import ghidra.program.model.listing.Program;
+import ghidra.util.timer.GTimerCache;
+
+/**
+ * Class for doing time based Program caching.
+ *
+ * Caching programs has some unique challenges because
+ * of the way they are shared using a consumer concept.
+ * Program instances are shared even if unrelated clients open
+ * them. Each client using a program registers its use by giving it a
+ * unique consumer object. When done with the program, the client removes its consumer. When the
+ * last consumer is removed, the program instance is closed.
+ *
+ * When a program is put into the cache, the cache adds itself as a consumer on the program,
+ * effectively keeping it open even if all clients release it. Further, when an entry expires
+ * the cache removes itself as a consumer. A race condition can occur when a client attempts to
+ * retrieve a program from the cache and add itself as a consumer, while the entry's expiration is
+ * being processed. Specifically, there may be a small window where there are no consumers on that
+ * program, causing it to be closed. However, since accessing the program will renew its expiration
+ * time, it is very unlikely to happen, except for debugging scenarios.
+ *
+ * Also, because Program instances can change their association from one DomainFile to another
+ * (Save As), we need to add a listener to the program to detect this. If this occurs on
+ * a program in the cache, we simple remove it from the cache instead of trying to fix it.
+ */
+class ProgramCache extends GTimerCache {
+ private Map listenerMap = new HashMap<>();
+
+ /**
+ * Constructs new ProgramCache with a duration for keeping programs open and a maximum
+ * number of programs to cache.
+ * @param duration the time that a program will remain in the cache without being
+ * accessed (accessing a cached program resets its time)
+ * @param capacity the maximum number of programs in the cache before least recently used
+ * programs are removed.
+ */
+ public ProgramCache(Duration duration, int capacity) {
+ super(duration, capacity);
+ }
+
+ @Override
+ protected void valueAdded(ProgramLocator key, Program program) {
+ program.addConsumer(this);
+ ProgramFileListener listener = new ProgramFileListener(key);
+ program.addDomainFileListener(listener);
+ listenerMap.put(program, listener);
+ }
+
+ @Override
+ protected void valueRemoved(ProgramLocator locator, Program program) {
+ // whenever programs are removed from the cache, we need to remove the cache as a consumer
+ // and remove the file changed listener
+ program.release(this);
+ ProgramFileListener listener = listenerMap.remove(program);
+ program.removeDomainFileListener(listener);
+ }
+
+ @Override
+ protected boolean shouldRemoveFromCache(ProgramLocator locator, Program program) {
+ // Only remove the program from the cache if it is not being used by anyone else. The idea
+ // is that if it is still being used, it is more likely to be needed again by some other
+ // client.
+ //
+ // Note: when a program is purged due to the cache size limit, this method will not be called
+ return program.getConsumerList().size() <= 1;
+ }
+
+ /**
+ * DomainObjectFileListener for programs in the cache. If a program instance has its DomainFile
+ * changed (e.g., 'Save As' action), then the cache mapping is incorrect as it sill has the
+ * program instance associated with its old DomainFile. So we need to add a listener to
+ * recognize when this occurs. If it does, we simply remove the entry from the cache.
+ */
+ class ProgramFileListener implements DomainObjectFileListener {
+ private ProgramLocator key;
+
+ ProgramFileListener(ProgramLocator key) {
+ this.key = key;
+ }
+
+ @Override
+ public void domainFileChanged(DomainObject object) {
+ remove(key);
+ }
+ }
+
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramLocator.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramLocator.java
new file mode 100644
index 0000000000..330f749823
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramLocator.java
@@ -0,0 +1,201 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.progmgr;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Objects;
+
+import ghidra.framework.data.DomainFileProxy;
+import ghidra.framework.data.LinkHandler;
+import ghidra.framework.model.*;
+import ghidra.framework.protocol.ghidra.GhidraURL;
+import ghidra.program.model.listing.Program;
+
+/**
+ * Programs locations can be specified from either a {@link DomainFile} or a ghidra {@link URL}.
+ * This class combines the two ways to specify the location of a program into a single object. The
+ * DomainFile or URL will be normalized, so that this ProgramLocator can be used as a key that
+ * uniquely represents the program, even if the location is specified from different
+ * DomainFiles or URLs that represent the same program instance.
+ *
+ * The class must specify either a DomainFile or a URL, but not both.
+ */
+public class ProgramLocator {
+ private final DomainFile domainFile;
+ private final URL ghidraURL;
+ private final int version;
+ private final boolean invalidContent;
+
+ /**
+ * Creates a {@link URL} based ProgramLocator. The URL must be using the Ghidra protocol
+ * @param url the URL to a Ghidra Program
+ */
+ public ProgramLocator(URL url) {
+ Objects.requireNonNull(url, "URL can't be null");
+ if (!GhidraURL.isGhidraURL(url)) {
+ throw new IllegalArgumentException("unsupported protocol: " + url.getProtocol());
+ }
+ this.ghidraURL = GhidraURL.getNormalizedURL(url);
+ this.domainFile = null;
+ this.version = DomainFile.DEFAULT_VERSION;
+ this.invalidContent = false; // unable to validate
+ }
+
+ /**
+ * Creates a {@link DomainFile} based based ProgramLocator for the current version of a Program.
+ * @param domainFile the DomainFile for a program
+ */
+ public ProgramLocator(DomainFile domainFile) {
+ this(domainFile, DomainFile.DEFAULT_VERSION);
+ }
+
+ /**
+ * Creates a {@link DomainFile} based based ProgramLocator for a specific Program version.
+ * @param domainFile the DomainFile for a program
+ * @param version the specific version of the program
+ */
+ public ProgramLocator(DomainFile domainFile, int version) {
+ this.version = version;
+ this.invalidContent = !Program.class.isAssignableFrom(domainFile.getDomainObjectClass());
+
+ DomainFile file = null;
+ URL url = null;
+
+ DomainFolder parent = domainFile.getParent();
+ if (invalidContent || version != DomainFile.DEFAULT_VERSION || parent == null ||
+ parent.isInWritableProject()) {
+ file = domainFile;
+ }
+ else {
+ try {
+ url = GhidraURL.getNormalizedURL(resolveURL(domainFile));
+ }
+ catch (IOException e) {
+ file = domainFile;
+ }
+ }
+ this.domainFile = file;
+ this.ghidraURL = url;
+ }
+
+ /**
+ * Returns the DomainFile for this locator or null if this is a URL based locator
+ * @return the DomainFile for this locator or null if this is a URL based locator
+ */
+ public DomainFile getDomainFile() {
+ return domainFile;
+ }
+
+ /**
+ * Returns the URL for this locator or null if this is a DomainFile based locator
+ * @return the URL for this locator or null if this is a DomainFile based locator
+ */
+ public URL getURL() {
+ return ghidraURL;
+ }
+
+ /**
+ * Returns the version of the program that this locator represents
+ * @return the version of the program that this locator represents
+ */
+ public int getVersion() {
+ return version;
+ }
+
+ /**
+ * Returns true if this is a DomainFile based program locator
+ * @return true if this is a DomainFile based program locator
+ */
+ public boolean isDomainFile() {
+ return domainFile != null;
+ }
+
+ /**
+ * Returns true if this is a URL based program locator
+ * @return true if this is a URL based program locator
+ */
+ public boolean isURL() {
+ return ghidraURL != null;
+ }
+
+ /**
+ * Returns true if this ProgramLocator represents a valid program location
+ * @return true if this ProgramLocator represents a valid program location
+ */
+ public boolean isValid() {
+ return !invalidContent;
+ }
+
+ /**
+ * Returns true if the information in this location can be used to reopen a program.
+ * @return true if the information in this location can be used to reopen a program
+ */
+ public boolean canReopen() {
+ return !invalidContent && !(domainFile instanceof DomainFileProxy);
+ }
+
+ @Override
+ public String toString() {
+ if (domainFile != null) {
+ return domainFile.toString();
+ }
+ return ghidraURL.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(domainFile, ghidraURL, version);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ ProgramLocator other = (ProgramLocator) obj;
+ return Objects.equals(domainFile, other.domainFile) &&
+ Objects.equals(ghidraURL, other.ghidraURL) && version == other.version;
+ }
+
+ private URL resolveURL(DomainFile file) throws IOException {
+ if (file.isLinkFile()) {
+ return LinkHandler.getURL(file);
+ }
+ DomainFolder parent = file.getParent();
+ if (file instanceof LinkedDomainFile linkedFile) {
+ return resolveLinkedDomainFile(linkedFile);
+ }
+ if (!parent.getProjectLocator().isTransient()) {
+ return file.getLocalProjectURL(null);
+ }
+ return file.getSharedProjectURL(null);
+ }
+
+ private URL resolveLinkedDomainFile(LinkedDomainFile linkedFile) {
+ URL url = linkedFile.getLocalProjectURL(null);
+ if (url == null) {
+ url = linkedFile.getSharedProjectURL(null);
+ }
+ return url;
+ }
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramManagerPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramManagerPlugin.java
index 3154e63d6b..58f3a0857c 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramManagerPlugin.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramManagerPlugin.java
@@ -15,12 +15,14 @@
*/
package ghidra.app.plugin.core.progmgr;
-import java.awt.Component;
import java.beans.PropertyEditor;
import java.net.MalformedURLException;
import java.net.URL;
-import java.util.ArrayList;
-import java.util.List;
+import java.time.Duration;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import org.apache.commons.lang3.StringUtils;
import docking.action.DockingAction;
import docking.action.builder.ActionBuilder;
@@ -34,15 +36,14 @@ import ghidra.app.plugin.core.progmgr.MultiProgramManager.ProgramInfo;
import ghidra.app.services.ProgramManager;
import ghidra.app.util.HelpTopics;
import ghidra.app.util.NamespaceUtils;
+import ghidra.app.util.task.OpenProgramRequest;
import ghidra.app.util.task.OpenProgramTask;
-import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest;
import ghidra.framework.client.ClientUtil;
import ghidra.framework.main.OpenVersionedFileDialog;
import ghidra.framework.model.*;
import ghidra.framework.options.*;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
-import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.Symbol;
@@ -75,14 +76,19 @@ import ghidra.util.task.TaskLauncher;
ProgramActivatedPluginEvent.class }
)
//@formatter:on
-public class ProgramManagerPlugin extends Plugin implements ProgramManager {
+public class ProgramManagerPlugin extends Plugin implements ProgramManager, OptionsChangeListener {
+ private final static String CACHE_DURATION_OPTION =
+ "Program Cache.Program Cache Time (minutes)";
+ private final static String CACHE_SIZE_OPTION = "Program Cache.Program Cache Size";
+ private static final int DEFAULT_PROGRAM_CACHE_CAPACITY = 50;
+ private static final int DEFAULT_PROGRAM_CACHE_DURATION = 30; // in minutes
private static final String SAVE_GROUP = "DomainObjectSave";
static final String OPEN_GROUP = "DomainObjectOpen";
private MultiProgramManager programMgr;
+ private ProgramCache programCache;
private ProgramSaveManager programSaveMgr;
private int transactionID = -1;
- private boolean locked = false;
private UndoAction undoAction;
private RedoAction redoAction;
private ProgramLocation currentLocation;
@@ -92,7 +98,39 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
createActions();
programMgr = new MultiProgramManager(this);
+ programCache = new ProgramCache(Duration.ofMinutes(DEFAULT_PROGRAM_CACHE_DURATION),
+ DEFAULT_PROGRAM_CACHE_CAPACITY);
programSaveMgr = new ProgramSaveManager(tool, this);
+ initializeOptions(tool.getOptions("Tool"));
+ }
+
+ @Override
+ public void optionsChanged(ToolOptions options, String optionName, Object oldValue,
+ Object newValue) {
+ if (optionName.equals(CACHE_DURATION_OPTION)) {
+ Duration duration = Duration.ofMinutes((int) newValue);
+ programCache.setDuration(duration);
+ }
+ if (optionName.equals(CACHE_SIZE_OPTION)) {
+ int capacity = (int) newValue;
+ programCache.setCapacity(capacity);
+ }
+ }
+
+ private void initializeOptions(ToolOptions options) {
+ HelpLocation helpLocation =
+ new HelpLocation(ToolConstants.TOOL_HELP_TOPIC, "Program_Cache_Duration");
+ options.registerOption(CACHE_DURATION_OPTION, DEFAULT_PROGRAM_CACHE_DURATION, helpLocation,
+ "Sets the time (in minutes) cached programs are kept around before closing");
+ helpLocation = new HelpLocation(ToolConstants.TOOL_HELP_TOPIC, "Program_Cache_Size");
+ options.registerOption(CACHE_SIZE_OPTION, DEFAULT_PROGRAM_CACHE_CAPACITY, helpLocation,
+ "Sets the maximum number of programs to be cached");
+
+ int duration = options.getInt(CACHE_DURATION_OPTION, DEFAULT_PROGRAM_CACHE_DURATION);
+ int capacity = options.getInt(CACHE_SIZE_OPTION, DEFAULT_PROGRAM_CACHE_CAPACITY);
+ programCache.setCapacity(capacity);
+ programCache.setDuration(Duration.ofMinutes(duration));
+ options.addOptionsChangeListener(this);
}
/**
@@ -107,12 +145,6 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
return false;
}
- if (locked) {
- Msg.showError(this, tool.getToolFrame(), "Open Program Failed",
- "Program manager is locked and cannot open additional programs");
- return false;
- }
-
List filesToOpen = new ArrayList<>();
for (DomainFile domainFile : data) {
if (domainFile == null) {
@@ -138,110 +170,9 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
return new Class[] { Program.class };
}
- @Override
- public Program openProgram(URL ghidraURL, int state) {
- if (locked) {
- Msg.showError(this, tool.getToolFrame(), "Open Program Failed",
- "Program manager is locked and cannot open additional programs");
- return null;
- }
-
- // Check for URL already open and re-use
- URL url = GhidraURL.getNormalizedURL(ghidraURL);
- Program p = programMgr.getOpenProgram(url);
- if (p != null) {
- showProgram(p, url, state);
- if (state == ProgramManager.OPEN_CURRENT) {
- gotoProgramRef(p, ghidraURL.getRef());
- programMgr.saveLocation();
- }
- return p;
- }
-
- Program program = Swing.runNow(() -> doOpenProgram(ghidraURL, state));
-
- if (program != null) {
- Msg.info(this, "Opened program in " + tool.getName() + " tool: " + ghidraURL);
- }
- return program;
- }
-
- /**
- * Open GhidraURL which corresponds to {@code ghidra://} remote URLs which correspond to a
- * repository program file.
- * @param ghidraURL Ghidra URL which specified Program to be opened which optional ref
- * @param openState open state
- * @return program instance of null if open failed
- */
- private Program doOpenProgram(URL ghidraURL, int openState) {
- Program p = null;
- try {
- URL url = GhidraURL.getNormalizedURL(ghidraURL);
- OpenProgramTask task = new OpenProgramTask(url, this);
- new TaskLauncher(task, tool.getToolFrame());
- OpenProgramRequest openProgramReq = task.getOpenProgram();
- if (openProgramReq != null) {
- p = openProgramReq.getProgram();
- showProgram(p, url, openState);
- openProgramReq.release();
- }
- }
- finally {
- if (p != null && openState == ProgramManager.OPEN_CURRENT) {
- gotoProgramRef(p, ghidraURL.getRef());
- programMgr.saveLocation();
- }
- }
- return p;
- }
-
- private boolean gotoProgramRef(Program program, String ref) {
- if (ref == null) {
- return false;
- }
-
- String trimmedRef = ref.trim();
- if (trimmedRef.length() == 0) {
- return false;
- }
- List symbols = NamespaceUtils.getSymbols(trimmedRef, program);
- Symbol sym = symbols.isEmpty() ? null : symbols.get(0);
-
- ProgramLocation loc = null;
- if (sym != null) {
- SymbolType type = sym.getSymbolType();
- if (type == SymbolType.FUNCTION) {
- loc = new FunctionSignatureFieldLocation(sym.getProgram(), sym.getAddress());
- }
- else if (type == SymbolType.LABEL) {
- loc = new LabelFieldLocation(sym);
- }
- }
- else {
- Address addr = program.getAddressFactory().getAddress(trimmedRef);
- if (addr != null && addr.isMemoryAddress()) {
- loc = new CodeUnitLocation(program, addr, 0, 0, 0);
- }
- }
- if (loc == null) {
- Msg.showError(this, null, "Navigation Failed",
- "Referenced label/function not found: " + trimmedRef);
- return false;
- }
-
- firePluginEvent(new ProgramLocationPluginEvent(getName(), loc, program));
-
- return true;
- }
-
@Override
public Program openProgram(DomainFile df) {
- return openProgram(df, -1, OPEN_CURRENT);
- }
-
- @Override
- public Program openProgram(DomainFile df, Component parent) {
- return openProgram(df, -1, OPEN_CURRENT);
+ return openProgram(df, DomainFile.DEFAULT_VERSION, OPEN_CURRENT);
}
@Override
@@ -251,23 +182,140 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
@Override
public Program openProgram(DomainFile domainFile, int version, int state) {
+ return openProgram(new ProgramLocator(domainFile, version), state);
+ }
- if (domainFile == null) {
- throw new IllegalArgumentException("Domain file cannot be null");
+ @Override
+ public Program openProgram(URL ghidraURL, int state) {
+ String location = ghidraURL.getRef();
+ Program program = openProgram(new ProgramLocator(ghidraURL), state);
+
+ if (program != null && location != null && state == OPEN_CURRENT) {
+ gotoProgramRef(program, ghidraURL.getRef());
+ programMgr.saveLocation();
}
- if (locked) {
- Msg.showError(this, tool.getToolFrame(), "Open Program Failed",
- "Program manager is locked and cannot open additional programs");
+ return program;
+ }
+
+ private Program openProgram(ProgramLocator locator, int state) {
+ Program program = Swing.runNow(() -> {
+ return doOpenProgramSwing(locator, state);
+ });
+ if (program != null) {
+ Msg.info(this, "Opened program in " + tool.getName() + " tool: " + locator);
+ }
+ return program;
+ }
+
+ private Program doOpenProgramSwing(ProgramLocator programLocator, int state) {
+ // see if already open
+ Program program = programMgr.getOpenProgram(programLocator);
+ if (program != null) {
+ showProgram(program, programLocator, state);
+ return program;
+ }
+ // see if cached
+ program = programCache.get(programLocator);
+ if (program != null) {
+ programMgr.addProgram(program, programLocator, state);
+ return program;
+ }
+ // ok, then open it
+ OpenProgramTask task = new OpenProgramTask(programLocator, this);
+ new TaskLauncher(task, tool.getToolFrame());
+ OpenProgramRequest openProgramReq = task.getOpenProgram();
+ if (openProgramReq != null) {
+ program = openProgramReq.getProgram();
+ programMgr.addProgram(program, programLocator, state);
+ openProgramReq.release();
+ return program;
+ }
+ return null;
+ }
+
+ private boolean gotoProgramRef(Program program, String ref) {
+ if (StringUtils.isBlank(ref)) {
+ return false;
+ }
+
+ String trimmedRef = ref.trim();
+ ProgramLocation loc = getLocationForSymbolRef(program, trimmedRef);
+ if (loc == null) {
+ loc = getLocationForAddressRef(program, trimmedRef);
+ }
+
+ if (loc == null) {
+ Msg.showError(this, null, "Navigation Failed",
+ "Referenced label/function not found: " + trimmedRef);
+ return false;
+ }
+
+ firePluginEvent(new ProgramLocationPluginEvent(getName(), loc, program));
+ return true;
+ }
+
+ private ProgramLocation getLocationForAddressRef(Program program, String ref) {
+ Address addr = program.getAddressFactory().getAddress(ref);
+ if (addr != null && addr.isMemoryAddress()) {
+ return new CodeUnitLocation(program, addr, 0, 0, 0);
+ }
+ return null;
+ }
+
+ private ProgramLocation getLocationForSymbolRef(Program program, String ref) {
+ List symbols = NamespaceUtils.getSymbols(ref, program);
+ if (symbols.isEmpty()) {
return null;
}
- Program program = Swing.runNow(() -> {
- return doOpenProgram(domainFile, version, state);
- });
-
- if (program != null) {
- Msg.info(this, "Opened program in " + tool.getName() + " tool: " + domainFile);
+ Symbol symbol = symbols.get(0);
+ if (symbol == null) {
+ return null;
}
+
+ SymbolType type = symbol.getSymbolType();
+ if (type == SymbolType.FUNCTION) {
+ return new FunctionSignatureFieldLocation(program, symbol.getAddress());
+ }
+
+ if (type == SymbolType.LABEL) {
+ return new LabelFieldLocation(symbol);
+ }
+ return null;
+ }
+
+ @Override
+ public Program openCachedProgram(URL ghidraURL, Object consumer) {
+ return openCachedProgram(new ProgramLocator(ghidraURL), consumer);
+ }
+
+ @Override
+ public Program openCachedProgram(DomainFile domainFile, Object consumer) {
+ return openCachedProgram(new ProgramLocator(domainFile), consumer);
+ }
+
+ private Program openCachedProgram(ProgramLocator locator, Object consumer) {
+ Program program = programCache.get(locator);
+ if (program != null) {
+ program.addConsumer(consumer);
+ return program;
+ }
+
+ program = programMgr.getOpenProgram(locator);
+ if (program != null) {
+ program.addConsumer(consumer);
+ programCache.put(locator, program);
+ return program;
+ }
+
+ OpenProgramTask task = new OpenProgramTask(locator, consumer);
+ new TaskLauncher(task, tool.getToolFrame());
+ OpenProgramRequest result = task.getOpenProgram();
+ if (result == null) {
+ return null;
+ }
+ program = result.getProgram();
+ programCache.put(locator, program);
return program;
}
@@ -288,7 +336,7 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
@Override
public Program[] getAllOpenPrograms() {
- return programMgr.getAllPrograms();
+ return programMgr.getAllPrograms().toArray(Program[]::new);
}
@Override
@@ -299,7 +347,7 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
@Override
public boolean closeOtherPrograms(boolean ignoreChanges) {
- Program[] otherPrograms = programMgr.getOtherPrograms();
+ List otherPrograms = programMgr.getOtherPrograms();
Runnable r = () -> doCloseAllPrograms(otherPrograms, ignoreChanges);
Swing.runNow(r);
return programMgr.isEmpty();
@@ -307,13 +355,13 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
@Override
public boolean closeAllPrograms(boolean ignoreChanges) {
- Program[] openPrograms = programMgr.getAllPrograms();
+ List openPrograms = programMgr.getAllPrograms();
Runnable r = () -> doCloseAllPrograms(openPrograms, ignoreChanges);
Swing.runNow(r);
return programMgr.isEmpty();
}
- private void doCloseAllPrograms(Program[] openPrograms, boolean ignoreChanges) {
+ private void doCloseAllPrograms(List openPrograms, boolean ignoreChanges) {
List toRemove = new ArrayList<>();
Program currentProgram = programMgr.getCurrentProgram();
for (Program p : openPrograms) {
@@ -371,8 +419,8 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
@Override
protected void close() {
- Program[] programs = programMgr.getAllPrograms();
- if (programs.length == 0) {
+ List programs = programMgr.getAllPrograms();
+ if (programs.isEmpty()) {
return;
}
// Don't remove currentProgram until last to prevent activation of other programs.
@@ -414,49 +462,21 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
*/
@Override
public void openProgram(Program program) {
- openProgram(program, true);
- }
-
- @Override
- public void openProgram(Program program, boolean current) {
- openProgram(program, current ? OPEN_CURRENT : OPEN_VISIBLE);
+ openProgram(program, OPEN_CURRENT);
}
@Override
public void openProgram(final Program program, final int state) {
- showProgram(program, program.getDomainFile(), state);
+ showProgram(program, new ProgramLocator(program.getDomainFile()), state);
}
- private void showProgram(Program p, URL ghidraUrl, final int state) {
+ private void showProgram(Program p, ProgramLocator locator, final int state) {
if (p == null || p.isClosed()) {
throw new AssertException("Opened program required");
}
- if (locked) {
- throw new IllegalStateException(
- "Progam manager is locked and cannot accept a new program");
- }
Runnable r = () -> {
- programMgr.addProgram(p, ghidraUrl, state);
- if (state == ProgramManager.OPEN_CURRENT) {
- programMgr.saveLocation();
- }
- contextChanged();
- };
- Swing.runNow(r);
- }
-
- private void showProgram(Program p, DomainFile domainFile, final int state) {
- if (p == null || p.isClosed()) {
- throw new AssertException("Opened program required");
- }
- if (locked) {
- throw new IllegalStateException(
- "Progam manager is locked and cannot accept a new program");
- }
-
- Runnable r = () -> {
- programMgr.addProgram(p, domainFile, state);
+ programMgr.addProgram(p, locator, state);
if (state == ProgramManager.OPEN_CURRENT) {
programMgr.saveLocation();
}
@@ -496,7 +516,6 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
new ActionBuilder("Open File", getName()).menuPath(ToolConstants.MENU_FILE, "&Open...")
.menuGroup(OPEN_GROUP, Integer.toString(subMenuGroup++))
.keyBinding("ctrl O")
- .enabledWhen(c -> !locked)
.onAction(c -> open())
.buildAndInstall(tool);
openAction.addToWindowWhen(ProgramActionContext.class);
@@ -617,7 +636,7 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
}
else {
openDialog.close();
- doOpenProgram(domainFile, version, OPEN_CURRENT);
+ doOpenProgramSwing(new ProgramLocator(domainFile, version), OPEN_CURRENT);
}
});
@@ -626,54 +645,73 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
}
public void openPrograms(List filesToOpen) {
- Program showIfNeeded = null;
- OpenProgramTask openTask = null;
- for (DomainFile domainFile : filesToOpen) {
- Program p = programMgr.getOpenProgram(domainFile, -1);
- if (p != null) {
- showIfNeeded = p;
- continue;
- }
- if (openTask == null) {
- openTask = new OpenProgramTask(domainFile, -1, this);
- }
- else {
- openTask.addProgramToOpen(domainFile, -1);
- }
+ List locators =
+ filesToOpen.stream().map(f -> new ProgramLocator(f)).collect(Collectors.toList());
+
+ openProgramLocations(locators);
+ }
+
+ private void openProgramLocations(List locators) {
+
+ Set toOpen = new LinkedHashSet<>(locators); // preserve order
+
+ // ensure already opened programs are visible in the tool
+ Map alreadyOpen = getOpenPrograms(toOpen);
+ makeVisibleInTool(alreadyOpen.values());
+ toOpen.removeAll(alreadyOpen.keySet());
+
+ // ensure cached programs are in the tool
+ Map openedFromCache = openCachedProgramsInTool(toOpen);
+ toOpen.removeAll(openedFromCache.keySet());
+
+ // if nothing to open, make the first program in the list the current program
+ if (toOpen.isEmpty()) {
+ Program first = programMgr.getOpenProgram(locators.get(0));
+ showProgram(first, locators.get(0), OPEN_CURRENT);
+ return;
}
- if (openTask != null) {
- new TaskLauncher(openTask, tool.getToolFrame());
- List openProgramReqs = openTask.getOpenPrograms();
- boolean isFirst = true;
- for (OpenProgramRequest programReq : openProgramReqs) {
- showProgram(programReq.getProgram(), programReq.getDomainFile(),
- isFirst ? OPEN_CURRENT : OPEN_VISIBLE);
- programReq.release();
- isFirst = false;
- showIfNeeded = null;
- }
- }
- if (showIfNeeded != null) {
- showProgram(showIfNeeded, showIfNeeded.getDomainFile(), OPEN_CURRENT);
+
+ // Need to open at least one program. Make the first one to open the current program.
+ OpenProgramTask task = new OpenProgramTask(new ArrayList<>(toOpen), this);
+ new TaskLauncher(task, tool.getToolFrame());
+
+ List openProgramReqs = task.getOpenPrograms();
+
+ int openState = OPEN_CURRENT;
+ for (OpenProgramRequest programReq : openProgramReqs) {
+ showProgram(programReq.getProgram(), programReq.getLocator(), openState);
+ programReq.release();
+ openState = OPEN_VISIBLE;
}
}
- protected Program doOpenProgram(DomainFile domainFile, int version, int openState) {
- Program p = programMgr.getOpenProgram(domainFile, version);
- if (p != null) {
- openProgram(p, openState);
+ private void makeVisibleInTool(Collection programs) {
+ for (Program program : programs) {
+ openProgram(program, OPEN_VISIBLE);
}
- else {
- OpenProgramTask task = new OpenProgramTask(domainFile, version, this);
- new TaskLauncher(task, tool.getToolFrame());
- OpenProgramRequest programReq = task.getOpenProgram();
- if (programReq != null) {
- p = programReq.getProgram();
- showProgram(p, programReq.getDomainFile(), openState);
- programReq.release();
+ }
+
+ private Map openCachedProgramsInTool(Set toOpen) {
+ Map map = new HashMap<>();
+ for (ProgramLocator programLocator : toOpen) {
+ Program program = programCache.get(programLocator);
+ if (program != null) {
+ openProgram(program, OPEN_VISIBLE);
+ map.put(programLocator, program);
}
}
- return p;
+ return map;
+ }
+
+ private Map getOpenPrograms(Collection locators) {
+ Map map = new HashMap<>();
+ for (ProgramLocator locator : locators) {
+ Program program = programMgr.getOpenProgram(locator);
+ if (program != null) {
+ map.put(locator, program);
+ }
+ }
+ return map;
}
@Override
@@ -705,7 +743,7 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
ArrayList programInfos = new ArrayList<>();
for (Program p : programMgr.getAllPrograms()) {
ProgramInfo info = programMgr.getInfo(p);
- if (info != null) {
+ if (info != null && info.canReopen()) {
programInfos.add(info);
}
}
@@ -740,8 +778,8 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
loadPrograms(saveState);
String currentFile = saveState.getString("CURRENT_FILE", null);
- Program[] programs = programMgr.getAllPrograms();
- if (programs.length != 0) {
+ List programs = programMgr.getAllPrograms();
+ if (!programs.isEmpty()) {
if (currentFile != null) {
for (Program program : programs) {
if (program.getDomainFile().getName().equals(currentFile)) {
@@ -752,7 +790,7 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
}
}
if (getCurrentProgram() == null) {
- programMgr.setCurrentProgram(programs[0]);
+ programMgr.setCurrentProgram(programs.get(0));
}
}
contextChanged();
@@ -767,12 +805,8 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
}
private void writeProgramInfo(ProgramInfo programInfo, SaveState saveState, int index) {
- if (locked) {
- return; // do not save state when locked.
- }
-
- URL url = programInfo.getGhidraUrl();
- if (url != null) {
+ if (programInfo.getProgramLocator().isURL()) {
+ URL url = programInfo.getProgramLocator().getURL();
saveState.putString("URL_" + index, url.toString());
return;
}
@@ -805,54 +839,43 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
*/
private void loadPrograms(SaveState saveState) {
- int n = saveState.getInt("NUM_PROGRAMS", 0);
- if (n == 0) {
+ int programCount = saveState.getInt("NUM_PROGRAMS", 0);
+ if (programCount == 0) {
return;
}
- OpenProgramTask openTask = new OpenProgramTask(this);
+ List openList = new ArrayList<>();
- for (int index = 0; index < n; index++) {
+ for (int index = 0; index < programCount; index++) {
URL url = getGhidraURL(saveState, index);
if (url != null) {
- openTask.addProgramToOpen(url);
+ openList.add(new ProgramLocator(url));
continue;
}
DomainFile domainFile = getDomainFile(saveState, index);
- if (domainFile == null) {
- continue;
+ if (domainFile != null) {
+ int version = getVersion(saveState, index);
+ openList.add(new ProgramLocator(domainFile, version));
}
- int version = getVersion(saveState, index);
- openTask.addProgramToOpen(domainFile, version);
}
-
- if (!openTask.hasOpenProgramRequests()) {
+ if (openList.isEmpty()) {
return;
}
+ OpenProgramTask task = new OpenProgramTask(openList, this);
+
// Restore state should not ask about checking out since
// hopefully it is in the same state it was in when project
// was closed and state was saved.
- openTask.setNoCheckout();
+ task.setNoCheckout();
- try {
- new TaskLauncher(openTask, tool.getToolFrame(), 100);
- }
- catch (RuntimeException e) {
- Msg.showError(this, tool.getToolFrame(), "Error Getting Domain File",
- "Can't open program", e);
- }
+ new TaskLauncher(task, tool.getToolFrame(), 100);
- List openProgramReqs = openTask.getOpenPrograms();
+ List openProgramReqs = task.getOpenPrograms();
for (OpenProgramRequest programReq : openProgramReqs) {
- DomainFile df = programReq.getDomainFile();
- if (df != null) {
- showProgram(programReq.getProgram(), df, OPEN_VISIBLE);
- }
- else {
- showProgram(programReq.getProgram(), programReq.getGhidraURL(), OPEN_VISIBLE);
- }
+ ProgramLocator locator = programReq.getLocator();
+ showProgram(programReq.getProgram(), locator, OPEN_VISIBLE);
programReq.release();
}
}
@@ -1056,17 +1079,6 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
return programMgr.setPersistentOwner(program, owner);
}
- @Override
- public boolean isLocked() {
- return locked;
- }
-
- @Override
- public void lockDown(boolean state) {
- locked = state;
- contextChanged();
- }
-
public boolean isManaged(Program program) {
return programMgr.contains(program);
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/ProgramManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/ProgramManager.java
index a7e700a22c..fc089ff6c7 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/services/ProgramManager.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/ProgramManager.java
@@ -15,7 +15,6 @@
*/
package ghidra.app.services;
-import java.awt.Component;
import java.net.URL;
import ghidra.app.plugin.core.progmgr.ProgramManagerPlugin;
@@ -29,7 +28,9 @@ import ghidra.program.model.listing.Program;
* Service for managing programs. Multiple programs may be open in a tool, but only one is active at
* any given time.
*/
-@ServiceInfo(defaultProvider = ProgramManagerPlugin.class, description = "Get the currently open program")
+@ServiceInfo(
+ defaultProvider = ProgramManagerPlugin.class,
+ description = "Get the currently open program")
public interface ProgramManager {
/**
@@ -78,8 +79,7 @@ public interface ProgramManager {
* @param ghidraURL valid server-based program URL
* @param state initial open state (OPEN_HIDDEN, OPEN_CURRENT, OPEN_VISIBLE). The visibility
* states will be ignored if the program is already open.
- * @return null if the user canceled the "open" for the new program or an error occurred and was
- * displayed.
+ * @return the opened program or null if the user canceled the "open" or an error occurred
* @see GhidraURL
*/
public Program openProgram(URL ghidraURL, int state);
@@ -88,24 +88,43 @@ public interface ProgramManager {
* Open the program for the given domainFile. Once open it will become the active program.
*
* @param domainFile domain file that has the program
- * @return null if the user canceled the "open" for the new program
+ * @return the opened program or null if the user canceled the "open" or an error occurred
*/
public Program openProgram(DomainFile domainFile);
/**
- * Open the program for the given domainFile. Once open it will become the active program.
- *
- *
- * Note: this method functions exactly as {@link #openProgram(DomainFile)}
- *
- * @param domainFile domain file that has the program
- * @param dialogParent unused
- * @return the program
- * @deprecated deprecated for 10.1; removal for 10.3 or later; use
- * {@link #openProgram(DomainFile)}
+ * Opens a program or retrieves it from a cache. If the program is in the cache, the consumer
+ * will be added the program before returning it. Otherwise, the program will be opened with
+ * the consumer. In addition, opening or accessing a cached program, will guarantee that it will
+ * remain open for period of time, even if the caller of this method releases it from the
+ * consumer that was passed in. If the program isn't accessed again, it will be eventually be
+ * released from the cache. If the program is still in use when the timer expires, the
+ * program will remain in the cache with a new full expiration time. Calling this method
+ * does not open the program in the tool.
+ *
+ * @param domainFile the DomainFile from which to open a program.
+ * @param consumer the consumer that is using the program. The caller is responsible for
+ * releasing (See {@link Program#release(Object)}) the consumer when done with the program.
+ * @return the program for the given domainFile or null if unable to open the program
*/
- @Deprecated
- public Program openProgram(DomainFile domainFile, Component dialogParent);
+ public Program openCachedProgram(DomainFile domainFile, Object consumer);
+
+ /**
+ * Opens a program or retrieves it from a cache. If the program is in the cache, the consumer
+ * will be added the program before returning it. Otherwise, the program will be opened with
+ * the consumer. In addition, opening or accessing a cached program, will guarantee that it will
+ * remain open for period of time, even if the caller of this method releases it from the
+ * consumer that was passed in. If the program isn't accessed again, it will be eventually be
+ * released from the cache. If the program is still in use when the timer expires, the
+ * program will remain in the cache with a new full expiration time. Calling this method
+ * does not open the program in the tool.
+ *
+ * @param ghidraURL the ghidra URL from which to open a program.
+ * @param consumer the consumer that is using the program. The caller is responsible for
+ * releasing (See {@link Program#release(Object)}) the consumer when done with the program.
+ * @return the program for the given URL or null if unable to open the program
+ */
+ public Program openCachedProgram(URL ghidraURL, Object consumer);
/**
* Opens the specified version of the program represented by the given DomainFile. This method
@@ -113,7 +132,7 @@ public interface ProgramManager {
*
* @param df the DomainFile to open
* @param version the version of the Program to open
- * @return the opened program or null if the given version does not exist.
+ * @return the opened program or null if the user canceled the "open" or an error occurred
*/
public Program openProgram(DomainFile df, int version);
@@ -125,8 +144,7 @@ public interface ProgramManager {
* file update mode.
* @param state initial open state (OPEN_HIDDEN, OPEN_CURRENT, OPEN_VISIBLE). The visibility
* states will be ignored if the program is already open.
- * @return null if the user canceled the "open" for the new program or an error occurred and was
- * displayed.
+ * @return the opened program or null if the user canceled the "open" or an error occurred
*/
public Program openProgram(DomainFile domainFile, int version, int state);
@@ -138,18 +156,6 @@ public interface ProgramManager {
*/
public void openProgram(Program program);
- /**
- * Opens the program to the tool. In this case the program is already open, but this tool may
- * not have it registered as open. The program is made the active program.
- *
- * @param program the program to register as open with the tool.
- * @param current if true, the program is made the current active program. If false, then the
- * program is made active only if it the first open program in the tool.
- * @deprecated use openProgram(Program program, int state) instead.
- */
- @Deprecated
- public void openProgram(Program program, boolean current);
-
/**
* Open the specified program in the tool.
*
@@ -275,22 +281,4 @@ public interface ProgramManager {
*/
public Program[] getAllOpenPrograms();
- /**
- * Allows program manager state to be locked/unlocked. While locked, the program manager will
- * not support opening additional programs.
- *
- * @param state locked if true, unlocked if false
- * @deprecated deprecated for 10.1; removal for 10.3 or later
- */
- @Deprecated
- public void lockDown(boolean state);
-
- /**
- * Returns true if program manager is in the locked state
- *
- * @return true if program manager is in the locked state
- * @deprecated deprecated for 10.1; removal for 10.3 or later
- */
- @Deprecated
- public boolean isLocked();
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/OpenProgramRequest.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/OpenProgramRequest.java
new file mode 100644
index 0000000000..0a46d49198
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/OpenProgramRequest.java
@@ -0,0 +1,55 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.util.task;
+
+import ghidra.app.plugin.core.progmgr.ProgramLocator;
+import ghidra.program.model.listing.Program;
+
+public class OpenProgramRequest {
+ private final ProgramLocator locator;
+ private final Program program;
+ private final Object consumer;
+
+ public OpenProgramRequest(Program program, ProgramLocator locator, Object consumer) {
+ this.program = program;
+ this.locator = locator;
+ this.consumer = consumer;
+ }
+
+ /**
+ * Get the open Program instance which corresponds to this open request.
+ * @return program instance or null if never opened.
+ */
+ public Program getProgram() {
+ return program;
+ }
+
+ /**
+ * Release opened program. This must be done once, and only once, on a successful
+ * open request. If handing ownership off to another consumer, they should be added
+ * as a program consumer prior to invoking this method. Releasing the last consumer
+ * will close the program instance.
+ */
+ public void release() {
+ if (program != null) {
+ program.release(consumer);
+ }
+ }
+
+ public ProgramLocator getLocator() {
+ return locator;
+ }
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/OpenProgramTask.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/OpenProgramTask.java
index 401d0eb881..26721dfb0e 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/OpenProgramTask.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/OpenProgramTask.java
@@ -15,91 +15,83 @@
*/
package ghidra.app.util.task;
-import java.io.IOException;
-import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
-import docking.widgets.OptionDialog;
-import ghidra.app.util.dialog.CheckoutDialog;
-import ghidra.framework.client.ClientUtil;
-import ghidra.framework.client.RepositoryAdapter;
-import ghidra.framework.main.AppInfo;
+import ghidra.app.plugin.core.progmgr.ProgramLocator;
import ghidra.framework.model.DomainFile;
-import ghidra.framework.protocol.ghidra.*;
-import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
-import ghidra.framework.remote.User;
-import ghidra.framework.store.ExclusiveCheckoutException;
-import ghidra.program.database.ProgramLinkContentHandler;
-import ghidra.program.model.lang.LanguageNotFoundException;
import ghidra.program.model.listing.Program;
-import ghidra.util.*;
-import ghidra.util.exception.CancelledException;
-import ghidra.util.exception.VersionException;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
+/**
+ * Task for opening one or more programs.
+ */
public class OpenProgramTask extends Task {
-
- private final List openProgramRequests = new ArrayList<>();
- private List openedProgramList = new ArrayList<>();
+ private List programsToOpen = new ArrayList<>();
+ private List openedPrograms = new ArrayList<>();
+ private ProgramOpener programOpener;
private final Object consumer;
- private boolean silent; // if true operation does not permit interaction
- private boolean noCheckout; // if true operation should not perform optional checkout
- private String openPromptText = "Open";
-
- public OpenProgramTask(Object consumer) {
+ /**
+ * Construct a task for opening one or more programs.
+ * @param programLocatorList the list of program locations to open
+ * @param consumer the consumer to use for opening the programs
+ */
+ public OpenProgramTask(List programLocatorList, Object consumer) {
super("Open Program(s)", true, false, true);
this.consumer = consumer;
+ programOpener = new ProgramOpener(consumer);
+ programsToOpen.addAll(programLocatorList);
}
- public OpenProgramTask(DomainFile domainFile, int version, boolean forceReadOnly,
- Object consumer) {
- super("Open Program(s)", true, false, true);
- this.consumer = consumer;
- openProgramRequests.add(new OpenProgramRequest(domainFile, version, forceReadOnly));
+ /**
+ * Construct a task for opening a program.
+ * @param locator the program location to open
+ * @param consumer the consumer to use for opening the programs
+ */
+ public OpenProgramTask(ProgramLocator locator, Object consumer) {
+ this(Arrays.asList(locator), consumer);
}
+ /**
+ * Construct a task for opening a program
+ * @param domainFile the {@link DomainFile} to open
+ * @param version the version to open (versions other than the current version will be
+ * opened read-only)
+ * @param consumer the consumer to use for opening the programs
+ */
public OpenProgramTask(DomainFile domainFile, int version, Object consumer) {
- this(domainFile, version, false, consumer);
- }
-
- public OpenProgramTask(DomainFile domainFile, boolean forceReadOnly, Object consumer) {
- this(domainFile, DomainFile.DEFAULT_VERSION, forceReadOnly, consumer);
+ this(new ProgramLocator(domainFile, version), consumer);
}
+ /**
+ * Construct a task for opening the current version of a program
+ * @param domainFile the {@link DomainFile} to open
+ * @param consumer the consumer to use for opening the programs
+ */
public OpenProgramTask(DomainFile domainFile, Object consumer) {
- this(domainFile, DomainFile.DEFAULT_VERSION, false, consumer);
+ this(new ProgramLocator(domainFile), consumer);
}
+ /**
+ * Construct a task for opening a program from a URL
+ * @param ghidraURL the URL to the program to be opened
+ * @param consumer the consumer to use for opening the programs
+ */
public OpenProgramTask(URL ghidraURL, Object consumer) {
- super("Open Program(s)", true, false, true);
- this.consumer = consumer;
- openProgramRequests.add(new OpenProgramRequest(ghidraURL));
+ this(new ProgramLocator(ghidraURL), consumer);
}
+ /**
+ * Sets the text to use for the base action type for various prompts that can appear
+ * when opening programs. (The default is "Open".) For example, you may want to override
+ * this so be something like "Open Source", or "Open target".
+ * @param text the text to use as the base action name.
+ */
public void setOpenPromptText(String text) {
- openPromptText = text;
- }
-
- public void addProgramToOpen(DomainFile domainFile, int version) {
- addProgramToOpen(domainFile, version, false);
- }
-
- public void addProgramToOpen(DomainFile domainFile, int version, boolean forceReadOnly) {
- setHasProgress(true);
- openProgramRequests.add(new OpenProgramRequest(domainFile, version, forceReadOnly));
- }
-
- public void addProgramToOpen(URL ghidraURL) {
- setHasProgress(true);
- openProgramRequests.add(new OpenProgramRequest(ghidraURL));
- }
-
- public boolean hasOpenProgramRequests() {
- return !openProgramRequests.isEmpty();
+ programOpener.setPromptText(text);
}
/**
@@ -109,7 +101,7 @@ public class OpenProgramTask extends Task {
* may still be displayed if they occur.
*/
public void setSilent() {
- this.silent = true;
+ programOpener.setSilent();
}
/**
@@ -118,7 +110,7 @@ public class OpenProgramTask extends Task {
* user.
*/
public void setNoCheckout() {
- this.noCheckout = true;
+ programOpener.setNoCheckout();
}
/**
@@ -126,7 +118,7 @@ public class OpenProgramTask extends Task {
* @return all successful open program requests
*/
public List getOpenPrograms() {
- return Collections.unmodifiableList(openedProgramList);
+ return Collections.unmodifiableList(openedPrograms);
}
/**
@@ -134,310 +126,27 @@ public class OpenProgramTask extends Task {
* @return first successful open program request or null if none
*/
public OpenProgramRequest getOpenProgram() {
- if (openedProgramList.isEmpty()) {
+ if (openedPrograms.isEmpty()) {
return null;
}
- return openedProgramList.get(0);
+ return openedPrograms.get(0);
}
@Override
public void run(TaskMonitor monitor) {
- taskMonitor.initialize(openProgramRequests.size());
+ taskMonitor.initialize(programsToOpen.size());
- for (OpenProgramRequest domainFileInfo : openProgramRequests) {
+ for (ProgramLocator locator : programsToOpen) {
if (taskMonitor.isCancelled()) {
return;
}
- domainFileInfo.open();
+ Program program = programOpener.openProgram(locator, monitor);
+ if (program != null) {
+ openedPrograms.add(new OpenProgramRequest(program, locator, consumer));
+ }
taskMonitor.incrementProgress(1);
}
}
- private Object openReadOnlyFile(DomainFile domainFile, URL url, int version) {
- taskMonitor.setMessage("Opening " + domainFile.getName());
- return openReadOnly(domainFile, url, version);
- }
-
- private Object openVersionedFile(DomainFile domainFile, URL url, int version) {
- taskMonitor.setMessage("Getting Version " + version + " for " + domainFile.getName());
- return openReadOnly(domainFile, url, version);
- }
-
- private Object openReadOnly(DomainFile domainFile, URL url, int version) {
- String contentType = domainFile.getContentType();
- String path = url != null ? url.toString() : domainFile.getPathname();
- Object obj = null;
- try {
- obj = domainFile.getReadOnlyDomainObject(consumer, version, taskMonitor);
- }
- catch (CancelledException e) {
- // we don't care, the task has been cancelled
- }
- catch (IOException e) {
- if (url == null && domainFile.isInWritableProject()) {
- ClientUtil.handleException(AppInfo.getActiveProject().getRepository(), e,
- "Get " + contentType, null);
- }
- else if (version != DomainFile.DEFAULT_VERSION) {
- Msg.showError(this, null, "Error Getting Versioned Program",
- "Could not get version " + version + " for " + path, e);
- }
- else {
- Msg.showError(this, null, "Error Getting Program",
- "Open program failed for " + path, e);
- }
- }
- catch (VersionException e) {
- VersionExceptionHandler.showVersionError(null, domainFile.getName(), contentType,
- "Open", e);
- }
- return obj;
- }
-
- private Program openUnversionedFile(DomainFile domainFile) {
- String filename = domainFile.getName();
- taskMonitor.setMessage("Opening " + filename);
- performOptionalCheckout(domainFile);
- try {
- return openFileMaybeUgrade(domainFile);
- }
- catch (VersionException e) {
- String contentType = domainFile.getContentType();
- VersionExceptionHandler.showVersionError(null, filename, contentType, "Open", e);
- }
- catch (CancelledException e) {
- // we don't care, the task has been cancelled
- }
- catch (LanguageNotFoundException e) {
- Msg.showError(this, null, "Error Opening " + filename,
- e.getMessage() + "\nPlease contact the Ghidra team for assistance.");
- }
- catch (Exception e) {
- if (domainFile.isInWritableProject() && (e instanceof IOException)) {
- RepositoryAdapter repo = domainFile.getParent().getProjectData().getRepository();
- ClientUtil.handleException(repo, e, "Open File", null);
- }
- else {
- Msg.showError(this, null, "Error Opening " + filename,
- "Getting domain object failed.\n" + e.getMessage(), e);
- }
- }
- return null;
- }
-
- private Program openFileMaybeUgrade(DomainFile domainFile)
- throws IOException, CancelledException, VersionException {
-
- boolean recoverFile = false;
- if (!silent && domainFile.isInWritableProject() && domainFile.canRecover()) {
- recoverFile = askRecoverFile(domainFile.getName());
- }
-
- Program program = null;
- try {
- program =
- (Program) domainFile.getDomainObject(consumer, false, recoverFile, taskMonitor);
- }
- catch (VersionException e) {
- if (VersionExceptionHandler.isUpgradeOK(null, domainFile, openPromptText, e)) {
- program =
- (Program) domainFile.getDomainObject(consumer, true, recoverFile, taskMonitor);
- }
- }
- return program;
- }
-
- private boolean askRecoverFile(final String filename) {
-
- int option = OptionDialog.showYesNoDialog(null, "Crash Recovery Data Found",
- "" + HTMLUtilities.escapeHTML(filename) + " has crash data. " +
- "Would you like to recover unsaved changes?");
- return option == OptionDialog.OPTION_ONE;
- }
-
- private void performOptionalCheckout(DomainFile domainFile) {
-
- if (silent || noCheckout || !domainFile.canCheckout()) {
- return;
- }
-
- User user = domainFile.getParent().getProjectData().getUser();
-
- CheckoutDialog dialog = new CheckoutDialog(domainFile, user);
- if (dialog.showDialog() == CheckoutDialog.CHECKOUT) {
- try {
- taskMonitor.setMessage("Checking Out " + domainFile.getName());
- if (domainFile.checkout(dialog.exclusiveCheckout(), taskMonitor)) {
- return;
- }
- Msg.showError(this, null, "Checkout Failed", "Exclusive checkout failed for: " +
- domainFile.getName() + "\nOne or more users have file checked out!");
- }
- catch (CancelledException e) {
- // we don't care, the task has been cancelled
- }
- catch (ExclusiveCheckoutException e) {
- Msg.showError(this, null, "Checkout Failed", e.getMessage());
- }
- catch (IOException e) {
- Msg.showError(this, null, "Error on Check Out", e.getMessage(), e);
- }
- }
- }
-
- public class OpenProgramRequest {
-
- // ghidraURL and domainFile use are mutually exclusive
- private final URL ghidraURL;
- private final DomainFile domainFile;
-
- private URL linkURL; // link URL read from domainFile
-
- private final int version;
- private final boolean forceReadOnly;
- private Program program;
-
- public OpenProgramRequest(URL ghidraURL) {
- if (!GhidraURL.PROTOCOL.equals(ghidraURL.getProtocol())) {
- throw new IllegalArgumentException(
- "unsupported protocol: " + ghidraURL.getProtocol());
- }
- this.ghidraURL = ghidraURL;
- this.domainFile = null;
- this.version = -1;
- this.forceReadOnly = true;
- }
-
- public OpenProgramRequest(DomainFile domainFile, int version, boolean forceReadOnly) {
- this.domainFile = domainFile;
- this.ghidraURL = null;
- this.version =
- (domainFile.isReadOnly() && domainFile.isVersioned()) ? domainFile.getVersion()
- : version;
- this.forceReadOnly = forceReadOnly;
- }
-
- /**
- * Get the {@link DomainFile} which corresponds to program open request. This will be
- * null for all URL-based open requests.
- * @return {@link DomainFile} which corresponds to program open request or null.
- */
- public DomainFile getDomainFile() {
- return domainFile;
- }
-
- /**
- * Get the {@link URL} which corresponds to program open request. This will be
- * null for all non-URL-based open requests. URL will be a {@link GhidraURL}.
- * @return {@link URL} which corresponds to program open request or null.
- */
- public URL getGhidraURL() {
- return ghidraURL;
- }
-
- /**
- * Get the {@link URL} which corresponds to the link domainFile used to open a program.
- * @return {@link URL} which corresponds to the link domainFile used to open a program.
- */
- public URL getLinkURL() {
- return linkURL;
- }
-
- /**
- * Get the open Program instance which corresponds to this open request.
- * @return program instance or null if never opened.
- */
- public Program getProgram() {
- return program;
- }
-
- /**
- * Release opened program. This must be done once, and only once, on a successful
- * open request. If handing ownership off to another consumer, they should be added
- * as a program consumer prior to invoking this method. Releasing the last consumer
- * will close the program instance.
- */
- public void release() {
- if (program != null) {
- program.release(consumer);
- }
- }
-
- private Program openProgram(DomainFile df, URL url) {
- if (version != DomainFile.DEFAULT_VERSION) {
- return (Program) openVersionedFile(df, url, version);
- }
- if (forceReadOnly) {
- return (Program) openReadOnlyFile(df, url, version);
- }
- return openUnversionedFile(df);
- }
-
- void open() {
- DomainFile df = domainFile;
- URL url = ghidraURL;
- GhidraURLWrappedContent wrappedContent = null;
- Object content = null;
- try {
- if (df == null && url != null) {
- GhidraURLConnection c = (GhidraURLConnection) url.openConnection();
- Object obj = c.getContent(); // read-only access
- if (c.getStatusCode() == StatusCode.UNAUTHORIZED) {
- return; // assume user already notified
- }
- if (!(obj instanceof GhidraURLWrappedContent)) {
- messageBadProgramURL(url);
- return;
- }
- wrappedContent = (GhidraURLWrappedContent) obj;
- content = wrappedContent.getContent(this);
- if (!(content instanceof DomainFile)) {
- messageBadProgramURL(url);
- return;
- }
- df = (DomainFile) content;
-
- if (ProgramLinkContentHandler.PROGRAM_LINK_CONTENT_TYPE
- .equals(df.getContentType())) {
- Msg.showError(this, null, "Program Multi-Link Error",
- "Multi-link Program access not supported: " + url);
- return;
- }
- }
-
- if (!Program.class.isAssignableFrom(df.getDomainObjectClass())) {
- Msg.showError(this, null, "Error Opening Program",
- "File does not correspond to a Ghidra Program: " + df.getPathname());
- return;
- }
-
- program = openProgram(df, url);
-
- }
- catch (MalformedURLException e) {
- Msg.showError(this, null, "Invalid Ghidra URL",
- "Improperly formed Ghidra URL: " + url);
- }
- catch (IOException e) {
- Msg.showError(this, null, "Program Open Failed",
- "Failed to open Ghidra URL: " + e.getMessage());
- }
- finally {
- if (content != null) {
- wrappedContent.release(content, this);
- }
- }
-
- if (program != null) {
- openedProgramList.add(this);
- }
- }
-
- private void messageBadProgramURL(URL url) {
- Msg.error("Invalid Ghidra URL",
- "Ghidra URL does not reference a Ghidra Program: " + url);
- }
- }
-
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java
new file mode 100644
index 0000000000..07f99c04c0
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java
@@ -0,0 +1,304 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.util.task;
+
+import java.io.IOException;
+import java.net.URL;
+
+import docking.widgets.OptionDialog;
+import ghidra.app.plugin.core.progmgr.ProgramLocator;
+import ghidra.app.util.dialog.CheckoutDialog;
+import ghidra.framework.client.ClientUtil;
+import ghidra.framework.client.RepositoryAdapter;
+import ghidra.framework.main.AppInfo;
+import ghidra.framework.model.DomainFile;
+import ghidra.framework.protocol.ghidra.GhidraURLConnection;
+import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
+import ghidra.framework.protocol.ghidra.GhidraURLWrappedContent;
+import ghidra.framework.remote.User;
+import ghidra.framework.store.ExclusiveCheckoutException;
+import ghidra.program.model.lang.LanguageNotFoundException;
+import ghidra.program.model.listing.Program;
+import ghidra.util.*;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.exception.VersionException;
+import ghidra.util.task.TaskMonitor;
+
+/**
+ * Helper class that contains the logic for opening program for all the various program locations
+ * and program states. It handles opening DomainFiles, URLs, versioned DomainFiles, and links
+ * to DomainFiles. It also handles upgrades and checkouts.
+ */
+public class ProgramOpener {
+ private final Object consumer;
+ private String openPromptText = "Open";
+ private boolean silent = false; // if true operation does not permit interaction
+ private boolean noCheckout = false; // if true operation should not perform optional checkout
+
+ /**
+ * Constructs this class with a consumer to use when opening a program.
+ * @param consumer the consumer for opening a program
+ */
+ public ProgramOpener(Object consumer) {
+ this.consumer = consumer;
+ }
+
+ /**
+ * Sets the text to use for the base action type for various prompts that can appear
+ * when opening programs. (The default is "Open".) For example, you may want to override
+ * this so be something like "Open Source", or "Open target".
+ * @param text the text to use as the base action name.
+ */
+ public void setPromptText(String text) {
+ openPromptText = text;
+ }
+
+ /**
+ * Invoking this method prior to task execution will prevent any confirmation interaction with
+ * the user (e.g., optional checkout, snapshot recovery, etc.). Errors may still be displayed
+ * if they occur.
+ */
+ public void setSilent() {
+ this.silent = true;
+ }
+
+ /**
+ * Invoking this method prior to task execution will prevent the use of optional checkout which
+ * require prompting the user.
+ */
+ public void setNoCheckout() {
+ this.noCheckout = true;
+ }
+
+ /**
+ * Opens the program for the given location
+ * @param locator the program location to open
+ * @param monitor the TaskMonitor used for status and cancelling
+ * @return the opened program or null if the operation failed or was cancelled
+ */
+ public Program openProgram(ProgramLocator locator, TaskMonitor monitor) {
+ if (locator.isURL()) {
+ return openURL(locator, monitor);
+ }
+ return openProgram(locator, locator.getDomainFile(), monitor);
+ }
+
+ private Program openURL(ProgramLocator locator, TaskMonitor monitor) {
+ URL url = locator.getURL();
+ GhidraURLWrappedContent wrappedContent = getWrappedContent(url);
+ if (wrappedContent == null) {
+ return null;
+ }
+ DomainFile remoteDomainFile = getDomainFile(url, wrappedContent);
+ if (remoteDomainFile == null) {
+ return null;
+ }
+
+ try {
+ return openProgram(locator, remoteDomainFile, monitor);
+ }
+ finally {
+ wrappedContent.release(remoteDomainFile, this);
+ }
+ }
+
+ private DomainFile getDomainFile(URL url, GhidraURLWrappedContent wrappedContent) {
+ try {
+ Object content = wrappedContent.getContent(this);
+ if (content instanceof DomainFile domainFile) {
+ return domainFile;
+ }
+ messageBadProgramURL(url);
+ if (content != null) {
+ wrappedContent.release(content, this);
+ }
+ }
+ catch (IOException e) {
+ Msg.showError(this, null, "Program Open Failed", "Failed to open Ghidra URL: " + url,
+ e);
+ }
+ return null;
+ }
+
+ private GhidraURLWrappedContent getWrappedContent(URL url) {
+ try {
+ GhidraURLConnection c = (GhidraURLConnection) url.openConnection();
+ Object obj = c.getContent(); // read-only access
+
+ if (c.getStatusCode() == StatusCode.UNAUTHORIZED) {
+ return null; // assume user already notified
+ }
+
+ if (obj instanceof GhidraURLWrappedContent wrappedContent) {
+ return wrappedContent;
+ }
+ return null;
+ }
+ catch (IOException e) {
+ Msg.showError(this, null, "Program Open Failed", "Failed to open Ghidra URL: " + url,
+ e);
+ }
+ return null;
+ }
+
+ private Program openProgram(ProgramLocator locator, DomainFile domainFile,
+ TaskMonitor monitor) {
+
+ if (!Program.class.isAssignableFrom(domainFile.getDomainObjectClass())) {
+ Msg.showError(this, null, "Error Opening Program",
+ "File does not correspond to a Ghidra Program: " + locator);
+ return null;
+ }
+
+ int version = locator.getVersion();
+ if (version != DomainFile.DEFAULT_VERSION) {
+ monitor.setMessage("Getting Version " + version + " for " + domainFile.getName());
+ return openReadOnly(locator, domainFile, monitor);
+ }
+ monitor.setMessage("Opening " + locator);
+ if (locator.isURL()) {
+ return openReadOnly(locator, domainFile, monitor);
+ }
+ return openNormal(domainFile, monitor);
+
+ }
+
+ private Program openNormal(DomainFile domainFile, TaskMonitor monitor) {
+ String filename = domainFile.getName();
+ performOptionalCheckout(domainFile, monitor);
+ try {
+ return openFileMaybeUgrade(domainFile, monitor);
+ }
+ catch (VersionException e) {
+ String contentType = domainFile.getContentType();
+ VersionExceptionHandler.showVersionError(null, filename, contentType, "Open", e);
+ }
+ catch (CancelledException e) {
+ // we don't care, the task has been cancelled
+ }
+ catch (LanguageNotFoundException e) {
+ Msg.showError(this, null, "Error Opening " + filename,
+ e.getMessage() + "\nPlease contact the Ghidra team for assistance.");
+ }
+ catch (Exception e) {
+ if (domainFile.isInWritableProject() && (e instanceof IOException)) {
+ RepositoryAdapter repo = domainFile.getParent().getProjectData().getRepository();
+ ClientUtil.handleException(repo, e, "Open File", null);
+ }
+ else {
+ Msg.showError(this, null, "Error Opening " + filename,
+ "Getting domain object failed.\n" + e.getMessage(), e);
+ }
+ }
+ return null;
+ }
+
+ private Program openReadOnly(ProgramLocator locator, DomainFile domainFile,
+ TaskMonitor monitor) {
+ String contentType = domainFile.getContentType();
+ String path = locator.toString();
+ try {
+ return (Program) domainFile.getReadOnlyDomainObject(consumer, locator.getVersion(),
+ monitor);
+ }
+ catch (CancelledException e) {
+ // we don't care, the task has been cancelled
+ }
+ catch (IOException e) {
+ if (locator.isDomainFile() && domainFile.isInWritableProject()) {
+ ClientUtil.handleException(AppInfo.getActiveProject().getRepository(), e,
+ "Get " + contentType, null);
+ }
+ else if (locator.getVersion() != DomainFile.DEFAULT_VERSION) {
+ Msg.showError(this, null, "Error Getting Versioned Program",
+ "Could not get version " + locator.getVersion() + " for " + path, e);
+ }
+ else {
+ Msg.showError(this, null, "Error Getting Program",
+ "Open program failed for " + path, e);
+ }
+ }
+ catch (VersionException e) {
+ VersionExceptionHandler.showVersionError(null, domainFile.getName(), contentType,
+ "Open", e);
+ }
+ return null;
+ }
+
+ private void performOptionalCheckout(DomainFile domainFile, TaskMonitor monitor) {
+
+ if (silent || noCheckout || !domainFile.canCheckout()) {
+ return;
+ }
+
+ User user = domainFile.getParent().getProjectData().getUser();
+
+ CheckoutDialog dialog = new CheckoutDialog(domainFile, user);
+ if (dialog.showDialog() == CheckoutDialog.CHECKOUT) {
+ try {
+ monitor.setMessage("Checking Out " + domainFile.getName());
+ if (domainFile.checkout(dialog.exclusiveCheckout(), monitor)) {
+ return;
+ }
+ Msg.showError(this, null, "Checkout Failed", "Exclusive checkout failed for: " +
+ domainFile.getName() + "\nOne or more users have file checked out!");
+ }
+ catch (CancelledException e) {
+ // we don't care, the task has been cancelled
+ }
+ catch (ExclusiveCheckoutException e) {
+ Msg.showError(this, null, "Checkout Failed", e.getMessage());
+ }
+ catch (IOException e) {
+ Msg.showError(this, null, "Error on Check Out", e.getMessage(), e);
+ }
+ }
+ }
+
+ private Program openFileMaybeUgrade(DomainFile domainFile, TaskMonitor monitor)
+ throws IOException, CancelledException, VersionException {
+
+ boolean recoverFile = false;
+ if (!silent && domainFile.isInWritableProject() && domainFile.canRecover()) {
+ recoverFile = askRecoverFile(domainFile.getName());
+ }
+
+ Program program = null;
+ try {
+ program = (Program) domainFile.getDomainObject(consumer, false, recoverFile, monitor);
+ }
+ catch (VersionException e) {
+ if (VersionExceptionHandler.isUpgradeOK(null, domainFile, openPromptText, e)) {
+ program =
+ (Program) domainFile.getDomainObject(consumer, true, recoverFile, monitor);
+ }
+ }
+ return program;
+ }
+
+ private boolean askRecoverFile(final String filename) {
+
+ int option = OptionDialog.showYesNoDialog(null, "Crash Recovery Data Found",
+ "" + HTMLUtilities.escapeHTML(filename) + " has crash data. " +
+ "Would you like to recover unsaved changes?");
+ return option == OptionDialog.OPTION_ONE;
+ }
+
+ private void messageBadProgramURL(URL url) {
+ Msg.error("Invalid Ghidra URL", "Ghidra URL does not reference a Ghidra Program: " + url);
+ }
+
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/test/TestEnv.java b/Ghidra/Features/Base/src/main/java/ghidra/test/TestEnv.java
index a7c0f20961..075d4fb9a9 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/test/TestEnv.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/test/TestEnv.java
@@ -882,7 +882,7 @@ public class TestEnv {
AbstractGuiTest.runSwing(() -> {
PluginTool newTool = doLaunchTool(toolName);
ref.set(newTool);
- if (newTool != null) {
+ if (newTool != null && domainFile != null) {
newTool.acceptDomainFiles(new DomainFile[] { domainFile });
}
});
@@ -1007,9 +1007,8 @@ public class TestEnv {
private void cleanupAutoAnalysisManagers(PluginTool t) {
@SuppressWarnings("unchecked")
- Map map =
- (Map) TestUtils.getInstanceField("managerMap",
- AutoAnalysisManager.class);
+ Map map = (Map) TestUtils
+ .getInstanceField("managerMap", AutoAnalysisManager.class);
Collection managers = map.values();
for (AutoAnalysisManager manager : managers) {
@SuppressWarnings("unchecked")
@@ -1103,8 +1102,8 @@ public class TestEnv {
}
private void deleteTestProject(String projectName) {
- boolean deletedProject = AbstractGhidraHeadlessIntegrationTest.deleteProject(
- AbstractGTest.getTestDirectoryPath(), projectName);
+ boolean deletedProject = AbstractGhidraHeadlessIntegrationTest
+ .deleteProject(AbstractGTest.getTestDirectoryPath(), projectName);
if (!deletedProject) {
Msg.error(TestEnv.class, "dispose() - Open programs after disposing project: ");
@@ -1131,9 +1130,8 @@ public class TestEnv {
// Note: background tool tasks are disposed by the tool
@SuppressWarnings("unchecked")
- Map tasks =
- (Map) TestUtils.getInstanceField("runningTasks",
- TaskUtilities.class);
+ Map tasks = (Map) TestUtils
+ .getInstanceField("runningTasks", TaskUtilities.class);
for (TaskMonitor tm : tasks.values()) {
tm.cancel();
}
@@ -1193,9 +1191,8 @@ public class TestEnv {
// the managers will dispose the managers.
//
@SuppressWarnings("unchecked")
- WeakSet s =
- (WeakSet) TestUtils.getInstanceField("instances",
- SwingUpdateManager.class);
+ WeakSet s = (WeakSet) TestUtils
+ .getInstanceField("instances", SwingUpdateManager.class);
/* Debug for undisposed SwingUpdateManagers
Msg.out("complete update manager list: ");
diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/progmgr/ProgramCacheTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/progmgr/ProgramCacheTest.java
new file mode 100644
index 0000000000..e50d7dec30
--- /dev/null
+++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/progmgr/ProgramCacheTest.java
@@ -0,0 +1,115 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.progmgr;
+
+import static org.junit.Assert.*;
+
+import java.time.Duration;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import ghidra.program.database.ProgramBuilder;
+import ghidra.program.model.listing.Program;
+import ghidra.test.AbstractGhidraHeadlessIntegrationTest;
+
+public class ProgramCacheTest extends AbstractGhidraHeadlessIntegrationTest {
+ private static long KEEP_TIME = 100;
+ private static int MAX_SIZE = 4;
+
+ private ProgramCache cache;
+ private Program program;
+ private ProgramLocator locator;
+
+ @Before
+ public void setup() throws Exception {
+ cache = new ProgramCache(Duration.ofMillis(KEEP_TIME), MAX_SIZE);
+ program = buildProgram();
+ locator = new ProgramLocator(program.getDomainFile());
+ }
+
+ private Program buildProgram() throws Exception {
+ ProgramBuilder builder = new ProgramBuilder("Test Program", ProgramBuilder._TOY, this);
+ return builder.getProgram();
+ }
+
+ @Test
+ public void testCacheReleasesProgramWithNoOtherConsumers() {
+ assertFalse(program.isClosed());
+ cache.put(locator, program);
+ program.release(this); // close the only other consumer besides cache
+
+ assertEquals(1, cache.size());
+ assertFalse(program.isClosed());
+ sleep(110);
+ assertEquals(0, cache.size());
+ assertTrue(program.isClosed());
+ }
+
+ @Test
+ public void testCacheDoesNotReleaseProgramWhenOtherConsumersExist() {
+ assertFalse(program.isClosed());
+ cache.put(locator, program);
+
+ assertEquals(1, cache.size());
+ assertFalse(program.isClosed());
+ sleep(110);
+ assertEquals(1, cache.size());
+ assertFalse(program.isClosed());
+
+ program.release(this); // close the only other consumer besides cache
+ sleep(110);
+ assertEquals(0, cache.size());
+ assertTrue(program.isClosed());
+
+ }
+
+ @Test
+ public void testAddingProgramTwiceOnlyAddsConsumerOnce() {
+ cache.put(locator, program);
+ cache.put(locator, program);
+ cache.put(locator, program);
+ program.release(this); // release this so as to not confuse the issue
+
+ assertEquals(1, program.getConsumerList().size());
+ sleep(110);
+ assertEquals(0, cache.size());
+ assertTrue(program.isClosed());
+
+ }
+
+ @Test
+ public void testTooManuProgramsRemovesOldest() throws Exception {
+ cache.put(locator, program);
+
+ Program p1 = buildProgram();
+ cache.put(new ProgramLocator(p1.getDomainFile()), p1);
+ Program p2 = buildProgram();
+ cache.put(new ProgramLocator(p2.getDomainFile()), p2);
+ Program p3 = buildProgram();
+ cache.put(new ProgramLocator(p3.getDomainFile()), p3);
+
+ assertEquals(2, program.getConsumerList().size());
+
+ Program p4 = buildProgram();
+ cache.put(new ProgramLocator(p4.getDomainFile()), p4);
+
+ // program should have been kicked out as the cache size is only 4
+ assertEquals(1, program.getConsumerList().size());
+
+ }
+
+}
diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/progmgr/ProgramCachingServiceTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/progmgr/ProgramCachingServiceTest.java
new file mode 100644
index 0000000000..0523195fad
--- /dev/null
+++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/progmgr/ProgramCachingServiceTest.java
@@ -0,0 +1,97 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.progmgr;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import org.junit.*;
+
+import ghidra.app.services.ProgramManager;
+import ghidra.framework.model.*;
+import ghidra.framework.plugintool.PluginTool;
+import ghidra.program.database.ProgramBuilder;
+import ghidra.program.model.listing.Program;
+import ghidra.test.AbstractGhidraHeadedIntegrationTest;
+import ghidra.test.TestEnv;
+import ghidra.util.InvalidNameException;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+public class ProgramCachingServiceTest extends AbstractGhidraHeadedIntegrationTest {
+ private TestEnv env;
+ private Project project;
+ private DomainFolder rootFolder;
+ private DomainFile domainFile;
+ private PluginTool tool;
+ private ProgramManager service;
+
+ @Before
+ public void setup() throws Exception {
+ env = new TestEnv();
+ tool = env.getTool();
+ project = env.getProject();
+ rootFolder = project.getProjectData().getRootFolder();
+ ProgramBuilder builder = new ProgramBuilder("A", ProgramBuilder._TOY, this);
+ Program program = builder.getProgram();
+ domainFile = rootFolder.createFile("A", program, TaskMonitor.DUMMY);
+ service = tool.getService(ProgramManager.class);
+ program.release(this);
+ }
+
+ @After
+ public void tearDown() {
+ env.dispose();
+ }
+
+ @Test
+ public void testCacheProgram() {
+ Object consumer1 = new Object();
+ Object consumer2 = new Object();
+ Program program1 = service.openCachedProgram(domainFile, consumer1);
+ assertEquals(2, program1.getConsumerList().size()); // one we added and one by the cache
+
+ program1.release(consumer1);
+ assertEquals(1, program1.getConsumerList().size()); // just the cache
+
+ Program program2 = service.openCachedProgram(domainFile, consumer2);
+ assertTrue(program1 == program2);
+ assertEquals(2, program2.getConsumerList().size()); // consumer2 and the cache
+
+ }
+
+ @Test
+ public void testSaveAs() throws InvalidNameException, CancelledException, IOException {
+ Object consumer1 = new Object();
+ Object consumer2 = new Object();
+ Program program = service.openCachedProgram(domainFile, consumer1);
+ assertEquals(2, program.getConsumerList().size()); // consumer1 and the cache
+ assertTrue(program.getConsumerList().contains(consumer1));
+
+ rootFolder.createFile("B", program, TaskMonitor.DUMMY); // doing 'Save As'
+ assertEquals(1, program.getConsumerList().size()); // cache should have removed it, so just consumer1
+ assertTrue(program.getConsumerList().contains(consumer1));
+
+ Program other = service.openCachedProgram(domainFile, consumer2);
+ assertTrue(program != other);
+ assertEquals(2, other.getConsumerList().size()); // cache and consumer 2
+ assertTrue(other.getConsumerList().contains(consumer2));
+ assertFalse(other.getConsumerList().contains(consumer1));
+
+ }
+
+}
diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/tool/CloseToolTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/tool/CloseToolTest.java
index 3e46da1431..0945a9ce12 100644
--- a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/tool/CloseToolTest.java
+++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/tool/CloseToolTest.java
@@ -25,6 +25,7 @@ import docking.action.DockingActionIf;
import docking.widgets.OptionDialog;
import ghidra.app.context.ProgramActionContext;
import ghidra.app.plugin.core.progmgr.ProgramManagerPlugin;
+import ghidra.app.services.ProgramManager;
import ghidra.framework.cmd.BackgroundCommand;
import ghidra.framework.model.DomainObject;
import ghidra.framework.plugintool.PluginTool;
@@ -76,9 +77,9 @@ public class CloseToolTest extends AbstractGhidraHeadedIntegrationTest {
ProgramDB program2 =
new ProgramBuilder("WinHelloCPP.exe", ProgramBuilder._TOY).getProgram();
ProgramDB program3 = new ProgramBuilder("DiffTestPgm1", ProgramBuilder._TOY).getProgram();
- pm.openProgram(program1, true);
- pm.openProgram(program2, true);
- pm.openProgram(program3, true);
+ pm.openProgram(program1, ProgramManager.OPEN_CURRENT);
+ pm.openProgram(program2, ProgramManager.OPEN_CURRENT);
+ pm.openProgram(program3, ProgramManager.OPEN_CURRENT);
Program[] allOpenPrograms = pm.getAllOpenPrograms();
assertEquals(3, allOpenPrograms.length);
diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/services/TestDummyProgramManager.java b/Ghidra/Features/Base/src/test/java/ghidra/app/services/TestDummyProgramManager.java
index 798502615a..737052a866 100644
--- a/Ghidra/Features/Base/src/test/java/ghidra/app/services/TestDummyProgramManager.java
+++ b/Ghidra/Features/Base/src/test/java/ghidra/app/services/TestDummyProgramManager.java
@@ -15,7 +15,6 @@
*/
package ghidra.app.services;
-import java.awt.Component;
import java.net.URL;
import ghidra.framework.model.DomainFile;
@@ -52,6 +51,12 @@ public class TestDummyProgramManager implements ProgramManager {
return null;
}
+ @Override
+ public Program openCachedProgram(URL ghidraURL, Object consumer) {
+ // stub
+ return null;
+ }
+
@Override
public Program openProgram(DomainFile domainFile) {
// stub
@@ -59,7 +64,7 @@ public class TestDummyProgramManager implements ProgramManager {
}
@Override
- public Program openProgram(DomainFile domainFile, Component dialogParent) {
+ public Program openCachedProgram(DomainFile domainFile, Object consumer) {
// stub
return null;
}
@@ -81,11 +86,6 @@ public class TestDummyProgramManager implements ProgramManager {
// stub
}
- @Override
- public void openProgram(Program program, boolean current) {
- // stub
- }
-
@Override
public void openProgram(Program program, int state) {
// stub
@@ -156,16 +156,4 @@ public class TestDummyProgramManager implements ProgramManager {
// stub
return null;
}
-
- @Override
- public void lockDown(boolean state) {
- // stub
- }
-
- @Override
- public boolean isLocked() {
- // stub
- return false;
- }
-
}
diff --git a/Ghidra/Features/ProgramDiff/src/main/java/ghidra/app/plugin/core/diff/DiffProgramManager.java b/Ghidra/Features/ProgramDiff/src/main/java/ghidra/app/plugin/core/diff/DiffProgramManager.java
index 4c73890b22..0a2b9ec36a 100644
--- a/Ghidra/Features/ProgramDiff/src/main/java/ghidra/app/plugin/core/diff/DiffProgramManager.java
+++ b/Ghidra/Features/ProgramDiff/src/main/java/ghidra/app/plugin/core/diff/DiffProgramManager.java
@@ -15,7 +15,6 @@
*/
package ghidra.app.plugin.core.diff;
-import java.awt.Component;
import java.net.URL;
import ghidra.app.services.ProgramManager;
@@ -79,18 +78,23 @@ public class DiffProgramManager implements ProgramManager {
return null;
}
+ @Override
+ public Program openCachedProgram(URL ghidraURL, Object consumer) {
+ return null;
+ }
+
@Override
public Program openProgram(DomainFile domainFile) {
return null;
}
@Override
- public Program openProgram(DomainFile df, int version) {
+ public Program openCachedProgram(DomainFile domainFile, Object consumer) {
return null;
}
@Override
- public Program openProgram(DomainFile domainFile, Component dialogParent) {
+ public Program openProgram(DomainFile df, int version) {
return null;
}
@@ -104,11 +108,6 @@ public class DiffProgramManager implements ProgramManager {
// stub
}
- @Override
- public void openProgram(Program program, boolean current) {
- // stub
- }
-
@Override
public void openProgram(Program program, int state) {
// stub
@@ -148,14 +147,4 @@ public class DiffProgramManager implements ProgramManager {
public boolean setPersistentOwner(Program program, Object owner) {
return false;
}
-
- @Override
- public boolean isLocked() {
- return false;
- }
-
- @Override
- public void lockDown(boolean state) {
- // Not doing anything
- }
}
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java
index 110a103a92..142adcbb57 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java
@@ -20,8 +20,8 @@ import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import db.*;
+import ghidra.app.util.task.OpenProgramRequest;
import ghidra.app.util.task.OpenProgramTask;
-import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest;
import ghidra.feature.vt.api.correlator.program.ImpliedMatchProgramCorrelator;
import ghidra.feature.vt.api.correlator.program.ManualMatchProgramCorrelator;
import ghidra.feature.vt.api.impl.*;
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/util/EmptyVTSession.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/util/EmptyVTSession.java
index 864c4e9e9b..8956970089 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/util/EmptyVTSession.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/util/EmptyVTSession.java
@@ -21,6 +21,7 @@ import java.util.*;
import db.Transaction;
import ghidra.feature.vt.api.main.*;
+import ghidra.framework.data.DomainObjectFileListener;
import ghidra.framework.model.*;
import ghidra.framework.options.Options;
import ghidra.framework.store.LockException;
@@ -91,6 +92,16 @@ public class EmptyVTSession implements VTSession {
// do nothing
}
+ @Override
+ public void addDomainFileListener(DomainObjectFileListener listener) {
+ // do nothing
+ }
+
+ @Override
+ public void removeDomainFileListener(DomainObjectFileListener listener) {
+ // do nothing
+ }
+
@Override
public EventQueueID createPrivateEventQueue(DomainObjectListener listener, int maxDelay) {
return null;
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTSubToolManager.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTSubToolManager.java
index 7b87f176d9..48931bfc15 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTSubToolManager.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTSubToolManager.java
@@ -84,7 +84,7 @@ public class VTSubToolManager implements VTControllerListener, OptionsChangeList
destinationTool = createTool(DESTINATION_TOOL_NAME, false);
}
ProgramManager service = destinationTool.getService(ProgramManager.class);
- return service.openProgram(domainFile, parent);
+ return service.openProgram(domainFile);
}
Program openSourceProgram(DomainFile domainFile, Component parent) {
@@ -92,7 +92,7 @@ public class VTSubToolManager implements VTControllerListener, OptionsChangeList
sourceTool = createTool(SOURCE_TOOL_NAME, true);
}
ProgramManager service = sourceTool.getService(ProgramManager.class);
- return service.openProgram(domainFile, parent);
+ return service.openProgram(domainFile);
}
void closeSourceProgram(Program source) {
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/NewSessionPanel.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/NewSessionPanel.java
index d4359d9769..c5080c6d64 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/NewSessionPanel.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/NewSessionPanel.java
@@ -30,8 +30,8 @@ import docking.wizard.*;
import generic.theme.GIcon;
import generic.theme.GThemeDefaults.Ids.Fonts;
import generic.theme.Gui;
+import ghidra.app.util.task.OpenProgramRequest;
import ghidra.app.util.task.OpenProgramTask;
-import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest;
import ghidra.framework.main.DataTreeDialog;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/timer/GTimer.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/timer/GTimer.java
index c8f932379b..bb80759137 100644
--- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/timer/GTimer.java
+++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/timer/GTimer.java
@@ -80,8 +80,8 @@ public class GTimer {
static class GTimerTask extends TimerTask implements GTimerMonitor {
private final Runnable runnable;
- private boolean wasCancelled;
- private boolean wasRun;
+ private transient boolean wasCancelled;
+ private transient boolean wasRun;
GTimerTask(Runnable runnable) {
this.runnable = runnable;
diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/timer/GTimerCache.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/timer/GTimerCache.java
new file mode 100644
index 0000000000..46621b07bd
--- /dev/null
+++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/timer/GTimerCache.java
@@ -0,0 +1,336 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.util.timer;
+
+import java.time.Duration;
+import java.util.*;
+import java.util.Map.Entry;
+
+/**
+ * Class for caching key,value entries for a limited time and cache size. Entries in this cache
+ * will be removed after the cache duration time has passed. If the cache ever exceeds its capacity,
+ * the least recently used entry will be removed.
+ *
+ * This class uses a {@link LinkedHashMap} with it ordering mode set to "access order". This means
+ * that iterating through keys, values, or entries of the map will be presented oldest first.
+ * Inserting or accessing an entry in the map will move the entry to the back of the list, thus
+ * making it the youngest. This means that entries closest to or past expiration will be presented
+ * first.
+ *
+ * This class is designed to be subclassed for two specific cases. The first case is for when
+ * additional processing is required when an entry is removed from the cache. This typically would
+ * be for cases where resources need to be released, such as closing a File or disposing the object.
+ * The second reason to subclass this cache is to get more control of expiring values. Overriding
+ * {@link #shouldRemoveFromCache(Object, Object)}, which gets called when an entry's time
+ * has expired, gives the client a chance to decide if the entry should be removed.
+ *
+ * @param the key
+ * @param the value
+ */
+public class GTimerCache {
+ // These defines are the HashMap defaults, but the map class didn't provide public constants
+ private static final int INITIAL_MAP_SIZE = 16;
+ private static final float LOAD_FACTOR = 0.75f;
+
+ private int capacity;
+ private long lifetime;
+ private Runnable timerExpiredRunnable = this::timerExpired;
+
+ // the following fields should only be used in synchronized blocks
+ private Map map;
+ private GTimerMonitor timerMonitor;
+
+ /**
+ * Constructs new GTimerCache with a duration for cached entries and a maximum
+ * number of entries to cache.
+ * @param lifetime the duration that a key,value will remain in the cache without being
+ * accessed (accessing a cached entry resets its time)
+ * @param capacity the maximum number of entries in the cache before least recently used
+ * entries are removed
+ */
+ public GTimerCache(Duration lifetime, int capacity) {
+ if (lifetime.isZero() || lifetime.isNegative()) {
+ throw new IllegalArgumentException("The duration must be a time > 0!");
+ }
+ if (capacity < 1) {
+ throw new IllegalArgumentException("The capacity must be > 0!");
+ }
+ this.lifetime = lifetime.toMillis();
+ this.capacity = capacity;
+
+ map = new LinkedHashMap<>(INITIAL_MAP_SIZE, LOAD_FACTOR, true) {
+ @Override
+ protected boolean removeEldestEntry(Entry eldest) {
+ if (size() > GTimerCache.this.capacity) {
+ valueRemoved(eldest.getKey(), eldest.getValue().getValue());
+ return true;
+ }
+ return false;
+ }
+ };
+ }
+
+ /**
+ * Sets the capacity for this cache. If this cache currently has more values than the new
+ * capacity, oldest values will be removed.
+ * @param capacity the new capacity for this cache
+ */
+ public synchronized void setCapacity(int capacity) {
+ if (capacity < 1) {
+ throw new IllegalArgumentException("The capacity must be > 0!");
+ }
+ this.capacity = capacity;
+ if (map.size() <= capacity) {
+ return;
+ }
+
+ Iterator> it = map.entrySet().iterator();
+ int n = map.size() - capacity;
+ for (int i = 0; i < n; i++) {
+ Entry next = it.next();
+ it.remove();
+ CachedValue value = next.getValue();
+ valueRemoved(value.getKey(), value.getValue());
+ }
+ }
+
+ /**
+ * Sets the duration for keeping cached values.
+ * @param duration the length of time to keep a cached value
+ */
+ public synchronized void setDuration(Duration duration) {
+ if (duration.isZero() || duration.isNegative()) {
+ throw new IllegalArgumentException("The duration must be a time > 0!");
+ }
+
+ this.lifetime = duration.toMillis();
+ if (timerMonitor != null) {
+ timerMonitor.cancel();
+ timerMonitor = null;
+ }
+ timerExpired();// this will purge any older values and reset the timer to the correct delay
+ }
+
+ /**
+ * Adds an key,value entry to the cache
+ * @param key the key with which the value is associated
+ * @param value the value being cached
+ * @return The previous value associated with the key or null if no previous value
+ */
+ public synchronized V put(K key, V value) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(value);
+
+ CachedValue old = map.put(key, new CachedValue(key, value));
+ V previous = old == null ? null : old.getValue();
+ if (!Objects.equals(value, previous)) {
+ if (previous != null) {
+ valueRemoved(key, previous);
+ }
+ valueAdded(key, value);
+ }
+
+ if (timerMonitor == null) {
+ timerMonitor = GTimer.scheduleRunnable(lifetime, timerExpiredRunnable);
+ }
+ return previous;
+ }
+
+ /**
+ * Removes the cache entry with the given key.
+ * @param key the key of the entry to remove
+ * @return the value removed or null if the key wasn't in the cache
+ */
+ public synchronized V remove(K key) {
+ CachedValue removed = map.remove(key);
+ if (removed == null) {
+ return null;
+ }
+ valueRemoved(removed.getKey(), removed.getValue());
+ return removed.value;
+ }
+
+ /**
+ * Returns true if the cache contains a value for the given key.
+ * @param key the key to check if it is in the cache
+ * @return true if the cache contains a value for the given key
+ */
+ public synchronized boolean containsKey(K key) {
+ return map.containsKey(key);
+ }
+
+ /**
+ * Returns the number of entries in the cache.
+ * @return the number of entries in the cache
+ */
+ public synchronized int size() {
+ return map.size();
+ }
+
+ /**
+ * Returns the value for the given key. Also, resets time the associated with this entry.
+ * @param key the key to retrieve a value
+ * @return the value for the given key
+ */
+ public synchronized V get(K key) {
+ // Note: the map's get() updates its access order
+ CachedValue cachedValue = map.get(key);
+ if (cachedValue == null) {
+ return null;
+ }
+ cachedValue.updateAccessTime();
+ return cachedValue.getValue();
+ }
+
+ /**
+ * Clears all the values in the cache. The expired callback will be called for each entry
+ * that was in the cache.
+ */
+ public synchronized void clear() {
+ for (Entry entry : map.entrySet()) {
+ CachedValue value = entry.getValue();
+ valueRemoved(value.getKey(), value.getValue());
+ }
+ map.clear();
+ }
+
+ /**
+ * Called when an item is being removed from the cache. This method is for use by subclasses
+ * that need to do more processing on items as they are removed, such as releasing resources.
+ *
+ * Note: this method will always be called from within a synchronized block. Subclasses should
+ * be careful if they make any external calls from within this method.
+ *
+ * @param key The key of the value being removed
+ * @param value the value that is being removed
+ */
+ protected void valueRemoved(K key, V value) {
+ // stub for subclasses
+ }
+
+ /**
+ * Called when an value is being added to the cache. This method is for use by
+ * subclasses that need to do more processing on items when they are added to the cache.
+ *
+ * Note: this method will always be called from within a synchronized block. Subclasses should
+ * be careful if they make any external calls from within this method.
+ *
+ * @param key The key of the value being added
+ * @param value the new value
+ */
+ protected void valueAdded(K key, V value) {
+ // stub for subclasses
+ }
+
+ /**
+ * Called when an item's cache time has expired to determine if the item should be removed from
+ * the cache. The default to to remove an item when its time has expired. Subclasses can
+ * override this method to have more control over expiring value removal.
+ *
+ * Note: this method will always be called from within a synchronized block. Subclasses should
+ * be careful if they make any external calls from within this method.
+ *
+ * @param key the key of the item whose time has expired
+ * @param value the value of the item whose time has expired
+ * @return true if the item should be removed, false otherwise
+ */
+ protected boolean shouldRemoveFromCache(K key, V value) {
+ return true;
+ }
+
+ private synchronized void timerExpired() {
+ timerMonitor = null;
+ long eventTime = System.currentTimeMillis();
+ List expiredValues = getAndRemoveExpiredValues(eventTime);
+ purgeOrReinstateExpiredValues(expiredValues);
+ restartTimer(eventTime);
+ }
+
+ private List getAndRemoveExpiredValues(long eventTime) {
+ List expiredValues = new ArrayList<>();
+
+ Iterator it = map.values().iterator();
+ while (it.hasNext()) {
+ CachedValue next = it.next();
+ if (!next.isExpired(eventTime)) {
+ // since the map is ordered by expire time, none that follow can be expired
+ break;
+ }
+ expiredValues.add(next);
+ it.remove();
+ }
+ return expiredValues;
+ }
+
+ private void purgeOrReinstateExpiredValues(List expiredValues) {
+ for (CachedValue cachedValue : expiredValues) {
+ if (shouldRemoveFromCache(cachedValue.getKey(), cachedValue.getValue())) {
+ valueRemoved(cachedValue.getKey(), cachedValue.getValue());
+ }
+ else {
+ // The client wants to keep the entry in the cache. We've decided to treat this like
+ // adding a new entry.
+ cachedValue.updateAccessTime();
+ map.put(cachedValue.getKey(), cachedValue);
+ }
+ }
+ }
+
+ private void restartTimer(long eventTime) {
+ if (map.isEmpty()) {
+ return;
+ }
+
+ CachedValue first = map.values().iterator().next();
+ long elapsed = eventTime - first.getLastAccessedTime();
+ long remaining = lifetime - elapsed;
+ timerMonitor = GTimer.scheduleRunnable(remaining, timerExpiredRunnable);
+ }
+
+ private class CachedValue {
+ private final K key;
+ private final V value;
+ private long lastAccessedTime;
+
+ CachedValue(K key, V value) {
+ this.key = key;
+ this.value = value;
+ this.lastAccessedTime = System.currentTimeMillis();
+ }
+
+ void updateAccessTime() {
+ lastAccessedTime = System.currentTimeMillis();
+ }
+
+ long getLastAccessedTime() {
+ return lastAccessedTime;
+ }
+
+ K getKey() {
+ return key;
+ }
+
+ V getValue() {
+ return value;
+ }
+
+ boolean isExpired(long eventTime) {
+ long elapsed = eventTime - lastAccessedTime;
+ return elapsed >= lifetime;
+
+ }
+ }
+}
diff --git a/Ghidra/Framework/Generic/src/test/java/ghidra/util/timer/GTimerCacheTest.java b/Ghidra/Framework/Generic/src/test/java/ghidra/util/timer/GTimerCacheTest.java
new file mode 100644
index 0000000000..0df85f53df
--- /dev/null
+++ b/Ghidra/Framework/Generic/src/test/java/ghidra/util/timer/GTimerCacheTest.java
@@ -0,0 +1,253 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.util.timer;
+
+import static org.junit.Assert.*;
+
+import java.time.Duration;
+import java.util.Deque;
+import java.util.concurrent.ConcurrentLinkedDeque;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import generic.test.AbstractGTest;
+
+public class GTimerCacheTest extends AbstractGTest {
+ private static long KEEP_TIME = 100;
+ private static int MAX_SIZE = 4;
+ private GTimerCache cache;
+ private Deque removed = new ConcurrentLinkedDeque<>();
+
+ @Before
+ public void setup() {
+ cache = new TestTimerCache();
+ }
+
+ @Test
+ public void testValueExpiring() {
+ cache.put("AAA", 5);
+ assertEquals(1, cache.size());
+ assertTrue(cache.containsKey("AAA"));
+
+ sleep(KEEP_TIME - 10);
+ assertEquals(1, cache.size());
+ assertTrue(cache.containsKey("AAA"));
+ assertTrue(removed.isEmpty());
+
+ sleep(200);
+ assertEquals(0, cache.size());
+ assertNull(cache.get("AAA"));
+ assertFalse(cache.containsKey("AAA"));
+ assertFalse(removed.isEmpty());
+ assertEquals(new Removed("AAA", 5), removed.getFirst());
+ }
+
+ @Test
+ public void testAccessingValueKeepsAliveLonger() {
+ cache.put("AAA", 5);
+ sleep(KEEP_TIME - 50);
+ assertEquals(5, (int) cache.get("AAA"));
+ sleep(KEEP_TIME - 10);
+ assertEquals(1, cache.size());
+ sleep(20);
+ assertEquals(0, cache.size());
+ }
+
+ @Test
+ public void testAccessingValueReordersValues() {
+ cache.put("AAA", 5);
+ cache.put("BBB", 8);
+ cache.get("AAA");
+ sleep(KEEP_TIME + 10);
+ assertEquals(0, cache.size());
+ assertEquals(2, removed.size());
+ assertEquals(new Removed("BBB", 8), removed.getFirst());
+ assertEquals(new Removed("AAA", 5), removed.getLast());
+ }
+
+ @Test
+ public void testMaxsize() {
+ // max size is 4, so put in 6 and see that the first two are removed (And the "expired"
+ // callback is called for them)
+
+ cache.put("A", 1);
+ cache.put("B", 2);
+ cache.put("C", 3);
+ cache.put("D", 4);
+ cache.put("E", 5);
+ cache.put("F", 6);
+
+ assertEquals(4, cache.size());
+ assertEquals(2, removed.size());
+ assertEquals(new Removed("A", 1), removed.getFirst());
+ assertEquals(new Removed("B", 2), removed.getLast());
+ }
+
+ @Test
+ public void testRemove() {
+ cache.put("A", 1);
+ Integer removedValue = cache.remove("A");
+ assertEquals(1, (int) removedValue);
+ // verify that the expired consumer wasn't called with "A" since we deleted it before the
+ // cache expired
+ sleep(KEEP_TIME + 10);
+ assertEquals(0, cache.size());
+ assertEquals(1, removed.size());
+
+ }
+
+ @Test
+ public void testRemoveNonExistent() {
+ cache.put("A", 1);
+ assertNull(cache.remove("B"));
+ }
+
+ @Test
+ public void testClear() {
+ cache.put("A", 1);
+ cache.put("B", 2);
+
+ cache.clear();
+ assertEquals(2, removed.size());
+
+ }
+
+ @Test
+ public void testSetCapacitySmaller() {
+ // fill cache to current capacity (4)
+ cache.put("A", 1);
+ cache.put("B", 2);
+ cache.put("C", 3);
+ cache.put("D", 4);
+ // set cache size to 2 and see that two items are removed
+ assertEquals(4, cache.size());
+ cache.setCapacity(2);
+ assertEquals(2, cache.size());
+
+ assertEquals(2, removed.size());
+ }
+
+ @Test
+ public void testSetCapacityLarger() {
+ // fill cache to current capacity (4)
+ cache.put("A", 1);
+ cache.put("B", 2);
+ cache.put("C", 3);
+ cache.put("D", 4);
+ // set cache size to 6 and see the cache stays the same
+ assertEquals(4, cache.size());
+ cache.setCapacity(6);
+ assertEquals(4, cache.size());
+
+ assertEquals(0, removed.size());
+ }
+
+ @Test
+ public void testSetDurationShorterWithTimeStillRemainingOnCachedItem() {
+ cache.put("A", 1);
+ cache.setDuration(Duration.ofMillis(50));
+ sleep(40);
+ assertEquals(1, cache.size());
+ sleep(15);
+ assertEquals(0, cache.size());
+ }
+
+ @Test
+ public void testSetDurationShorterWithImmediateExpirationOnCachedItem() {
+ cache.put("A", 1);
+ sleep(50);
+ cache.setDuration(Duration.ofMillis(40));
+ assertEquals(0, cache.size());
+ assertEquals(1, removed.size());
+ }
+
+ @Test
+ public void testSetDurationLonger() {
+ cache.put("A", 1);
+ sleep(50);
+ cache.setDuration(Duration.ofMillis(150));
+ assertEquals(1, cache.size());
+ sleep(60);
+ assertEquals(1, cache.size());
+ sleep(50);
+ assertEquals(0, cache.size());
+ }
+
+ @Test
+ public void testPuttingInNewValueWithSameKeyReportsOldValueAndCallsRemovedCallback() {
+ assertNull(cache.put("A", 1));
+ assertEquals(1, (int) cache.put("A", 2));
+ assertEquals(1, removed.size());
+ assertEquals("A", removed.getFirst().key());
+ assertEquals(1, removed.getFirst().value());
+ }
+
+ @Test
+ public void testPuttingInEqualValueWithSameKeyReportsOldValueAndDoesNotCallRemovedCallback() {
+ assertNull(cache.put("A", 1));
+ assertEquals(1, (int) cache.put("A", 1));
+ assertEquals(0, removed.size());
+ }
+
+ @Test
+ public void testTimerExpiredButShouldRemovedReturnedFalse() {
+ cache = new KeepOnceTestTimerCache();
+ cache.put("A", 1);
+ sleep(110);
+ assertEquals(1, cache.size()); // first time expired, the item should remain in cache
+ assertEquals(0, removed.size());
+
+ sleep(110);
+ assertEquals(0, cache.size());
+ assertEquals(1, removed.size());
+
+ }
+
+ class TestTimerCache extends GTimerCache {
+
+ public TestTimerCache() {
+ super(Duration.ofMillis(KEEP_TIME), MAX_SIZE);
+
+ }
+
+ @Override
+ protected void valueRemoved(String key, Integer value) {
+ removed.add(new Removed(key, value));
+ }
+
+ }
+
+ // keeps an item it the cache the first time it is ever called
+ class KeepOnceTestTimerCache extends TestTimerCache {
+ boolean shouldRemove = false;
+
+ @Override
+ protected boolean shouldRemoveFromCache(String key, Integer value) {
+ // keeps it around for 1 expiration
+ if (shouldRemove) {
+ return true;
+ }
+ shouldRemove = true;
+ return false;
+ }
+
+ }
+
+ record Removed(String key, int value) {
+ }
+
+}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java
index c391c727fd..bd75b57952 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java
@@ -18,7 +18,6 @@ package ghidra.framework.data;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArraySet;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
@@ -28,6 +27,7 @@ import ghidra.framework.store.FileSystem;
import ghidra.framework.store.LockException;
import ghidra.util.Lock;
import ghidra.util.classfinder.ClassSearcher;
+import ghidra.util.datastruct.ListenerSet;
/**
* An abstract class that provides default behavior for DomainObject(s), specifically it handles
@@ -54,7 +54,11 @@ public abstract class DomainObjectAdapter implements DomainObject {
protected Map changeSupportMap =
new ConcurrentHashMap();
private volatile boolean eventsEnabled = true;
- private Set closeListeners = new CopyOnWriteArraySet<>();
+
+ private ListenerSet closeListeners =
+ new ListenerSet<>(DomainObjectClosedListener.class, false);
+ private ListenerSet fileChangeListeners =
+ new ListenerSet<>(DomainObjectFileListener.class, false);
private ArrayList consumers;
protected Map metadata = new LinkedHashMap();
@@ -185,10 +189,15 @@ public abstract class DomainObjectAdapter implements DomainObject {
if (df == null) {
throw new IllegalArgumentException("DomainFile must not be null");
}
+ if (df == domainFile) {
+ return;
+ }
clearDomainObj();
DomainFile oldDf = domainFile;
domainFile = df;
fireEvent(new DomainObjectChangeRecord(DO_DOMAIN_FILE_CHANGED, oldDf, df));
+ fileChangeListeners.invoke().domainFileChanged(this);
+
}
protected void close() {
@@ -204,13 +213,7 @@ public abstract class DomainObjectAdapter implements DomainObject {
queue.dispose();
}
- notifyCloseListeners();
- }
-
- private void notifyCloseListeners() {
- for (DomainObjectClosedListener listener : closeListeners) {
- listener.domainObjectClosed(this);
- }
+ closeListeners.invoke().domainObjectClosed(this);
closeListeners.clear();
}
@@ -251,6 +254,16 @@ public abstract class DomainObjectAdapter implements DomainObject {
closeListeners.remove(listener);
}
+ @Override
+ public void addDomainFileListener(DomainObjectFileListener listener) {
+ fileChangeListeners.add(listener);
+ }
+
+ @Override
+ public void removeDomainFileListener(DomainObjectFileListener listener) {
+ fileChangeListeners.remove(listener);
+ }
+
@Override
public EventQueueID createPrivateEventQueue(DomainObjectListener listener, int maxDelay) {
EventQueueID eventQueueID = new EventQueueID();
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectFileListener.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectFileListener.java
new file mode 100644
index 0000000000..f254f7c5f0
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectFileListener.java
@@ -0,0 +1,32 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.framework.data;
+
+import ghidra.framework.model.DomainFile;
+import ghidra.framework.model.DomainObject;
+
+/**
+ * Listener for when the {@link DomainFile} associated with a {@link DomainObject} changes, such
+ * as when a 'Save As' action occurs. Unlike DomainObject events, these callbacks are not buffered
+ * and happen immediately when the DomainFile is changed.
+ */
+public interface DomainObjectFileListener {
+ /**
+ * Notification that the DomainFile for the given DomainObject has changed
+ * @param domainObject the DomainObject whose DomainFile changed
+ */
+ public void domainFileChanged(DomainObject domainObject);
+}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainObject.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainObject.java
index 3c17e4b5b7..5ac50c9107 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainObject.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainObject.java
@@ -20,6 +20,7 @@ import java.io.IOException;
import java.util.List;
import java.util.Map;
+import ghidra.framework.data.DomainObjectFileListener;
import ghidra.framework.options.Options;
import ghidra.util.ReadOnlyException;
import ghidra.util.exception.CancelledException;
@@ -165,6 +166,22 @@ public interface DomainObject {
*/
public void removeCloseListener(DomainObjectClosedListener listener);
+ /**
+ * Adds a listener that will be notified when this DomainFile associated with this
+ * DomainObject changes, such as when a 'Save As' action occurs. Unlike DomainObject events,
+ * these notifications are not buffered and happen immediately when the DomainFile is changed.
+ *
+ * @param listener the listener to be notified when the associated DomainFile changes
+ */
+ public void addDomainFileListener(DomainObjectFileListener listener);
+
+ /**
+ * Removes the given DomainObjectFileListener listener.
+ *
+ * @param listener the listener to remove.
+ */
+ public void removeDomainFileListener(DomainObjectFileListener listener);
+
/**
* Creates a private event queue that can be flushed independently from the main event queue.
* @param listener the listener to be notified of domain object events.
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java
index bdf93627de..6989f1c4aa 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java
@@ -74,6 +74,15 @@ public class GhidraURL {
return str != null && str.startsWith(PROTOCOL_URL_START);
}
+ /**
+ * Tests if the given url is using the Ghidra protocol
+ * @param url the url to test
+ * @return true if the url is using the Ghidra protocol
+ */
+ public static boolean isGhidraURL(URL url) {
+ return url != null && url.getProtocol().equals(PROTOCOL);
+ }
+
/**
* Determine if URL string uses a local format (e.g., {@code ghidra:/path...}).
* Extensive validation is not performed. This method is intended to differentiate
diff --git a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/StubProgram.java b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/StubProgram.java
index 69a5831dac..8644803ab8 100644
--- a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/StubProgram.java
+++ b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/StubProgram.java
@@ -20,6 +20,7 @@ import java.io.IOException;
import java.util.*;
import db.Transaction;
+import ghidra.framework.data.DomainObjectFileListener;
import ghidra.framework.model.*;
import ghidra.framework.options.Options;
import ghidra.framework.store.LockException;
@@ -149,6 +150,16 @@ public class StubProgram implements Program {
throw new UnsupportedOperationException();
}
+ @Override
+ public void addDomainFileListener(DomainObjectFileListener listener) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void removeDomainFileListener(DomainObjectFileListener listener) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public EventQueueID createPrivateEventQueue(DomainObjectListener listener, int maxDelay) {
throw new UnsupportedOperationException();
diff --git a/Ghidra/Framework/Utility/src/main/java/utility/function/Dummy.java b/Ghidra/Framework/Utility/src/main/java/utility/function/Dummy.java
index 933f9be160..2850615c64 100644
--- a/Ghidra/Framework/Utility/src/main/java/utility/function/Dummy.java
+++ b/Ghidra/Framework/Utility/src/main/java/utility/function/Dummy.java
@@ -42,6 +42,16 @@ public class Dummy {
};
}
+ /**
+ * Creates a dummy consumer
+ * @return a dummy consumer
+ */
+ public static BiConsumer biConsumer() {
+ return (t, u) -> {
+ // no-op
+ };
+ }
+
/**
* Creates a dummy function
* @param the input type
@@ -82,6 +92,17 @@ public class Dummy {
return c == null ? consumer() : c;
}
+ /**
+ * Returns the given consumer object if it is not {@code null}. Otherwise, a
+ * {@link #biConsumer()} is returned. This is useful to avoid using {@code null}.
+ *
+ * @param c the consumer function to check for {@code null}
+ * @return a non-null consumer
+ */
+ public static BiConsumer ifNull(BiConsumer c) {
+ return c == null ? biConsumer() : c;
+ }
+
/**
* Returns the given callback object if it is not {@code null}. Otherwise, a {@link #callback()}
* is returned. This is useful to avoid using {@code null}.