GP-3970 program caching and refactoring of ProgramManager and OpenProgramTask

This commit is contained in:
ghidragon 2023-11-27 11:47:18 -05:00
parent 5d487a6518
commit 7d67188d0b
34 changed files with 2198 additions and 948 deletions

View file

@ -621,9 +621,7 @@ public class ObjectTreeModel implements DisplaysModified {
protected List<GTreeNode> generateObjectChildren(TraceObject object) { protected List<GTreeNode> generateObjectChildren(TraceObject object) {
List<GTreeNode> result = ObjectTableModel List<GTreeNode> result = ObjectTableModel
.distinctCanonical(object.getValues(span) .distinctCanonical(object.getValues(span).stream().filter(this::isValueVisible))
.stream()
.filter(this::isValueVisible))
.map(v -> nodeCache.getOrCreateNode(v)) .map(v -> nodeCache.getOrCreateNode(v))
.sorted() .sorted()
.collect(Collectors.toList()); .collect(Collectors.toList());

View file

@ -26,6 +26,7 @@ import org.apache.commons.lang3.tuple.Pair;
import db.Transaction; import db.Transaction;
import ghidra.framework.data.DomainObjectEventQueues; import ghidra.framework.data.DomainObjectEventQueues;
import ghidra.framework.data.DomainObjectFileListener;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.options.Options; import ghidra.framework.options.Options;
import ghidra.framework.store.LockException; import ghidra.framework.store.LockException;
@ -1274,6 +1275,16 @@ public class DBTraceProgramView implements TraceProgramView {
trace.removeCloseListener(listener); trace.removeCloseListener(listener);
} }
@Override
public void addDomainFileListener(DomainObjectFileListener listener) {
throw new UnsupportedOperationException();
}
@Override
public void removeDomainFileListener(DomainObjectFileListener listener) {
throw new UnsupportedOperationException();
}
@Override @Override
public EventQueueID createPrivateEventQueue(DomainObjectListener listener, int maxDelay) { public EventQueueID createPrivateEventQueue(DomainObjectListener listener, int maxDelay) {
getEventTranslator(); getEventTranslator();

View file

@ -21,6 +21,7 @@ import java.util.*;
import db.Transaction; import db.Transaction;
import ghidra.framework.data.DomainObjectEventQueues; import ghidra.framework.data.DomainObjectEventQueues;
import ghidra.framework.data.DomainObjectFileListener;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.options.Options; import ghidra.framework.options.Options;
import ghidra.framework.store.LockException; import ghidra.framework.store.LockException;
@ -478,6 +479,16 @@ public class DBTraceProgramViewRegisters implements TraceProgramView {
view.removeCloseListener(listener); view.removeCloseListener(listener);
} }
@Override
public void addDomainFileListener(DomainObjectFileListener listener) {
throw new UnsupportedOperationException();
}
@Override
public void removeDomainFileListener(DomainObjectFileListener listener) {
throw new UnsupportedOperationException();
}
@Override @Override
public EventQueueID createPrivateEventQueue(DomainObjectListener listener, int maxDelay) { public EventQueueID createPrivateEventQueue(DomainObjectListener listener, int maxDelay) {
return eventQueues.createPrivateEventQueue(listener, maxDelay); return eventQueues.createPrivateEventQueue(listener, maxDelay);

View file

@ -389,7 +389,6 @@ src/main/help/help/topics/MemoryMapPlugin/images/MoveMemory.png||GHIDRA||||END|
src/main/help/help/topics/MemoryMapPlugin/images/SetImageBaseDialog.png||GHIDRA||||END| src/main/help/help/topics/MemoryMapPlugin/images/SetImageBaseDialog.png||GHIDRA||||END|
src/main/help/help/topics/MemoryMapPlugin/images/SplitMemoryBlock.png||GHIDRA||||END| src/main/help/help/topics/MemoryMapPlugin/images/SplitMemoryBlock.png||GHIDRA||||END|
src/main/help/help/topics/Misc/Appendix.htm||GHIDRA||||END| src/main/help/help/topics/Misc/Appendix.htm||GHIDRA||||END|
src/main/help/help/topics/Misc/Tips.htm||NONE||||END|
src/main/help/help/topics/Misc/Welcome_to_Ghidra_Help.htm||GHIDRA||||END| src/main/help/help/topics/Misc/Welcome_to_Ghidra_Help.htm||GHIDRA||||END|
src/main/help/help/topics/Navigation/Navigation.htm||GHIDRA||||END| src/main/help/help/topics/Navigation/Navigation.htm||GHIDRA||||END|
src/main/help/help/topics/Navigation/images/GoToDialog.png||GHIDRA||||END| src/main/help/help/topics/Navigation/images/GoToDialog.png||GHIDRA||||END|

View file

@ -22,12 +22,12 @@
</P> </P>
<BLOCKQUOTE> <BLOCKQUOTE>
<BLOCKQUOTE> <BLOCKQUOTE>
<P><IMG alt="" src="help/shared/note.png"> The Tool Options dialog has a filter text field <P><IMG alt="" src="help/shared/note.png"> The Tool Options dialog has a filter text field
that can be used to quickly find options relating to a keyword. Any options names or that can be used to quickly find options relating to a keyword. Any options names or
descriptions that contain the keyword text will be displayed.<BR> descriptions that contain the keyword text will be displayed.<BR>
</P> </P>
</BLOCKQUOTE> </BLOCKQUOTE>
</BLOCKQUOTE> </BLOCKQUOTE>
<P>To display the <I>Options</I> dialog, select <B>Edit<IMG alt="" border="0" src= <P>To display the <I>Options</I> dialog, select <B>Edit<IMG alt="" border="0" src=
@ -38,17 +38,11 @@
<BLOCKQUOTE> <BLOCKQUOTE>
<P>You can restore any currently selected options panel to its default settings by pressing <P>You can restore any currently selected options panel to its default settings by pressing
the <I><B>Restore Defaults</B></I> button at the bottom of the options panel. Use caution the <I><B>Restore Defaults</B></I> button at the bottom of the options panel. Use caution
when executing this action, as it cannot be undone.</P> when executing this action, as it cannot be undone.</P><BR>
<TABLE border="0" width="100%"> <CENTER>
<TBODY> <IMG alt="" border="0" src="images/RestoreDefaults.png">
<TR> </CENTER><BR>
<TD width="100%">
<P align="center"><IMG alt="" border="1" src="images/RestoreDefaults.png"></P>
</TD>
</TR>
</TBODY>
</TABLE>
</BLOCKQUOTE><BR> </BLOCKQUOTE><BR>
<BR> <BR>
@ -56,37 +50,34 @@
<H2><A name="KeyBindings_Option"></A><B>Key Bindings</B></H2> <H2><A name="KeyBindings_Option"></A><B>Key Bindings</B></H2>
<BLOCKQUOTE> <BLOCKQUOTE>
<P>You can create a new key binding (<I>accelerator key</I>) for an action or modify the <P>You can create a new key binding (<I>accelerator key</I>) for an action or modify the
default key binding. The key binding that you add can be used to execute the action default key binding. The key binding that you add can be used to execute the action using the
using the keyboard. Below we describe the <B>Key Bindings</B> options editor.</P> keyboard. Below we describe the <B>Key Bindings</B> options editor.</P>
<BLOCKQUOTE> <BLOCKQUOTE>
<P><IMG alt="" border="0" src="help/shared/tip.png">Not all key bindings are changeable <P><IMG alt="" border="0" src="help/shared/tip.png">Not all key bindings are changeable via
via the tool options. For example, the following keys cannot be changed: the tool options. For example, the following keys cannot be changed:</P>
<UL> <UL>
<LI> <LI><TT><B>F1, Help, Ctrl-F1, F4</B></TT> (this bindings are reserved and cannot be used
<TT><B>F1, Help, Ctrl-F1, F4</B></TT> (this bindings are reserved and cannot be used when assigning key bindings to actions)</LI>
when assigning key bindings to actions)
</LI> <LI>Menu Navigation Actions: <TT><B>1-9, Page Up/Down, End, Home</B></TT> (these key
<LI> bindings are usable with a menu or popup menu open and are otherwise available for
Menu Navigation Actions: <TT><B>1-9, Page Up/Down, End, Home</B></TT> (these key assignment to key bindings).</LI>
bindings are usable with a menu or popup menu open and are otherwise available for </UL><BR>
assignment to key bindings). <BR>
</LI>
</UL>
</P>
</BLOCKQUOTE> </BLOCKQUOTE>
<BLOCKQUOTE> <BLOCKQUOTE>
<P><IMG alt="" border="0" src="help/shared/tip.png"> You can also change key bindings <P><IMG alt="" border="0" src="help/shared/tip.png"> You can also change key bindings from
from within Ghidra by pressing <B>F4</B> while the mouse is over any toolbar icon or menu within Ghidra by pressing <B>F4</B> while the mouse is over any toolbar icon or menu item.
item. Click <A href="#KeyBindingPopup">here</A> for more info.</P> Click <A href="#KeyBindingPopup">here</A> for more info.</P>
</BLOCKQUOTE> </BLOCKQUOTE>
<P>The <B>Key Bindings</B> option editor has a table with the following sortable columns: <P>The <B>Key Bindings</B> option editor has a table with the following sortable columns:
<I>Action Name</I>, <I>Key Binding</I>, and <I>Plugin Name</I>. To change a value in <I>Action Name</I>, <I>Key Binding</I>, and <I>Plugin Name</I>. To change a value in the
the table, select the row and then edit the text field below the table.</P> table, select the row and then edit the text field below the table.</P>
<UL> <UL>
<LI>The text field below the table captures keystroke combinations entered.</LI> <LI>The text field below the table captures keystroke combinations entered.</LI>
@ -99,16 +90,9 @@
Key Bindings Options panel works the same as for a regular Ghidra Tool.</P> Key Bindings Options panel works the same as for a regular Ghidra Tool.</P>
</BLOCKQUOTE> </BLOCKQUOTE>
<TABLE border="0" width="100%"> <CENTER>
<TBODY> <IMG alt="" border="0" src="images/KeyBindings.png">
<TR> </CENTER><BR>
<TD width="100%">
<P align="center"><IMG alt="" border="0" src="images/KeyBindings.png"></P>
</TD>
</TR>
</TBODY>
</TABLE>
<BLOCKQUOTE> <BLOCKQUOTE>
<H3>Change a Key Binding</H3> <H3>Change a Key Binding</H3>
@ -144,9 +128,8 @@
is enabled), then a dialog is displayed for you to choose what action you want to is enabled), then a dialog is displayed for you to choose what action you want to
perform.</P> perform.</P>
<P>To avoid the extra step of choosing the <P>To avoid the extra step of choosing the action from the dialog, do not map the same key to
action from the dialog, do not map the same key to actions that are applicable in the same actions that are applicable in the same context.</P>
context.</P>
<H3>Remove a Key Binding</H3> <H3>Remove a Key Binding</H3>
@ -194,15 +177,15 @@
<LI>Press <B>OK</B> to import the key bindings.</LI> <LI>Press <B>OK</B> to import the key bindings.</LI>
</OL> </OL>
<P><IMG alt="" border="0" src="help/shared/warning.png"> Importing key bindings will override <P><IMG alt="" border="0" src="help/shared/warning.png"> Importing key bindings will
your current key bindings settings. It is suggested that you <A href="#Export">export your override your current key bindings settings. It is suggested that you <A href=
key bindings</A> before you import so that you may revert to your previous settings if "#Export">export your key bindings</A> before you import so that you may revert to your
necessary.</P> previous settings if necessary.</P>
<P><IMG alt="" border="0" src="help/shared/note.png"> After importing you must save your <P><IMG alt="" border="0" src="help/shared/note.png"> After importing you must save your
tool (<B><FONT size="4">File</FONT></B> <IMG alt="" border="0" src= tool (<B><FONT size="4">File</FONT></B> <IMG alt="" border="0" src="help/shared/arrow.gif">
"help/shared/arrow.gif"> <FONT size="4"><B>Save Tool</B></FONT>) if you want you changes <FONT size="4"><B>Save Tool</B></FONT>) if you want you changes to persist between tool
to persist between tool invocations.</P> invocations.</P>
</BLOCKQUOTE> </BLOCKQUOTE>
<H3><A name="KeyBindingPopup">Key Binding Short-Cut</A></H3> <H3><A name="KeyBindingPopup">Key Binding Short-Cut</A></H3>
@ -259,7 +242,8 @@
<OL> <OL>
<LI>Select <FONT size="4"><B>Edit</B></FONT><IMG alt="" border="0" src= <LI>Select <FONT size="4"><B>Edit</B></FONT><IMG alt="" border="0" src=
"help/shared/arrow.gif"> <FONT size="4"><B>Tool Options</B></FONT> from the menu bar.</LI> "help/shared/arrow.gif"> <FONT size="4"><B>Tool Options</B></FONT> from the menu
bar.</LI>
<LI>Select the <I>Key Bindings</I> node in the options tree.</LI> <LI>Select the <I>Key Bindings</I> node in the options tree.</LI>
@ -283,15 +267,14 @@
basic options. Plugins may add their own options to the <I>Tool</I> options. If a tool does basic options. Plugins may add their own options to the <I>Tool</I> options. If a tool does
not have a plugin that uses an option, the option will not show up on the <I>Tool</I> panel. not have a plugin that uses an option, the option will not show up on the <I>Tool</I> panel.
For example, the Ghidra Project Window does not have plugins that use the Max Go to Entries For example, the Ghidra Project Window does not have plugins that use the Max Go to Entries
or Subroutine Model, so these options will not appear on the <I>Tool</I> panel. or Subroutine Model, so these options will not appear on the <I>Tool</I> panel. If an option
If an option has a description, it will show up in the description panel below the tree when has a description, it will show up in the description panel below the tree when you pass the
you pass the mouse pointer over the component in the options panel.</P> mouse pointer over the component in the options panel.</P>
<DIV align="center"> <DIV align="center">
<CENTER> <CENTER>
<TABLE border="1" width="80%"> <TABLE border="1" width="80%">
<TBODY> <TBODY>
<TR> <TR>
<TH align="left"><B>Option</B></TH> <TH align="left"><B>Option</B></TH>
@ -379,7 +362,6 @@
<CENTER> <CENTER>
<TABLE border="1" width="80%"> <TABLE border="1" width="80%">
<TBODY> <TBODY>
<TR> <TR>
<TH valign="top" align="left"><B>Option</B></TH> <TH valign="top" align="left"><B>Option</B></TH>
@ -395,31 +377,27 @@
others.</TD> others.</TD>
</TR> </TR>
<TR>
<TR>
<TD valign="top" width="200" align="left">Automatically Save Tools</TD> <TD valign="top" width="200" align="left">Automatically Save Tools</TD>
<TD valign="top" align="left">This controls whether Ghidra will save tool state <TD valign="top" align="left">This controls whether Ghidra will save tool state
when the tool is closed.</TD> when the tool is closed.</TD>
</TR> </TR>
<TR>
<TR>
<TD valign="top" width="200" align="left">Restore Previous Project</TD> <TD valign="top" width="200" align="left">Restore Previous Project</TD>
<TD valign="top" align="left">This controls <TD valign="top" align="left">This controls whether or not Ghidra automatically
whether or not Ghidra automatically opens the previously loaded project on opens the previously loaded project on startup.</TD>
startup.</TD>
</TR> </TR>
<TR> <TR>
<TD valign="top" width="200" align="left">Default Tool Launch Mode</TD> <TD valign="top" width="200" align="left">Default Tool Launch Mode</TD>
<TD valign="top" align="left">This controls <TD valign="top" align="left">This controls if a new or already running tool should
if a new or already running tool should be used during default launch. be used during default launch. Tool "reuse" mode will open selected file within a
Tool "reuse" mode will open selected file within a suitable running tool suitable running tool if one can be identified, otherwise a new tool will be
if one can be identified, otherwise a new tool will be launched. launched.</TD>
</TD>
</TR> </TR>
<TR> <TR>
@ -450,7 +428,48 @@
</DIV> </DIV>
</BLOCKQUOTE> </BLOCKQUOTE>
<H3><A name="Program_Caching"></A><B>Program Caching</B></H3>
<BLOCKQUOTE>
<P>Some features of Ghidra require opening programs briefly. Often the same set of programs
may need to be opened repeatedly. Ghidra provides a caching service to make these uses more
efficient. The following two options are available:</P>
<BR>
<DIV align="center">
<CENTER>
<TABLE border="1" width="80%">
<TBODY>
<TR>
<TH valign="top" align="left"><B>Option</B></TH>
<TH valign="top" align="left"><B>Description</B></TH>
</TR>
<TR>
<TD valign="top" width="200" align="left">Program Cache Size</TD>
<TD valign="top" align="left"><A name="Program_Cache_Size"></A>This options
specifies the maximum number of programs to keep open in the cache.</TD>
</TR>
<TR>
<TD valign="top" width="200" align="left">Program Cache Duration</TD>
<TD valign="top" align="left"><A name="Program_Cache_Duration">This option
specifies how long (in minutes) to keep an otherwise unused cached program open. If
the program is in use by some feature, it won't be closed when the time
expires, and it will stay in the cache for the full cache time.</A></TD>
</TR>
</TBODY>
</TABLE>
</CENTER>
</DIV>
</BLOCKQUOTE>
<P class="relatedtopic">Related Topics:<A name="RelatedTopics"></A></P> <P class="relatedtopic">Related Topics:<A name="RelatedTopics"></A></P>
<BR>
<UL> <UL>
<LI><A href="help/topics/Navigation/Navigation.htm#Go_To_Address_Label">Go to Address or <LI><A href="help/topics/Navigation/Navigation.htm#Go_To_Address_Label">Go to Address or

View file

@ -15,6 +15,10 @@
*/ */
package ghidra.app.merge; package ghidra.app.merge;
import java.net.URL;
import javax.swing.JComponent;
import ghidra.app.CorePluginPackage; import ghidra.app.CorePluginPackage;
import ghidra.app.events.ProgramActivatedPluginEvent; import ghidra.app.events.ProgramActivatedPluginEvent;
import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.PluginCategoryNames;
@ -25,11 +29,6 @@ import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.address.Address; import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program; import ghidra.program.model.listing.Program;
import java.awt.Component;
import java.net.URL;
import javax.swing.JComponent;
/** /**
* Plugin that provides a merge component provider. * Plugin that provides a merge component provider.
* *
@ -218,13 +217,18 @@ public class ProgramMergeManagerPlugin extends MergeManagerPlugin implements Pro
return null; return null;
} }
@Override
public Program openCachedProgram(URL ghidraURL, Object consumer) {
return null;
}
@Override @Override
public Program openProgram(DomainFile domainFile) { public Program openProgram(DomainFile domainFile) {
return null; return null;
} }
@Override @Override
public Program openProgram(DomainFile domainFile, Component dialogParent) { public Program openCachedProgram(DomainFile domainFile, Object consumer) {
return null; return null;
} }
@ -240,38 +244,42 @@ public class ProgramMergeManagerPlugin extends MergeManagerPlugin implements Pro
@Override @Override
public void openProgram(Program program) { public void openProgram(Program program) {
} // not supported
@Override
public void openProgram(Program program, boolean current) {
} }
@Override @Override
public void openProgram(Program program, int state) { public void openProgram(Program program, int state) {
// not supported
} }
@Override @Override
public void releaseProgram(Program program, Object persistentOwner) { public void releaseProgram(Program program, Object persistentOwner) {
// not supported
} }
@Override @Override
public void saveProgram() { public void saveProgram() {
// not supported
} }
@Override @Override
public void saveProgram(Program program) { public void saveProgram(Program program) {
// not supported
} }
@Override @Override
public void saveProgramAs() { public void saveProgramAs() {
// not supported
} }
@Override @Override
public void saveProgramAs(Program program) { public void saveProgramAs(Program program) {
// not supported
} }
@Override @Override
public void setCurrentProgram(Program p) { public void setCurrentProgram(Program p) {
// not supported
} }
@Override @Override
@ -280,14 +288,7 @@ public class ProgramMergeManagerPlugin extends MergeManagerPlugin implements Pro
} }
public void setSearchPriority(Program p, int priority) { public void setSearchPriority(Program p, int priority) {
// not supported
} }
@Override
public boolean isLocked() {
return false;
}
@Override
public void lockDown(boolean state) {
}
} }

View file

@ -35,6 +35,7 @@ import ghidra.framework.plugintool.Plugin;
import ghidra.program.model.listing.Function; import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program; import ghidra.program.model.listing.Program;
import ghidra.util.HelpLocation; import ghidra.util.HelpLocation;
import utility.function.Callback;
/** /**
* Dockable provider that displays function comparisons Clients create/modify * Dockable provider that displays function comparisons Clients create/modify
@ -42,7 +43,7 @@ import ghidra.util.HelpLocation;
* creates instances of this provider as-needed. * creates instances of this provider as-needed.
*/ */
public class FunctionComparisonProvider extends ComponentProviderAdapter public class FunctionComparisonProvider extends ComponentProviderAdapter
implements PopupActionProvider, FunctionComparisonModelListener { implements PopupActionProvider, FunctionComparisonModelListener {
protected static final String HELP_TOPIC = "FunctionComparison"; protected static final String HELP_TOPIC = "FunctionComparison";
protected FunctionComparisonPanel functionComparisonPanel; protected FunctionComparisonPanel functionComparisonPanel;
@ -50,6 +51,7 @@ public class FunctionComparisonProvider extends ComponentProviderAdapter
/** Contains all the comparison data to be displayed by this provider */ /** Contains all the comparison data to be displayed by this provider */
protected FunctionComparisonModel model; protected FunctionComparisonModel model;
private Callback closeListener = Callback.dummy();
/** /**
* Constructor * Constructor
@ -73,9 +75,10 @@ public class FunctionComparisonProvider extends ComponentProviderAdapter
* @param contextType the type of context supported by this provider; may be null * @param contextType the type of context supported by this provider; may be null
*/ */
public FunctionComparisonProvider(Plugin plugin, String name, String owner, public FunctionComparisonProvider(Plugin plugin, String name, String owner,
Class<?> contextType) { Class<?> contextType) {
super(plugin.getTool(), name, owner, contextType); super(plugin.getTool(), name, owner, contextType);
this.plugin = plugin; this.plugin = plugin;
setTransient();
model = new FunctionComparisonModel(); model = new FunctionComparisonModel();
model.addFunctionComparisonModelListener(this); model.addFunctionComparisonModelListener(this);
functionComparisonPanel = getComponent(); functionComparisonPanel = getComponent();
@ -90,6 +93,12 @@ public class FunctionComparisonProvider extends ComponentProviderAdapter
return functionComparisonPanel; return functionComparisonPanel;
} }
@Override
public void closeComponent() {
super.closeComponent();
closeListener.call();
}
@Override @Override
public String toString() { public String toString() {
StringBuffer buff = new StringBuffer(); StringBuffer buff = new StringBuffer();
@ -229,7 +238,6 @@ public class FunctionComparisonProvider extends ComponentProviderAdapter
* Perform initialization for this provider and its panel * Perform initialization for this provider and its panel
*/ */
protected void initFunctionComparisonPanel() { protected void initFunctionComparisonPanel() {
setTransient();
setTabText(functionComparisonPanel.getDescription()); setTabText(functionComparisonPanel.getDescription());
addSpecificCodeComparisonActions(); addSpecificCodeComparisonActions();
tool.addPopupActionProvider(this); tool.addPopupActionProvider(this);
@ -266,7 +274,11 @@ public class FunctionComparisonProvider extends ComponentProviderAdapter
} }
public void removeAddFunctionsAction() { public void removeAddFunctionsAction() {
//TODO this is stupid merge multi and this into one //TODO merge multi and this into one
} }
public void setCloseListener(Callback closeListener) {
this.closeListener = Callback.dummyIfNull(closeListener);
}
} }

View file

@ -15,12 +15,9 @@
*/ */
package ghidra.app.plugin.core.progmgr; package ghidra.app.plugin.core.progmgr;
import java.net.URL;
import java.rmi.NoSuchObjectException; import java.rmi.NoSuchObjectException;
import java.util.Comparator; import java.util.*;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -29,18 +26,21 @@ import org.jdom.Element;
import ghidra.app.events.*; import ghidra.app.events.*;
import ghidra.app.nav.Navigatable; import ghidra.app.nav.Navigatable;
import ghidra.app.services.*; import ghidra.app.services.*;
import ghidra.app.util.task.OpenProgramRequest;
import ghidra.app.util.task.OpenProgramTask; import ghidra.app.util.task.OpenProgramTask;
import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest;
import ghidra.framework.data.DomainObjectAdapterDB; import ghidra.framework.data.DomainObjectAdapterDB;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.TransientToolState; import ghidra.framework.plugintool.util.TransientToolState;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.program.model.address.Address; import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program; import ghidra.program.model.listing.Program;
import ghidra.util.*; import ghidra.util.Msg;
import ghidra.util.Swing;
import ghidra.util.task.TaskLauncher; import ghidra.util.task.TaskLauncher;
/**
* Class for tracking open programs in the tool.
*/
class MultiProgramManager implements DomainObjectListener, TransactionListener { class MultiProgramManager implements DomainObjectListener, TransactionListener {
private ProgramManagerPlugin plugin; private ProgramManagerPlugin plugin;
@ -53,14 +53,13 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
private boolean hasUnsavedPrograms; private boolean hasUnsavedPrograms;
private String pluginName; private String pluginName;
// These data structures are accessed from multiple threads. Rather than synchronizing all // This data structure is accessed from multiple threads. Rather than synchronizing all
// accesses, we have chosen to be weakly consistent. We assume that any out-of-date checks // accesses, we have chosen to be weakly consistent. We assume that any out-of-date checks
// for open program state will be self-correcting. For example, if a client checks to see if // for open program state will be self-correcting. For example, if a client checks to see if
// a program is open before opening it, then a repeated call to open the program will not // a program is open before opening it, then a repeated call to open the program will not
// result in a second copy of that program being opened. This is safe because program opens // result in a second copy of that program being opened. This is safe because program opens
// and closes are all done from the Swing thread. // and closes are all done from the Swing thread.
private List<ProgramInfo> openPrograms = new CopyOnWriteArrayList<>(); private Map<Program, ProgramInfo> programMap = new ConcurrentHashMap<>();
private ConcurrentHashMap<Program, ProgramInfo> programMap = new ConcurrentHashMap<>();
MultiProgramManager(ProgramManagerPlugin programManagerPlugin) { MultiProgramManager(ProgramManagerPlugin programManagerPlugin) {
this.plugin = programManagerPlugin; this.plugin = programManagerPlugin;
@ -82,12 +81,8 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
}; };
} }
void addProgram(Program p, DomainFile domainFile, int state) { void addProgram(Program p, ProgramLocator locator, int state) {
addProgram(new ProgramInfo(p, domainFile, state != ProgramManager.OPEN_HIDDEN), state); addProgram(new ProgramInfo(p, locator, state != ProgramManager.OPEN_HIDDEN), state);
}
void addProgram(Program p, URL ghidraUrl, int state) {
addProgram(new ProgramInfo(p, ghidraUrl, state != ProgramManager.OPEN_HIDDEN), state);
} }
private void addProgram(ProgramInfo programInfo, int state) { private void addProgram(ProgramInfo programInfo, int state) {
@ -96,8 +91,6 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
if (oldInfo == null) { if (oldInfo == null) {
oldInfo = programInfo; oldInfo = programInfo;
p.addConsumer(tool); p.addConsumer(tool);
openPrograms.add(oldInfo);
openPrograms.sort(Comparator.naturalOrder());
programMap.put(p, oldInfo); programMap.put(p, oldInfo);
fireOpenEvents(p); fireOpenEvents(p);
@ -125,7 +118,6 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
p.release(tool); p.release(tool);
} }
programMap.clear(); programMap.clear();
openPrograms.clear();
tool.setSubTitle(""); tool.setSubTitle("");
tool.removeStatusComponent(txMonitor); tool.removeStatusComponent(txMonitor);
tool = null; tool = null;
@ -150,21 +142,20 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
p.removeTransactionListener(this); p.removeTransactionListener(this);
programMap.remove(p); programMap.remove(p);
p.removeListener(this); p.removeListener(this);
openPrograms.remove(info);
if (info == currentInfo) { if (info == currentInfo) {
ProgramInfo newCurrent = findNextCurrent(); ProgramInfo newCurrent = findNextCurrent();
setCurrentProgram(newCurrent); setCurrentProgram(newCurrent);
} }
fireCloseEvents(p); fireCloseEvents(p);
p.release(tool); p.release(tool);
if (openPrograms.isEmpty()) { if (programMap.isEmpty()) {
plugin.getTool().clearLastEvents(); plugin.getTool().clearLastEvents();
} }
} }
} }
private ProgramInfo findNextCurrent() { private ProgramInfo findNextCurrent() {
for (ProgramInfo pi : openPrograms) { for (ProgramInfo pi : getSortedProgramInfos()) {
if (pi.visible) { if (pi.visible) {
return pi; return pi;
} }
@ -172,19 +163,15 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
return null; return null;
} }
Program[] getOtherPrograms() { List<Program> getOtherPrograms() {
Program currentProgram = getCurrentProgram(); List<Program> otherPrograms = new ArrayList<>(programMap.keySet());
List<Program> list = openPrograms.stream() otherPrograms.remove(getCurrentProgram());
.map(info -> info.program) return otherPrograms;
.filter(program -> program != currentProgram)
.collect(Collectors.toList());
return list.toArray(new Program[list.size()]);
} }
Program[] getAllPrograms() { List<Program> getAllPrograms() {
List<Program> list = List<ProgramInfo> sorted = getSortedProgramInfos();
openPrograms.stream().map(info -> info.program).collect(Collectors.toList()); return sorted.stream().map(info -> info.program).collect(Collectors.toList());
return list.toArray(Program[]::new);
} }
Program getCurrentProgram() { Program getCurrentProgram() {
@ -212,7 +199,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
} }
Program getProgram(Address addr) { Program getProgram(Address addr) {
for (ProgramInfo pi : openPrograms) { for (ProgramInfo pi : getSortedProgramInfos()) {
if (pi.program.getMemory().contains(addr)) { if (pi.program.getMemory().contains(addr)) {
return pi.program; return pi.program;
} }
@ -236,6 +223,13 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
historyService.addNewLocation(defaultNavigatable); historyService.addNewLocation(defaultNavigatable);
} }
private List<ProgramInfo> getSortedProgramInfos() {
List<ProgramInfo> list = new ArrayList<>(programMap.values());
Collections.sort(list);
return list;
}
private void setCurrentProgram(ProgramInfo info) { private void setCurrentProgram(ProgramInfo info) {
if (currentInfo == info) { if (currentInfo == info) {
return; return;
@ -340,7 +334,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
} }
public boolean isEmpty() { public boolean isEmpty() {
return openPrograms.isEmpty(); return programMap.isEmpty();
} }
public boolean contains(Program p) { public boolean contains(Program p) {
@ -392,44 +386,15 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
return programMap.get(p); return programMap.get(p);
} }
Program getOpenProgram(URL ghidraURL) { Program getOpenProgram(ProgramLocator programLocator) {
URL normalizedURL = GhidraURL.getNormalizedURL(ghidraURL);
for (ProgramInfo info : programMap.values()) { for (ProgramInfo info : programMap.values()) {
URL url = info.ghidraURL; if (info.getProgramLocator().equals(programLocator)) {
if (url != null && url.equals(normalizedURL)) {
return info.program; return info.program;
} }
} }
return null; 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. * 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. * @return true if there is at least one program that has unsaved changes.
@ -445,7 +410,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
return true; return true;
} }
// look at all the open programs to see if any have changes // 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()) { if (programInfo.program.isChanged()) {
return true; return true;
} }
@ -497,7 +462,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
// recovering after closing the program during this swap // recovering after closing the program during this swap
dataState = tool.saveDataStateToXml(true); dataState = tool.saveDataStateToXml(true);
} }
OpenProgramTask openTask = new OpenProgramTask(file, -1, this); OpenProgramTask openTask = new OpenProgramTask(file, DomainFile.DEFAULT_VERSION, this);
openTask.setSilent(); openTask.setSilent();
new TaskLauncher(openTask, tool.getToolFrame()); new TaskLauncher(openTask, tool.getToolFrame());
OpenProgramRequest openProgramReq = openTask.getOpenProgram(); OpenProgramRequest openProgramReq = openTask.getOpenProgram();
@ -519,66 +484,31 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
private static final AtomicInteger nextAvailableId = new AtomicInteger(); private static final AtomicInteger nextAvailableId = new AtomicInteger();
public final Program program; public final Program program;
public ProgramLocator programLocator;
// 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;
private TransientToolState lastState; private TransientToolState lastState;
private int instance; private int instance;
private boolean visible = false; private boolean visible = false;
private Object owner; 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.program = p;
this.domainFile = domainFile; this.programLocator = programLocator;
if (domainFile instanceof LinkedDomainFile linkedDomainFile) {
this.ghidraURL = linkedDomainFile.getSharedProjectURL(null);
}
else {
this.ghidraURL = null;
}
this.visible = visible; this.visible = visible;
instance = nextAvailableId.incrementAndGet(); instance = nextAvailableId.incrementAndGet();
} }
ProgramInfo(Program p, URL ghidraURL, boolean visible) { ProgramLocator getProgramLocator() {
this.program = p; return programLocator;
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;
} }
void programSavedAs() { void programSavedAs() {
domainFile = program.getDomainFile(); programLocator = new ProgramLocator(program.getDomainFile());
ghidraURL = null; displayName = null;
str = null;
} }
public void setVisible(boolean state) { public void setVisible(boolean state) {
visible = state; visible = state;
fireVisibilityChangeEvent(program, visible); fireVisibilityChangeEvent(program, visible);
@ -591,21 +521,21 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
@Override @Override
public String toString() { public String toString() {
if (str != null) { if (displayName != null) {
return str; return displayName;
} }
StringBuilder buf = new StringBuilder(); StringBuilder buf = new StringBuilder();
DomainFile df = program.getDomainFile(); DomainFile df = program.getDomainFile();
if (domainFile != null && domainFile.isLinkFile()) { buf.append(program.getDomainFile().toString());
buf.append(domainFile.getName());
buf.append("->");
}
buf.append(df.toString());
if (df.isReadOnly()) { if (df.isReadOnly()) {
buf.append(" [Read-Only]"); buf.append(" [Read-Only]");
} }
str = buf.toString(); displayName = buf.toString();
return str; return displayName;
}
public boolean canReopen() {
return programLocator.canReopen();
} }
} }
} }

View file

@ -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.
* <P>
* 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.
* <P>
* 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.
* <P>
* 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<ProgramLocator, Program> {
private Map<Program, ProgramFileListener> 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);
}
}
}

View file

@ -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.
* <P>
* 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;
}
}

View file

@ -15,12 +15,14 @@
*/ */
package ghidra.app.plugin.core.progmgr; package ghidra.app.plugin.core.progmgr;
import java.awt.Component;
import java.beans.PropertyEditor; import java.beans.PropertyEditor;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.time.Duration;
import java.util.List; import java.util.*;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import docking.action.DockingAction; import docking.action.DockingAction;
import docking.action.builder.ActionBuilder; 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.services.ProgramManager;
import ghidra.app.util.HelpTopics; import ghidra.app.util.HelpTopics;
import ghidra.app.util.NamespaceUtils; import ghidra.app.util.NamespaceUtils;
import ghidra.app.util.task.OpenProgramRequest;
import ghidra.app.util.task.OpenProgramTask; import ghidra.app.util.task.OpenProgramTask;
import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest;
import ghidra.framework.client.ClientUtil; import ghidra.framework.client.ClientUtil;
import ghidra.framework.main.OpenVersionedFileDialog; import ghidra.framework.main.OpenVersionedFileDialog;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.options.*; import ghidra.framework.options.*;
import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus; import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.program.model.address.*; import ghidra.program.model.address.*;
import ghidra.program.model.listing.Program; import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.Symbol; import ghidra.program.model.symbol.Symbol;
@ -75,14 +76,19 @@ import ghidra.util.task.TaskLauncher;
ProgramActivatedPluginEvent.class } ProgramActivatedPluginEvent.class }
) )
//@formatter:on //@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"; private static final String SAVE_GROUP = "DomainObjectSave";
static final String OPEN_GROUP = "DomainObjectOpen"; static final String OPEN_GROUP = "DomainObjectOpen";
private MultiProgramManager programMgr; private MultiProgramManager programMgr;
private ProgramCache programCache;
private ProgramSaveManager programSaveMgr; private ProgramSaveManager programSaveMgr;
private int transactionID = -1; private int transactionID = -1;
private boolean locked = false;
private UndoAction undoAction; private UndoAction undoAction;
private RedoAction redoAction; private RedoAction redoAction;
private ProgramLocation currentLocation; private ProgramLocation currentLocation;
@ -92,7 +98,39 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
createActions(); createActions();
programMgr = new MultiProgramManager(this); programMgr = new MultiProgramManager(this);
programCache = new ProgramCache(Duration.ofMinutes(DEFAULT_PROGRAM_CACHE_DURATION),
DEFAULT_PROGRAM_CACHE_CAPACITY);
programSaveMgr = new ProgramSaveManager(tool, this); 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; return false;
} }
if (locked) {
Msg.showError(this, tool.getToolFrame(), "Open Program Failed",
"Program manager is locked and cannot open additional programs");
return false;
}
List<DomainFile> filesToOpen = new ArrayList<>(); List<DomainFile> filesToOpen = new ArrayList<>();
for (DomainFile domainFile : data) { for (DomainFile domainFile : data) {
if (domainFile == null) { if (domainFile == null) {
@ -138,110 +170,9 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
return new Class[] { Program.class }; 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<Symbol> 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 @Override
public Program openProgram(DomainFile df) { public Program openProgram(DomainFile df) {
return openProgram(df, -1, OPEN_CURRENT); return openProgram(df, DomainFile.DEFAULT_VERSION, OPEN_CURRENT);
}
@Override
public Program openProgram(DomainFile df, Component parent) {
return openProgram(df, -1, OPEN_CURRENT);
} }
@Override @Override
@ -251,23 +182,140 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
@Override @Override
public Program openProgram(DomainFile domainFile, int version, int state) { public Program openProgram(DomainFile domainFile, int version, int state) {
return openProgram(new ProgramLocator(domainFile, version), state);
}
if (domainFile == null) { @Override
throw new IllegalArgumentException("Domain file cannot be null"); 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) { return program;
Msg.showError(this, tool.getToolFrame(), "Open Program Failed", }
"Program manager is locked and cannot open additional programs");
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<Symbol> symbols = NamespaceUtils.getSymbols(ref, program);
if (symbols.isEmpty()) {
return null; return null;
} }
Program program = Swing.runNow(() -> { Symbol symbol = symbols.get(0);
return doOpenProgram(domainFile, version, state); if (symbol == null) {
}); return null;
if (program != null) {
Msg.info(this, "Opened program in " + tool.getName() + " tool: " + domainFile);
} }
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; return program;
} }
@ -288,7 +336,7 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
@Override @Override
public Program[] getAllOpenPrograms() { public Program[] getAllOpenPrograms() {
return programMgr.getAllPrograms(); return programMgr.getAllPrograms().toArray(Program[]::new);
} }
@Override @Override
@ -299,7 +347,7 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
@Override @Override
public boolean closeOtherPrograms(boolean ignoreChanges) { public boolean closeOtherPrograms(boolean ignoreChanges) {
Program[] otherPrograms = programMgr.getOtherPrograms(); List<Program> otherPrograms = programMgr.getOtherPrograms();
Runnable r = () -> doCloseAllPrograms(otherPrograms, ignoreChanges); Runnable r = () -> doCloseAllPrograms(otherPrograms, ignoreChanges);
Swing.runNow(r); Swing.runNow(r);
return programMgr.isEmpty(); return programMgr.isEmpty();
@ -307,13 +355,13 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
@Override @Override
public boolean closeAllPrograms(boolean ignoreChanges) { public boolean closeAllPrograms(boolean ignoreChanges) {
Program[] openPrograms = programMgr.getAllPrograms(); List<Program> openPrograms = programMgr.getAllPrograms();
Runnable r = () -> doCloseAllPrograms(openPrograms, ignoreChanges); Runnable r = () -> doCloseAllPrograms(openPrograms, ignoreChanges);
Swing.runNow(r); Swing.runNow(r);
return programMgr.isEmpty(); return programMgr.isEmpty();
} }
private void doCloseAllPrograms(Program[] openPrograms, boolean ignoreChanges) { private void doCloseAllPrograms(List<Program> openPrograms, boolean ignoreChanges) {
List<Program> toRemove = new ArrayList<>(); List<Program> toRemove = new ArrayList<>();
Program currentProgram = programMgr.getCurrentProgram(); Program currentProgram = programMgr.getCurrentProgram();
for (Program p : openPrograms) { for (Program p : openPrograms) {
@ -371,8 +419,8 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
@Override @Override
protected void close() { protected void close() {
Program[] programs = programMgr.getAllPrograms(); List<Program> programs = programMgr.getAllPrograms();
if (programs.length == 0) { if (programs.isEmpty()) {
return; return;
} }
// Don't remove currentProgram until last to prevent activation of other programs. // Don't remove currentProgram until last to prevent activation of other programs.
@ -414,49 +462,21 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
*/ */
@Override @Override
public void openProgram(Program program) { public void openProgram(Program program) {
openProgram(program, true); openProgram(program, OPEN_CURRENT);
}
@Override
public void openProgram(Program program, boolean current) {
openProgram(program, current ? OPEN_CURRENT : OPEN_VISIBLE);
} }
@Override @Override
public void openProgram(final Program program, final int state) { 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()) { if (p == null || p.isClosed()) {
throw new AssertException("Opened program required"); throw new AssertException("Opened program required");
} }
if (locked) {
throw new IllegalStateException(
"Progam manager is locked and cannot accept a new program");
}
Runnable r = () -> { Runnable r = () -> {
programMgr.addProgram(p, ghidraUrl, state); programMgr.addProgram(p, locator, 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);
if (state == ProgramManager.OPEN_CURRENT) { if (state == ProgramManager.OPEN_CURRENT) {
programMgr.saveLocation(); programMgr.saveLocation();
} }
@ -496,7 +516,6 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
new ActionBuilder("Open File", getName()).menuPath(ToolConstants.MENU_FILE, "&Open...") new ActionBuilder("Open File", getName()).menuPath(ToolConstants.MENU_FILE, "&Open...")
.menuGroup(OPEN_GROUP, Integer.toString(subMenuGroup++)) .menuGroup(OPEN_GROUP, Integer.toString(subMenuGroup++))
.keyBinding("ctrl O") .keyBinding("ctrl O")
.enabledWhen(c -> !locked)
.onAction(c -> open()) .onAction(c -> open())
.buildAndInstall(tool); .buildAndInstall(tool);
openAction.addToWindowWhen(ProgramActionContext.class); openAction.addToWindowWhen(ProgramActionContext.class);
@ -617,7 +636,7 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
} }
else { else {
openDialog.close(); 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<DomainFile> filesToOpen) { public void openPrograms(List<DomainFile> filesToOpen) {
Program showIfNeeded = null; List<ProgramLocator> locators =
OpenProgramTask openTask = null; filesToOpen.stream().map(f -> new ProgramLocator(f)).collect(Collectors.toList());
for (DomainFile domainFile : filesToOpen) {
Program p = programMgr.getOpenProgram(domainFile, -1); openProgramLocations(locators);
if (p != null) { }
showIfNeeded = p;
continue; private void openProgramLocations(List<ProgramLocator> locators) {
}
if (openTask == null) { Set<ProgramLocator> toOpen = new LinkedHashSet<>(locators); // preserve order
openTask = new OpenProgramTask(domainFile, -1, this);
} // ensure already opened programs are visible in the tool
else { Map<ProgramLocator, Program> alreadyOpen = getOpenPrograms(toOpen);
openTask.addProgramToOpen(domainFile, -1); makeVisibleInTool(alreadyOpen.values());
} toOpen.removeAll(alreadyOpen.keySet());
// ensure cached programs are in the tool
Map<ProgramLocator, Program> 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()); // Need to open at least one program. Make the first one to open the current program.
List<OpenProgramRequest> openProgramReqs = openTask.getOpenPrograms(); OpenProgramTask task = new OpenProgramTask(new ArrayList<>(toOpen), this);
boolean isFirst = true; new TaskLauncher(task, tool.getToolFrame());
for (OpenProgramRequest programReq : openProgramReqs) {
showProgram(programReq.getProgram(), programReq.getDomainFile(), List<OpenProgramRequest> openProgramReqs = task.getOpenPrograms();
isFirst ? OPEN_CURRENT : OPEN_VISIBLE);
programReq.release(); int openState = OPEN_CURRENT;
isFirst = false; for (OpenProgramRequest programReq : openProgramReqs) {
showIfNeeded = null; showProgram(programReq.getProgram(), programReq.getLocator(), openState);
} programReq.release();
} openState = OPEN_VISIBLE;
if (showIfNeeded != null) {
showProgram(showIfNeeded, showIfNeeded.getDomainFile(), OPEN_CURRENT);
} }
} }
protected Program doOpenProgram(DomainFile domainFile, int version, int openState) { private void makeVisibleInTool(Collection<Program> programs) {
Program p = programMgr.getOpenProgram(domainFile, version); for (Program program : programs) {
if (p != null) { openProgram(program, OPEN_VISIBLE);
openProgram(p, openState);
} }
else { }
OpenProgramTask task = new OpenProgramTask(domainFile, version, this);
new TaskLauncher(task, tool.getToolFrame()); private Map<ProgramLocator, Program> openCachedProgramsInTool(Set<ProgramLocator> toOpen) {
OpenProgramRequest programReq = task.getOpenProgram(); Map<ProgramLocator, Program> map = new HashMap<>();
if (programReq != null) { for (ProgramLocator programLocator : toOpen) {
p = programReq.getProgram(); Program program = programCache.get(programLocator);
showProgram(p, programReq.getDomainFile(), openState); if (program != null) {
programReq.release(); openProgram(program, OPEN_VISIBLE);
map.put(programLocator, program);
} }
} }
return p; return map;
}
private Map<ProgramLocator, Program> getOpenPrograms(Collection<ProgramLocator> locators) {
Map<ProgramLocator, Program> map = new HashMap<>();
for (ProgramLocator locator : locators) {
Program program = programMgr.getOpenProgram(locator);
if (program != null) {
map.put(locator, program);
}
}
return map;
} }
@Override @Override
@ -705,7 +743,7 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
ArrayList<ProgramInfo> programInfos = new ArrayList<>(); ArrayList<ProgramInfo> programInfos = new ArrayList<>();
for (Program p : programMgr.getAllPrograms()) { for (Program p : programMgr.getAllPrograms()) {
ProgramInfo info = programMgr.getInfo(p); ProgramInfo info = programMgr.getInfo(p);
if (info != null) { if (info != null && info.canReopen()) {
programInfos.add(info); programInfos.add(info);
} }
} }
@ -740,8 +778,8 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
loadPrograms(saveState); loadPrograms(saveState);
String currentFile = saveState.getString("CURRENT_FILE", null); String currentFile = saveState.getString("CURRENT_FILE", null);
Program[] programs = programMgr.getAllPrograms(); List<Program> programs = programMgr.getAllPrograms();
if (programs.length != 0) { if (!programs.isEmpty()) {
if (currentFile != null) { if (currentFile != null) {
for (Program program : programs) { for (Program program : programs) {
if (program.getDomainFile().getName().equals(currentFile)) { if (program.getDomainFile().getName().equals(currentFile)) {
@ -752,7 +790,7 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
} }
} }
if (getCurrentProgram() == null) { if (getCurrentProgram() == null) {
programMgr.setCurrentProgram(programs[0]); programMgr.setCurrentProgram(programs.get(0));
} }
} }
contextChanged(); contextChanged();
@ -767,12 +805,8 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
} }
private void writeProgramInfo(ProgramInfo programInfo, SaveState saveState, int index) { private void writeProgramInfo(ProgramInfo programInfo, SaveState saveState, int index) {
if (locked) { if (programInfo.getProgramLocator().isURL()) {
return; // do not save state when locked. URL url = programInfo.getProgramLocator().getURL();
}
URL url = programInfo.getGhidraUrl();
if (url != null) {
saveState.putString("URL_" + index, url.toString()); saveState.putString("URL_" + index, url.toString());
return; return;
} }
@ -805,54 +839,43 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
*/ */
private void loadPrograms(SaveState saveState) { private void loadPrograms(SaveState saveState) {
int n = saveState.getInt("NUM_PROGRAMS", 0); int programCount = saveState.getInt("NUM_PROGRAMS", 0);
if (n == 0) { if (programCount == 0) {
return; return;
} }
OpenProgramTask openTask = new OpenProgramTask(this); List<ProgramLocator> openList = new ArrayList<>();
for (int index = 0; index < n; index++) { for (int index = 0; index < programCount; index++) {
URL url = getGhidraURL(saveState, index); URL url = getGhidraURL(saveState, index);
if (url != null) { if (url != null) {
openTask.addProgramToOpen(url); openList.add(new ProgramLocator(url));
continue; continue;
} }
DomainFile domainFile = getDomainFile(saveState, index); DomainFile domainFile = getDomainFile(saveState, index);
if (domainFile == null) { if (domainFile != null) {
continue; int version = getVersion(saveState, index);
openList.add(new ProgramLocator(domainFile, version));
} }
int version = getVersion(saveState, index);
openTask.addProgramToOpen(domainFile, version);
} }
if (openList.isEmpty()) {
if (!openTask.hasOpenProgramRequests()) {
return; return;
} }
OpenProgramTask task = new OpenProgramTask(openList, this);
// Restore state should not ask about checking out since // Restore state should not ask about checking out since
// hopefully it is in the same state it was in when project // hopefully it is in the same state it was in when project
// was closed and state was saved. // was closed and state was saved.
openTask.setNoCheckout(); task.setNoCheckout();
try { new TaskLauncher(task, tool.getToolFrame(), 100);
new TaskLauncher(openTask, tool.getToolFrame(), 100);
}
catch (RuntimeException e) {
Msg.showError(this, tool.getToolFrame(), "Error Getting Domain File",
"Can't open program", e);
}
List<OpenProgramRequest> openProgramReqs = openTask.getOpenPrograms(); List<OpenProgramRequest> openProgramReqs = task.getOpenPrograms();
for (OpenProgramRequest programReq : openProgramReqs) { for (OpenProgramRequest programReq : openProgramReqs) {
DomainFile df = programReq.getDomainFile(); ProgramLocator locator = programReq.getLocator();
if (df != null) { showProgram(programReq.getProgram(), locator, OPEN_VISIBLE);
showProgram(programReq.getProgram(), df, OPEN_VISIBLE);
}
else {
showProgram(programReq.getProgram(), programReq.getGhidraURL(), OPEN_VISIBLE);
}
programReq.release(); programReq.release();
} }
} }
@ -1056,17 +1079,6 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
return programMgr.setPersistentOwner(program, owner); 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) { public boolean isManaged(Program program) {
return programMgr.contains(program); return programMgr.contains(program);
} }

View file

@ -15,7 +15,6 @@
*/ */
package ghidra.app.services; package ghidra.app.services;
import java.awt.Component;
import java.net.URL; import java.net.URL;
import ghidra.app.plugin.core.progmgr.ProgramManagerPlugin; 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 * Service for managing programs. Multiple programs may be open in a tool, but only one is active at
* any given time. * 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 { public interface ProgramManager {
/** /**
@ -78,8 +79,7 @@ public interface ProgramManager {
* @param ghidraURL valid server-based program URL * @param ghidraURL valid server-based program URL
* @param state initial open state (OPEN_HIDDEN, OPEN_CURRENT, OPEN_VISIBLE). The visibility * @param state initial open state (OPEN_HIDDEN, OPEN_CURRENT, OPEN_VISIBLE). The visibility
* states will be ignored if the program is already open. * 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 * @return the opened program or null if the user canceled the "open" or an error occurred
* displayed.
* @see GhidraURL * @see GhidraURL
*/ */
public Program openProgram(URL ghidraURL, int state); 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. * Open the program for the given domainFile. Once open it will become the active program.
* *
* @param domainFile domain file that has the 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); public Program openProgram(DomainFile domainFile);
/** /**
* Open the program for the given domainFile. Once open it will become the active program. * 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
* <P> * the consumer. In addition, opening or accessing a cached program, will guarantee that it will
* Note: this method functions exactly as {@link #openProgram(DomainFile)} * 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
* @param domainFile domain file that has the program * released from the cache. If the program is still in use when the timer expires, the
* @param dialogParent unused * program will remain in the cache with a new full expiration time. Calling this method
* @return the program * does not open the program in the tool.
* @deprecated deprecated for 10.1; removal for 10.3 or later; use *
* {@link #openProgram(DomainFile)} * @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 openCachedProgram(DomainFile domainFile, Object consumer);
public Program openProgram(DomainFile domainFile, Component dialogParent);
/**
* 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 * 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 df the DomainFile to open
* @param version the version of the Program 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); public Program openProgram(DomainFile df, int version);
@ -125,8 +144,7 @@ public interface ProgramManager {
* file update mode. * file update mode.
* @param state initial open state (OPEN_HIDDEN, OPEN_CURRENT, OPEN_VISIBLE). The visibility * @param state initial open state (OPEN_HIDDEN, OPEN_CURRENT, OPEN_VISIBLE). The visibility
* states will be ignored if the program is already open. * 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 * @return the opened program or null if the user canceled the "open" or an error occurred
* displayed.
*/ */
public Program openProgram(DomainFile domainFile, int version, int state); public Program openProgram(DomainFile domainFile, int version, int state);
@ -138,18 +156,6 @@ public interface ProgramManager {
*/ */
public void openProgram(Program program); 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. * Open the specified program in the tool.
* *
@ -275,22 +281,4 @@ public interface ProgramManager {
*/ */
public Program[] getAllOpenPrograms(); 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();
} }

View file

@ -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;
}
}

View file

@ -15,91 +15,83 @@
*/ */
package ghidra.app.util.task; package ghidra.app.util.task;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.*; import java.util.*;
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.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.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.Task;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
/**
* Task for opening one or more programs.
*/
public class OpenProgramTask extends Task { public class OpenProgramTask extends Task {
private List<ProgramLocator> programsToOpen = new ArrayList<>();
private final List<OpenProgramRequest> openProgramRequests = new ArrayList<>(); private List<OpenProgramRequest> openedPrograms = new ArrayList<>();
private List<OpenProgramRequest> openedProgramList = new ArrayList<>(); private ProgramOpener programOpener;
private final Object consumer; 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"; /**
* Construct a task for opening one or more programs.
public OpenProgramTask(Object consumer) { * @param programLocatorList the list of program locations to open
* @param consumer the consumer to use for opening the programs
*/
public OpenProgramTask(List<ProgramLocator> programLocatorList, Object consumer) {
super("Open Program(s)", true, false, true); super("Open Program(s)", true, false, true);
this.consumer = consumer; this.consumer = consumer;
programOpener = new ProgramOpener(consumer);
programsToOpen.addAll(programLocatorList);
} }
public OpenProgramTask(DomainFile domainFile, int version, boolean forceReadOnly, /**
Object consumer) { * Construct a task for opening a program.
super("Open Program(s)", true, false, true); * @param locator the program location to open
this.consumer = consumer; * @param consumer the consumer to use for opening the programs
openProgramRequests.add(new OpenProgramRequest(domainFile, version, forceReadOnly)); */
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) { public OpenProgramTask(DomainFile domainFile, int version, Object consumer) {
this(domainFile, version, false, consumer); this(new ProgramLocator(domainFile, version), consumer);
}
public OpenProgramTask(DomainFile domainFile, boolean forceReadOnly, Object consumer) {
this(domainFile, DomainFile.DEFAULT_VERSION, forceReadOnly, 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) { 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) { public OpenProgramTask(URL ghidraURL, Object consumer) {
super("Open Program(s)", true, false, true); this(new ProgramLocator(ghidraURL), consumer);
this.consumer = consumer;
openProgramRequests.add(new OpenProgramRequest(ghidraURL));
} }
/**
* 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) { public void setOpenPromptText(String text) {
openPromptText = text; programOpener.setPromptText(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();
} }
/** /**
@ -109,7 +101,7 @@ public class OpenProgramTask extends Task {
* may still be displayed if they occur. * may still be displayed if they occur.
*/ */
public void setSilent() { public void setSilent() {
this.silent = true; programOpener.setSilent();
} }
/** /**
@ -118,7 +110,7 @@ public class OpenProgramTask extends Task {
* user. * user.
*/ */
public void setNoCheckout() { public void setNoCheckout() {
this.noCheckout = true; programOpener.setNoCheckout();
} }
/** /**
@ -126,7 +118,7 @@ public class OpenProgramTask extends Task {
* @return all successful open program requests * @return all successful open program requests
*/ */
public List<OpenProgramRequest> getOpenPrograms() { public List<OpenProgramRequest> 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 * @return first successful open program request or null if none
*/ */
public OpenProgramRequest getOpenProgram() { public OpenProgramRequest getOpenProgram() {
if (openedProgramList.isEmpty()) { if (openedPrograms.isEmpty()) {
return null; return null;
} }
return openedProgramList.get(0); return openedPrograms.get(0);
} }
@Override @Override
public void run(TaskMonitor monitor) { public void run(TaskMonitor monitor) {
taskMonitor.initialize(openProgramRequests.size()); taskMonitor.initialize(programsToOpen.size());
for (OpenProgramRequest domainFileInfo : openProgramRequests) { for (ProgramLocator locator : programsToOpen) {
if (taskMonitor.isCancelled()) { if (taskMonitor.isCancelled()) {
return; return;
} }
domainFileInfo.open(); Program program = programOpener.openProgram(locator, monitor);
if (program != null) {
openedPrograms.add(new OpenProgramRequest(program, locator, consumer));
}
taskMonitor.incrementProgress(1); 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",
"<html>" + HTMLUtilities.escapeHTML(filename) + " has crash data.<br>" +
"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);
}
}
} }

View file

@ -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",
"<html>" + HTMLUtilities.escapeHTML(filename) + " has crash data.<br>" +
"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);
}
}

View file

@ -882,7 +882,7 @@ public class TestEnv {
AbstractGuiTest.runSwing(() -> { AbstractGuiTest.runSwing(() -> {
PluginTool newTool = doLaunchTool(toolName); PluginTool newTool = doLaunchTool(toolName);
ref.set(newTool); ref.set(newTool);
if (newTool != null) { if (newTool != null && domainFile != null) {
newTool.acceptDomainFiles(new DomainFile[] { domainFile }); newTool.acceptDomainFiles(new DomainFile[] { domainFile });
} }
}); });
@ -1007,9 +1007,8 @@ public class TestEnv {
private void cleanupAutoAnalysisManagers(PluginTool t) { private void cleanupAutoAnalysisManagers(PluginTool t) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Map<Program, AutoAnalysisManager> map = Map<Program, AutoAnalysisManager> map = (Map<Program, AutoAnalysisManager>) TestUtils
(Map<Program, AutoAnalysisManager>) TestUtils.getInstanceField("managerMap", .getInstanceField("managerMap", AutoAnalysisManager.class);
AutoAnalysisManager.class);
Collection<AutoAnalysisManager> managers = map.values(); Collection<AutoAnalysisManager> managers = map.values();
for (AutoAnalysisManager manager : managers) { for (AutoAnalysisManager manager : managers) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -1103,8 +1102,8 @@ public class TestEnv {
} }
private void deleteTestProject(String projectName) { private void deleteTestProject(String projectName) {
boolean deletedProject = AbstractGhidraHeadlessIntegrationTest.deleteProject( boolean deletedProject = AbstractGhidraHeadlessIntegrationTest
AbstractGTest.getTestDirectoryPath(), projectName); .deleteProject(AbstractGTest.getTestDirectoryPath(), projectName);
if (!deletedProject) { if (!deletedProject) {
Msg.error(TestEnv.class, "dispose() - Open programs after disposing project: "); 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 // Note: background tool tasks are disposed by the tool
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Map<Task, TaskMonitor> tasks = Map<Task, TaskMonitor> tasks = (Map<Task, TaskMonitor>) TestUtils
(Map<Task, TaskMonitor>) TestUtils.getInstanceField("runningTasks", .getInstanceField("runningTasks", TaskUtilities.class);
TaskUtilities.class);
for (TaskMonitor tm : tasks.values()) { for (TaskMonitor tm : tasks.values()) {
tm.cancel(); tm.cancel();
} }
@ -1193,9 +1191,8 @@ public class TestEnv {
// the managers will dispose the managers. // the managers will dispose the managers.
// //
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
WeakSet<SwingUpdateManager> s = WeakSet<SwingUpdateManager> s = (WeakSet<SwingUpdateManager>) TestUtils
(WeakSet<SwingUpdateManager>) TestUtils.getInstanceField("instances", .getInstanceField("instances", SwingUpdateManager.class);
SwingUpdateManager.class);
/* Debug for undisposed SwingUpdateManagers /* Debug for undisposed SwingUpdateManagers
Msg.out("complete update manager list: "); Msg.out("complete update manager list: ");

View file

@ -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());
}
}

View file

@ -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));
}
}

View file

@ -25,6 +25,7 @@ import docking.action.DockingActionIf;
import docking.widgets.OptionDialog; import docking.widgets.OptionDialog;
import ghidra.app.context.ProgramActionContext; import ghidra.app.context.ProgramActionContext;
import ghidra.app.plugin.core.progmgr.ProgramManagerPlugin; import ghidra.app.plugin.core.progmgr.ProgramManagerPlugin;
import ghidra.app.services.ProgramManager;
import ghidra.framework.cmd.BackgroundCommand; import ghidra.framework.cmd.BackgroundCommand;
import ghidra.framework.model.DomainObject; import ghidra.framework.model.DomainObject;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
@ -76,9 +77,9 @@ public class CloseToolTest extends AbstractGhidraHeadedIntegrationTest {
ProgramDB program2 = ProgramDB program2 =
new ProgramBuilder("WinHelloCPP.exe", ProgramBuilder._TOY).getProgram(); new ProgramBuilder("WinHelloCPP.exe", ProgramBuilder._TOY).getProgram();
ProgramDB program3 = new ProgramBuilder("DiffTestPgm1", ProgramBuilder._TOY).getProgram(); ProgramDB program3 = new ProgramBuilder("DiffTestPgm1", ProgramBuilder._TOY).getProgram();
pm.openProgram(program1, true); pm.openProgram(program1, ProgramManager.OPEN_CURRENT);
pm.openProgram(program2, true); pm.openProgram(program2, ProgramManager.OPEN_CURRENT);
pm.openProgram(program3, true); pm.openProgram(program3, ProgramManager.OPEN_CURRENT);
Program[] allOpenPrograms = pm.getAllOpenPrograms(); Program[] allOpenPrograms = pm.getAllOpenPrograms();
assertEquals(3, allOpenPrograms.length); assertEquals(3, allOpenPrograms.length);

View file

@ -15,7 +15,6 @@
*/ */
package ghidra.app.services; package ghidra.app.services;
import java.awt.Component;
import java.net.URL; import java.net.URL;
import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFile;
@ -52,6 +51,12 @@ public class TestDummyProgramManager implements ProgramManager {
return null; return null;
} }
@Override
public Program openCachedProgram(URL ghidraURL, Object consumer) {
// stub
return null;
}
@Override @Override
public Program openProgram(DomainFile domainFile) { public Program openProgram(DomainFile domainFile) {
// stub // stub
@ -59,7 +64,7 @@ public class TestDummyProgramManager implements ProgramManager {
} }
@Override @Override
public Program openProgram(DomainFile domainFile, Component dialogParent) { public Program openCachedProgram(DomainFile domainFile, Object consumer) {
// stub // stub
return null; return null;
} }
@ -81,11 +86,6 @@ public class TestDummyProgramManager implements ProgramManager {
// stub // stub
} }
@Override
public void openProgram(Program program, boolean current) {
// stub
}
@Override @Override
public void openProgram(Program program, int state) { public void openProgram(Program program, int state) {
// stub // stub
@ -156,16 +156,4 @@ public class TestDummyProgramManager implements ProgramManager {
// stub // stub
return null; return null;
} }
@Override
public void lockDown(boolean state) {
// stub
}
@Override
public boolean isLocked() {
// stub
return false;
}
} }

View file

@ -15,7 +15,6 @@
*/ */
package ghidra.app.plugin.core.diff; package ghidra.app.plugin.core.diff;
import java.awt.Component;
import java.net.URL; import java.net.URL;
import ghidra.app.services.ProgramManager; import ghidra.app.services.ProgramManager;
@ -79,18 +78,23 @@ public class DiffProgramManager implements ProgramManager {
return null; return null;
} }
@Override
public Program openCachedProgram(URL ghidraURL, Object consumer) {
return null;
}
@Override @Override
public Program openProgram(DomainFile domainFile) { public Program openProgram(DomainFile domainFile) {
return null; return null;
} }
@Override @Override
public Program openProgram(DomainFile df, int version) { public Program openCachedProgram(DomainFile domainFile, Object consumer) {
return null; return null;
} }
@Override @Override
public Program openProgram(DomainFile domainFile, Component dialogParent) { public Program openProgram(DomainFile df, int version) {
return null; return null;
} }
@ -104,11 +108,6 @@ public class DiffProgramManager implements ProgramManager {
// stub // stub
} }
@Override
public void openProgram(Program program, boolean current) {
// stub
}
@Override @Override
public void openProgram(Program program, int state) { public void openProgram(Program program, int state) {
// stub // stub
@ -148,14 +147,4 @@ public class DiffProgramManager implements ProgramManager {
public boolean setPersistentOwner(Program program, Object owner) { public boolean setPersistentOwner(Program program, Object owner) {
return false; return false;
} }
@Override
public boolean isLocked() {
return false;
}
@Override
public void lockDown(boolean state) {
// Not doing anything
}
} }

View file

@ -20,8 +20,8 @@ import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import db.*; import db.*;
import ghidra.app.util.task.OpenProgramRequest;
import ghidra.app.util.task.OpenProgramTask; 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.ImpliedMatchProgramCorrelator;
import ghidra.feature.vt.api.correlator.program.ManualMatchProgramCorrelator; import ghidra.feature.vt.api.correlator.program.ManualMatchProgramCorrelator;
import ghidra.feature.vt.api.impl.*; import ghidra.feature.vt.api.impl.*;

View file

@ -21,6 +21,7 @@ import java.util.*;
import db.Transaction; import db.Transaction;
import ghidra.feature.vt.api.main.*; import ghidra.feature.vt.api.main.*;
import ghidra.framework.data.DomainObjectFileListener;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.options.Options; import ghidra.framework.options.Options;
import ghidra.framework.store.LockException; import ghidra.framework.store.LockException;
@ -91,6 +92,16 @@ public class EmptyVTSession implements VTSession {
// do nothing // do nothing
} }
@Override
public void addDomainFileListener(DomainObjectFileListener listener) {
// do nothing
}
@Override
public void removeDomainFileListener(DomainObjectFileListener listener) {
// do nothing
}
@Override @Override
public EventQueueID createPrivateEventQueue(DomainObjectListener listener, int maxDelay) { public EventQueueID createPrivateEventQueue(DomainObjectListener listener, int maxDelay) {
return null; return null;

View file

@ -84,7 +84,7 @@ public class VTSubToolManager implements VTControllerListener, OptionsChangeList
destinationTool = createTool(DESTINATION_TOOL_NAME, false); destinationTool = createTool(DESTINATION_TOOL_NAME, false);
} }
ProgramManager service = destinationTool.getService(ProgramManager.class); ProgramManager service = destinationTool.getService(ProgramManager.class);
return service.openProgram(domainFile, parent); return service.openProgram(domainFile);
} }
Program openSourceProgram(DomainFile domainFile, Component parent) { Program openSourceProgram(DomainFile domainFile, Component parent) {
@ -92,7 +92,7 @@ public class VTSubToolManager implements VTControllerListener, OptionsChangeList
sourceTool = createTool(SOURCE_TOOL_NAME, true); sourceTool = createTool(SOURCE_TOOL_NAME, true);
} }
ProgramManager service = sourceTool.getService(ProgramManager.class); ProgramManager service = sourceTool.getService(ProgramManager.class);
return service.openProgram(domainFile, parent); return service.openProgram(domainFile);
} }
void closeSourceProgram(Program source) { void closeSourceProgram(Program source) {

View file

@ -30,8 +30,8 @@ import docking.wizard.*;
import generic.theme.GIcon; import generic.theme.GIcon;
import generic.theme.GThemeDefaults.Ids.Fonts; import generic.theme.GThemeDefaults.Ids.Fonts;
import generic.theme.Gui; import generic.theme.Gui;
import ghidra.app.util.task.OpenProgramRequest;
import ghidra.app.util.task.OpenProgramTask; import ghidra.app.util.task.OpenProgramTask;
import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest;
import ghidra.framework.main.DataTreeDialog; import ghidra.framework.main.DataTreeDialog;
import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder; import ghidra.framework.model.DomainFolder;

View file

@ -80,8 +80,8 @@ public class GTimer {
static class GTimerTask extends TimerTask implements GTimerMonitor { static class GTimerTask extends TimerTask implements GTimerMonitor {
private final Runnable runnable; private final Runnable runnable;
private boolean wasCancelled; private transient boolean wasCancelled;
private boolean wasRun; private transient boolean wasRun;
GTimerTask(Runnable runnable) { GTimerTask(Runnable runnable) {
this.runnable = runnable; this.runnable = runnable;

View file

@ -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.
* <P>
* 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.
* <P>
* 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 <K> the key
* @param <V> the value
*/
public class GTimerCache<K, V> {
// 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<K, CachedValue> 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<K, CachedValue> 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<Entry<K, CachedValue>> it = map.entrySet().iterator();
int n = map.size() - capacity;
for (int i = 0; i < n; i++) {
Entry<K, CachedValue> 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<K, CachedValue> 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.
* <P>
* 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.
* <P>
* 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.
* <P>
* 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<CachedValue> expiredValues = getAndRemoveExpiredValues(eventTime);
purgeOrReinstateExpiredValues(expiredValues);
restartTimer(eventTime);
}
private List<CachedValue> getAndRemoveExpiredValues(long eventTime) {
List<CachedValue> expiredValues = new ArrayList<>();
Iterator<CachedValue> 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<CachedValue> 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;
}
}
}

View file

@ -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<String, Integer> cache;
private Deque<Removed> 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<String, Integer> {
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) {
}
}

View file

@ -18,7 +18,6 @@ package ghidra.framework.data;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener; import javax.swing.event.ChangeListener;
@ -28,6 +27,7 @@ import ghidra.framework.store.FileSystem;
import ghidra.framework.store.LockException; import ghidra.framework.store.LockException;
import ghidra.util.Lock; import ghidra.util.Lock;
import ghidra.util.classfinder.ClassSearcher; import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.datastruct.ListenerSet;
/** /**
* An abstract class that provides default behavior for DomainObject(s), specifically it handles * 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<EventQueueID, DomainObjectChangeSupport> changeSupportMap = protected Map<EventQueueID, DomainObjectChangeSupport> changeSupportMap =
new ConcurrentHashMap<EventQueueID, DomainObjectChangeSupport>(); new ConcurrentHashMap<EventQueueID, DomainObjectChangeSupport>();
private volatile boolean eventsEnabled = true; private volatile boolean eventsEnabled = true;
private Set<DomainObjectClosedListener> closeListeners = new CopyOnWriteArraySet<>();
private ListenerSet<DomainObjectClosedListener> closeListeners =
new ListenerSet<>(DomainObjectClosedListener.class, false);
private ListenerSet<DomainObjectFileListener> fileChangeListeners =
new ListenerSet<>(DomainObjectFileListener.class, false);
private ArrayList<Object> consumers; private ArrayList<Object> consumers;
protected Map<String, String> metadata = new LinkedHashMap<String, String>(); protected Map<String, String> metadata = new LinkedHashMap<String, String>();
@ -185,10 +189,15 @@ public abstract class DomainObjectAdapter implements DomainObject {
if (df == null) { if (df == null) {
throw new IllegalArgumentException("DomainFile must not be null"); throw new IllegalArgumentException("DomainFile must not be null");
} }
if (df == domainFile) {
return;
}
clearDomainObj(); clearDomainObj();
DomainFile oldDf = domainFile; DomainFile oldDf = domainFile;
domainFile = df; domainFile = df;
fireEvent(new DomainObjectChangeRecord(DO_DOMAIN_FILE_CHANGED, oldDf, df)); fireEvent(new DomainObjectChangeRecord(DO_DOMAIN_FILE_CHANGED, oldDf, df));
fileChangeListeners.invoke().domainFileChanged(this);
} }
protected void close() { protected void close() {
@ -204,13 +213,7 @@ public abstract class DomainObjectAdapter implements DomainObject {
queue.dispose(); queue.dispose();
} }
notifyCloseListeners(); closeListeners.invoke().domainObjectClosed(this);
}
private void notifyCloseListeners() {
for (DomainObjectClosedListener listener : closeListeners) {
listener.domainObjectClosed(this);
}
closeListeners.clear(); closeListeners.clear();
} }
@ -251,6 +254,16 @@ public abstract class DomainObjectAdapter implements DomainObject {
closeListeners.remove(listener); closeListeners.remove(listener);
} }
@Override
public void addDomainFileListener(DomainObjectFileListener listener) {
fileChangeListeners.add(listener);
}
@Override
public void removeDomainFileListener(DomainObjectFileListener listener) {
fileChangeListeners.remove(listener);
}
@Override @Override
public EventQueueID createPrivateEventQueue(DomainObjectListener listener, int maxDelay) { public EventQueueID createPrivateEventQueue(DomainObjectListener listener, int maxDelay) {
EventQueueID eventQueueID = new EventQueueID(); EventQueueID eventQueueID = new EventQueueID();

View file

@ -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);
}

View file

@ -20,6 +20,7 @@ import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import ghidra.framework.data.DomainObjectFileListener;
import ghidra.framework.options.Options; import ghidra.framework.options.Options;
import ghidra.util.ReadOnlyException; import ghidra.util.ReadOnlyException;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
@ -165,6 +166,22 @@ public interface DomainObject {
*/ */
public void removeCloseListener(DomainObjectClosedListener listener); 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. * 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. * @param listener the listener to be notified of domain object events.

View file

@ -74,6 +74,15 @@ public class GhidraURL {
return str != null && str.startsWith(PROTOCOL_URL_START); 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...}). * Determine if URL string uses a local format (e.g., {@code ghidra:/path...}).
* Extensive validation is not performed. This method is intended to differentiate * Extensive validation is not performed. This method is intended to differentiate

View file

@ -20,6 +20,7 @@ import java.io.IOException;
import java.util.*; import java.util.*;
import db.Transaction; import db.Transaction;
import ghidra.framework.data.DomainObjectFileListener;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.options.Options; import ghidra.framework.options.Options;
import ghidra.framework.store.LockException; import ghidra.framework.store.LockException;
@ -149,6 +150,16 @@ public class StubProgram implements Program {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public void addDomainFileListener(DomainObjectFileListener listener) {
throw new UnsupportedOperationException();
}
@Override
public void removeDomainFileListener(DomainObjectFileListener listener) {
throw new UnsupportedOperationException();
}
@Override @Override
public EventQueueID createPrivateEventQueue(DomainObjectListener listener, int maxDelay) { public EventQueueID createPrivateEventQueue(DomainObjectListener listener, int maxDelay) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();

View file

@ -42,6 +42,16 @@ public class Dummy {
}; };
} }
/**
* Creates a dummy consumer
* @return a dummy consumer
*/
public static <T, U> BiConsumer<T, U> biConsumer() {
return (t, u) -> {
// no-op
};
}
/** /**
* Creates a dummy function * Creates a dummy function
* @param <T> the input type * @param <T> the input type
@ -82,6 +92,17 @@ public class Dummy {
return c == null ? consumer() : c; 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 <T, U> BiConsumer<T, U> ifNull(BiConsumer<T, U> c) {
return c == null ? biConsumer() : c;
}
/** /**
* Returns the given callback object if it is not {@code null}. Otherwise, a {@link #callback()} * 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}. * is returned. This is useful to avoid using {@code null}.