mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-03 17:59:46 +02:00
GP-3970 program caching and refactoring of ProgramManager and OpenProgramTask
This commit is contained in:
parent
5d487a6518
commit
7d67188d0b
34 changed files with 2198 additions and 948 deletions
|
@ -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());
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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: ");
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.*;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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}.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue