From c3386b72a28cd986a805ae865bb32ee40f1f1b64 Mon Sep 17 00:00:00 2001 From: ghidra1 Date: Thu, 7 Dec 2023 13:31:56 -0500 Subject: [PATCH] GP-4085 Added ability to add VTSession to a shared repository --- .../app/util/headless/HeadlessAnalyzer.java | 5 +- .../ghidra/app/util/task/ProgramOpener.java | 7 +- .../AutoVersionTrackingScript.java | 110 +++-- ...eateAppliedExactMatchingSessionScript.java | 3 +- .../FindChangedFunctionsScript.java | 4 +- .../OpenVersionTrackingSessionScript.java | 7 +- ... AbstractGhidraVersionTrackingScript.java} | 143 ++++-- .../{impl => db}/VTSessionContentHandler.java | 87 ++-- .../ghidra/feature/vt/api/db/VTSessionDB.java | 447 +++++++++++------- .../vt/api/util/VTSessionFileUtil.java | 186 ++++++++ .../feature/vt/gui/plugin/VTController.java | 2 +- .../vt/gui/plugin/VTControllerImpl.java | 278 +++++++++-- .../feature/vt/gui/plugin/VTPlugin.java | 40 +- .../vt/gui/wizard/CreateNewSessionTask.java | 67 +-- .../vt/gui/wizard/NewSessionPanel.java | 84 ++-- .../gui/wizard/VTNewSessionWizardManager.java | 3 +- .../feature/vt/api/VTAddToSessionTest.java | 45 +- .../feature/vt/api/VTMatchAcceptTest.java | 5 +- .../VTMatchApplyFunctionSignatureTest.java | 16 +- .../feature/vt/api/VTMatchApplyTest.java | 5 +- .../vt/api/AbstractCorrelatorTest.java | 6 +- .../markupitem/AbstractVTMarkupItemTest.java | 2 +- .../ghidra/feature/vt/db/VTBaseTestCase.java | 3 +- .../java/ghidra/feature/vt/gui/VTTestEnv.java | 4 +- .../vt/gui/plugin/StubVTController.java | 2 +- .../DB/src/main/java/db/Transaction.java | 6 +- .../framework/data/DomainFileProxy.java | 2 +- .../framework/data/DomainObjectAdapter.java | 9 +- .../ghidra/framework/data/GhidraFileData.java | 12 +- .../ghidra/framework/main/FrontEndTool.java | 8 +- .../ghidra/framework/model/DomainFile.java | 9 +- .../framework/project/DefaultProject.java | 16 +- .../project/DefaultProjectManager.java | 5 + 33 files changed, 1063 insertions(+), 565 deletions(-) rename Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/{GhidraVersionTrackingScript.java => AbstractGhidraVersionTrackingScript.java} (69%) rename Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/{impl => db}/VTSessionContentHandler.java (71%) create mode 100644 Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/util/VTSessionFileUtil.java diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java index 6edb79d974..33f5fb2f0c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java @@ -424,7 +424,6 @@ public class HeadlessAnalyzer { if (locator.getProjectDir().exists()) { project = openProject(locator); - AppInfo.setActiveProject(project); } else { if (options.runScriptsNoImport) { @@ -441,7 +440,6 @@ public class HeadlessAnalyzer { Msg.info(this, "Creating " + (options.deleteProject ? "temporary " : "") + "project: " + locator); project = getProjectManager().createProject(locator, null, false); - AppInfo.setActiveProject(project); } try { @@ -459,7 +457,6 @@ public class HeadlessAnalyzer { } finally { project.close(); - AppInfo.setActiveProject(null); if (!options.runScriptsNoImport && options.deleteProject) { FileUtilities.deleteDir(locator.getProjectDir()); locator.getMarkerFile().delete(); @@ -1841,11 +1838,13 @@ public class HeadlessAnalyzer { HeadlessProject(HeadlessGhidraProjectManager projectManager, GhidraURLConnection connection) throws IOException { super(projectManager, connection); + AppInfo.setActiveProject(this); } HeadlessProject(HeadlessGhidraProjectManager projectManager, ProjectLocator projectLocator) throws NotOwnerException, LockException, IOException { super(projectManager, projectLocator, false); + AppInfo.setActiveProject(this); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java index 07f99c04c0..37aa09d814 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java @@ -45,7 +45,7 @@ import ghidra.util.task.TaskMonitor; public class ProgramOpener { private final Object consumer; private String openPromptText = "Open"; - private boolean silent = false; // if true operation does not permit interaction + private boolean silent = SystemUtilities.isInHeadlessMode(); // if true operation does not permit interaction private boolean noCheckout = false; // if true operation should not perform optional checkout /** @@ -253,8 +253,9 @@ public class ProgramOpener { 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!"); + 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 diff --git a/Ghidra/Features/VersionTracking/ghidra_scripts/AutoVersionTrackingScript.java b/Ghidra/Features/VersionTracking/ghidra_scripts/AutoVersionTrackingScript.java index 72241ced58..3daa33f5d6 100644 --- a/Ghidra/Features/VersionTracking/ghidra_scripts/AutoVersionTrackingScript.java +++ b/Ghidra/Features/VersionTracking/ghidra_scripts/AutoVersionTrackingScript.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + // A script that runs Auto Version Tracking given the options set in one of the following ways: // 1. If script is run from the CodeBrowser, the GUI options are set in a pop up dialog by user. // 2. If script is run in headless mode either the defaults provided by the script are used or the @@ -129,67 +130,72 @@ public class AutoVersionTrackingScript extends GhidraScript { Program otherProgram = startupValues.getProgram("Please select the other program", this, state.getTool(), autoUpgradeIfNeeded); - if (isCurrentProgramSourceProg) { - sourceProgram = currentProgram; - destinationProgram = otherProgram; - } - else { - destinationProgram = currentProgram; - sourceProgram = otherProgram; - } - - if (sourceProgram == null || destinationProgram == null) { - return; - } - - // Need to end the script transaction or it interferes with vt things that need locks - end(true); - - VTSession session = - VTSessionDB.createVTSession(name, sourceProgram, destinationProgram, this); - - if (folder.getFile(name) == null) { - folder.createFile(name, session, monitor); - } - - // create a default options map in case cannot get user input - GhidraValuesMap optionsMap = createDefaultOptions(); - - // if running script in GUI get options from user and update the vtOptions with them - if (!isRunningHeadless()) { - optionsMap = getOptionsFromUser(); - - } - // else if running script in headless get possible options set by prescript that saves - // optionsMap in script state variable and update the vtOptions with them - else { - // try to get options map from state if running headless - // if user runs prescript to set up their own options map those options will be used - // See SetAutoVersionTrackingOptionsScript.java as an example - GhidraValuesMap stateOptionsMap = - (GhidraValuesMap) state.getEnvironmentVar("autoVTOptionsMap"); - if (optionsMap != null) { - optionsMap = stateOptionsMap; + VTSession session = null; + try { + if (isCurrentProgramSourceProg) { + sourceProgram = currentProgram; + destinationProgram = otherProgram; + } + else { + destinationProgram = currentProgram; + sourceProgram = otherProgram; } - } + if (sourceProgram == null || destinationProgram == null) { + return; + } - ToolOptions vtOptions = setToolOptionsFromOptionsMap(optionsMap); + // Need to end the script transaction or it interferes with vt things that need locks + end(true); - AutoVersionTrackingTask autoVtTask = new AutoVersionTrackingTask(session, vtOptions); + session = new VTSessionDB(name, sourceProgram, destinationProgram, this); - TaskLauncher.launch(autoVtTask); + if (folder.getFile(name) == null) { + folder.createFile(name, session, monitor); + } - // if not running headless user can decide whether to save or not - // if running headless - must save here or nothing that was done in this script will be - // accessible later. - if (isRunningHeadless()) { + // create a default options map in case cannot get user input + GhidraValuesMap optionsMap = createDefaultOptions(); + + // if running script in GUI get options from user and update the vtOptions with them + if (!isRunningHeadless()) { + optionsMap = getOptionsFromUser(); + + } + // else if running script in headless get possible options set by prescript that saves + // optionsMap in script state variable and update the vtOptions with them + else { + // try to get options map from state if running headless + // if user runs prescript to set up their own options map those options will be used + // See SetAutoVersionTrackingOptionsScript.java as an example + GhidraValuesMap stateOptionsMap = + (GhidraValuesMap) state.getEnvironmentVar("autoVTOptionsMap"); + if (optionsMap != null) { + optionsMap = stateOptionsMap; + } + + } + + ToolOptions vtOptions = setToolOptionsFromOptionsMap(optionsMap); + + AutoVersionTrackingTask autoVtTask = new AutoVersionTrackingTask(session, vtOptions); + + TaskLauncher.launch(autoVtTask); + + // Save destination program and session changes otherProgram.save("Updated with Auto Version Tracking", monitor); session.save(); - } - println(autoVtTask.getStatusMsg()); - otherProgram.release(this); + println(autoVtTask.getStatusMsg()); + } + finally { + if (otherProgram != null) { + otherProgram.release(this); + } + if (session != null) { + session.release(this); + } + } } /** diff --git a/Ghidra/Features/VersionTracking/ghidra_scripts/CreateAppliedExactMatchingSessionScript.java b/Ghidra/Features/VersionTracking/ghidra_scripts/CreateAppliedExactMatchingSessionScript.java index bcb3f993a3..0d449685c8 100644 --- a/Ghidra/Features/VersionTracking/ghidra_scripts/CreateAppliedExactMatchingSessionScript.java +++ b/Ghidra/Features/VersionTracking/ghidra_scripts/CreateAppliedExactMatchingSessionScript.java @@ -63,8 +63,7 @@ public class CreateAppliedExactMatchingSessionScript extends GhidraScript { return; } - VTSession session = - VTSessionDB.createVTSession(name, sourceProgram, destinationProgram, this); + VTSession session = new VTSessionDB(name, sourceProgram, destinationProgram, this); // it seems clunky to have to create this separately, but I'm not sure how else to do it folder.createFile(name, session, monitor); diff --git a/Ghidra/Features/VersionTracking/ghidra_scripts/FindChangedFunctionsScript.java b/Ghidra/Features/VersionTracking/ghidra_scripts/FindChangedFunctionsScript.java index 7fe1034a00..d9e1f18c05 100644 --- a/Ghidra/Features/VersionTracking/ghidra_scripts/FindChangedFunctionsScript.java +++ b/Ghidra/Features/VersionTracking/ghidra_scripts/FindChangedFunctionsScript.java @@ -20,14 +20,14 @@ import java.util.Collection; import java.util.Set; -import ghidra.feature.vt.GhidraVersionTrackingScript; +import ghidra.feature.vt.AbstractGhidraVersionTrackingScript; import ghidra.feature.vt.api.main.VTMatch; import ghidra.framework.model.Project; import ghidra.program.model.listing.Function; import ghidra.program.model.listing.Program; import ghidra.util.Msg; -public class FindChangedFunctionsScript extends GhidraVersionTrackingScript { +public class FindChangedFunctionsScript extends AbstractGhidraVersionTrackingScript { private Program p1; private Program p2; diff --git a/Ghidra/Features/VersionTracking/ghidra_scripts/OpenVersionTrackingSessionScript.java b/Ghidra/Features/VersionTracking/ghidra_scripts/OpenVersionTrackingSessionScript.java index 60a58b2a6b..32dfd2adfc 100644 --- a/Ghidra/Features/VersionTracking/ghidra_scripts/OpenVersionTrackingSessionScript.java +++ b/Ghidra/Features/VersionTracking/ghidra_scripts/OpenVersionTrackingSessionScript.java @@ -20,10 +20,10 @@ import java.util.Collection; import java.util.List; -import ghidra.feature.vt.GhidraVersionTrackingScript; +import ghidra.feature.vt.AbstractGhidraVersionTrackingScript; import ghidra.feature.vt.api.main.*; -public class OpenVersionTrackingSessionScript extends GhidraVersionTrackingScript { +public class OpenVersionTrackingSessionScript extends AbstractGhidraVersionTrackingScript { @Override protected void run() throws Exception { @@ -33,6 +33,9 @@ public class OpenVersionTrackingSessionScript extends GhidraVersionTrackingScrip } private void acceptMatchesWithGoodConfidence() throws Exception { + + VTSession vtSession = getVTSession(); + println("Working on session: " + vtSession); List matchSets = vtSession.getMatchSets(); diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/GhidraVersionTrackingScript.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/AbstractGhidraVersionTrackingScript.java similarity index 69% rename from Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/GhidraVersionTrackingScript.java rename to Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/AbstractGhidraVersionTrackingScript.java index 744f6b18f0..5de9da0724 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/GhidraVersionTrackingScript.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/AbstractGhidraVersionTrackingScript.java @@ -25,50 +25,81 @@ import ghidra.feature.vt.api.util.VTOptions; import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFolder; import ghidra.program.model.listing.*; +import ghidra.util.InvalidNameException; import ghidra.util.classfinder.ClassSearcher; import ghidra.util.exception.CancelledException; import ghidra.util.exception.VersionException; -public abstract class GhidraVersionTrackingScript extends GhidraScript { - protected VTSession vtSession; - protected Program sourceProgram; - protected Program destinationProgram; +public abstract class AbstractGhidraVersionTrackingScript extends GhidraScript { + private VTSession vtSession; + private Program sourceProgram; + private Program destinationProgram; private int transactionID; - public void createVersionTrackingSession(String sourceProgramPath, - String destinationProgramPath) throws Exception { + protected VTSession getVTSession() { + return vtSession; + } + + protected Program getSourceProgram() { + return sourceProgram; + } + + protected Program getDestinationProgram() { + return destinationProgram; + } + + public VTSession createVersionTrackingSession(String sourceProgramPath, + String destinationProgramPath) + throws VersionException, CancelledException, IOException { if (vtSession != null) { throw new RuntimeException("Attempted to open a new session with one already open!"); } - sourceProgram = openProgram(sourceProgramPath); - destinationProgram = openProgram(destinationProgramPath); - createVersionTrackingSession("New Session", sourceProgram, destinationProgram); + try { + sourceProgram = openProgram(sourceProgramPath); + + destinationProgram = openProgram(destinationProgramPath); + + vtSession = new VTSessionDB("New Session", sourceProgram, destinationProgram, this); + transactionID = vtSession.startTransaction("VT Script"); + } + finally { + if (vtSession == null) { + closeVersionTrackingSession(); + } + } + return vtSession; } - public void createVersionTrackingSession(String name, Program source, Program destination) - throws Exception { + public VTSession createVersionTrackingSession(String name, Program source, Program destination) + throws IOException { if (vtSession != null) { throw new RuntimeException("Attempted to create a new session with one already open!"); } - sourceProgram = source; - destinationProgram = destination; - if (!sourceProgram.isUsedBy(this)) { + try { + sourceProgram = source; sourceProgram.addConsumer(this); - } - if (!destinationProgram.isUsedBy(this)) { - destinationProgram.addConsumer(this); - } - vtSession = VTSessionDB.createVTSession(name, sourceProgram, destinationProgram, this); - transactionID = vtSession.startTransaction("VT Script"); + destinationProgram = destination; + destinationProgram.addConsumer(this); + + vtSession = new VTSessionDB(name, sourceProgram, destinationProgram, this); + transactionID = vtSession.startTransaction("VT Script"); + } + finally { + if (vtSession == null) { + closeVersionTrackingSession(); + } + } + return vtSession; } - public void openVersionTrackingSession(String path) throws Exception { + public VTSession openVersionTrackingSession(String path) + throws VersionException, CancelledException, IOException { if (vtSession != null) { throw new RuntimeException("Attempted to open a session with one already open!"); } @@ -79,51 +110,72 @@ public abstract class GhidraVersionTrackingScript extends GhidraScript { DomainFile file = state.getProject().getProjectData().getFile(path); vtSession = (VTSessionDB) file.getDomainObject(this, true, true, monitor); sourceProgram = vtSession.getSourceProgram(); + sourceProgram.addConsumer(this); destinationProgram = vtSession.getDestinationProgram(); + destinationProgram.addConsumer(this); - if (!sourceProgram.isUsedBy(this)) { - sourceProgram.addConsumer(this); - } - if (!destinationProgram.isUsedBy(this)) { - destinationProgram.addConsumer(this); - } transactionID = vtSession.startTransaction("VT Script"); + return vtSession; } public void saveVersionTrackingSession() throws IOException { + if (vtSession != null) { + throw new RuntimeException("Attempted to save a session when not open!"); + } vtSession.endTransaction(transactionID, true); - vtSession.save(); - transactionID = vtSession.startTransaction("VT Script"); + try { + vtSession.save(); + } + finally { + transactionID = vtSession.startTransaction("VT Script"); + } } - public void saveSessionAs(String path, String name) throws Exception { - DomainFolder folder = state.getProject().getProjectData().getFolder(path); - folder.createFile(name, vtSession, monitor); - vtSession.setName(name); + public void saveSessionAs(String path, String name) + throws InvalidNameException, CancelledException, IOException { + if (vtSession != null) { + throw new RuntimeException("Attempted to save a session when not open!"); + } + vtSession.endTransaction(transactionID, true); + try { + DomainFolder folder = state.getProject().getProjectData().getFolder(path); + folder.createFile(name, vtSession, monitor); + vtSession.setName(name); + } + finally { + transactionID = vtSession.startTransaction("VT Script"); + } } @Override public void cleanup(boolean success) { closeVersionTrackingSession(); - if (destinationProgram != null) { - closeProgram(destinationProgram); - } - if (sourceProgram != null) { - closeProgram(sourceProgram); - } - sourceProgram = null; - destinationProgram = null; + super.cleanup(success); } + /** + * This will release the current session and both source and destination programs. + * If either program needs to be held it is the script's responsibility to first retain + * the instance and add itself as a consumer. Any program consumer must release it + * when done using it. + */ public void closeVersionTrackingSession() { if (vtSession != null) { vtSession.endTransaction(transactionID, true); vtSession.release(this); + vtSession = null; + } + if (destinationProgram != null) { + destinationProgram.release(this); + destinationProgram = null; + } + if (sourceProgram != null) { + sourceProgram.release(this); + sourceProgram = null; } - } - public Program openProgram(String path) + private Program openProgram(String path) throws VersionException, CancelledException, IOException { if (state.getProject() == null) { throw new RuntimeException("No project open."); @@ -132,11 +184,6 @@ public abstract class GhidraVersionTrackingScript extends GhidraScript { return (Program) file.getDomainObject(this, true, true, monitor); } - @Override - public void closeProgram(Program program) { - program.release(this); - } - public Set getSourceFunctions() { if (vtSession == null) { throw new RuntimeException("You must have an open vt session"); diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/VTSessionContentHandler.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionContentHandler.java similarity index 71% rename from Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/VTSessionContentHandler.java rename to Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionContentHandler.java index 2698c4ed35..7f7be2ff35 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/VTSessionContentHandler.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionContentHandler.java @@ -13,33 +13,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.feature.vt.api.impl; +package ghidra.feature.vt.api.db; import java.io.IOException; import javax.swing.Icon; import db.DBHandle; -import db.OpenMode; import db.buffers.BufferFile; import generic.theme.GIcon; -import ghidra.feature.vt.api.db.VTSessionDB; import ghidra.framework.data.DBContentHandler; import ghidra.framework.data.DomainObjectMergeManager; import ghidra.framework.model.ChangeSet; import ghidra.framework.model.DomainObject; import ghidra.framework.store.*; -import ghidra.util.InvalidNameException; -import ghidra.util.Msg; +import ghidra.util.*; import ghidra.util.exception.CancelledException; import ghidra.util.exception.VersionException; import ghidra.util.task.TaskMonitor; public class VTSessionContentHandler extends DBContentHandler { - private static Icon ICON = new GIcon("icon.version.tracking.session.content.type"); + public static final String CONTENT_TYPE = "VersionTracking"; - public final static String CONTENT_TYPE = "VersionTracking"; + private static final Icon ICON = new GIcon("icon.version.tracking.session.content.type"); @Override public long createFile(FileSystem fs, FileSystem userfs, String path, String name, @@ -74,22 +71,40 @@ public class VTSessionContentHandler extends DBContentHandler { return "Version Tracking"; } + private void checkContentAndExclusiveCheckout(FolderItem item) throws IOException { + String contentType = item.getContentType(); + if (!contentType.equals(CONTENT_TYPE)) { + throw new IOException("Unsupported content type: " + contentType); + } + + // NOTE: item.isVersioned indicates that item is located on versioned filesystem + // and is not checked-out, otheriwse assume item in local filesystem and must + // ensure if any checkout is exclusive. + if (item.isVersioned() || (item.isCheckedOut() && !item.isCheckedOutExclusive())) { + throw new IOException( + "Unsupported VT Session use: session file must be checked-out exclusive"); + } + } + @Override public VTSessionDB getDomainObject(FolderItem item, FileSystem userfs, long checkoutId, boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor) throws IOException, CancelledException, VersionException { - String contentType = item.getContentType(); - if (!contentType.equals(CONTENT_TYPE)) { - throw new IOException("Unsupported content type: " + contentType); + checkContentAndExclusiveCheckout(item); + + if (item.isReadOnly()) { + throw new ReadOnlyException("VT Session file is set read-only which prevents its use"); } + try { DatabaseItem dbItem = (DatabaseItem) item; BufferFile bf = dbItem.openForUpdate(checkoutId); DBHandle dbh = new DBHandle(bf, okToRecover, monitor); boolean success = false; try { - VTSessionDB db = VTSessionDB.getVTSession(dbh, OpenMode.UPGRADE, consumer, monitor); + // NOTE: Always open with DB upgrade enabled + VTSessionDB db = new VTSessionDB(dbh, monitor, consumer); success = true; return db; } @@ -99,13 +114,7 @@ public class VTSessionContentHandler extends DBContentHandler { } } } - catch (VersionException e) { - throw e; - } - catch (IOException e) { - throw e; - } - catch (CancelledException e) { + catch (VersionException | IOException | CancelledException e) { throw e; } catch (Throwable t) { @@ -134,12 +143,7 @@ public class VTSessionContentHandler extends DBContentHandler { int minChangeVersion, TaskMonitor monitor) throws IOException, CancelledException, VersionException { - String contentType = item.getContentType(); - if (!contentType.equals(CONTENT_TYPE)) { - throw new IOException("Unsupported content type: " + contentType); - } return getReadOnlyObject(item, -1, false, consumer, monitor); - } @Override @@ -154,43 +158,14 @@ public class VTSessionContentHandler extends DBContentHandler { Object consumer, TaskMonitor monitor) throws IOException, VersionException, CancelledException { - String contentType = item.getContentType(); - if (contentType != null && !contentType.equals(CONTENT_TYPE)) { - throw new IOException("Unsupported content type: " + contentType); - } - try { - DatabaseItem dbItem = (DatabaseItem) item; - BufferFile bf = dbItem.open(); - DBHandle dbh = new DBHandle(bf); - boolean success = false; - try { - VTSessionDB manager = - VTSessionDB.getVTSession(dbh, OpenMode.READ_ONLY, consumer, monitor); - success = true; - return manager; - } - finally { - if (!success) { - dbh.close(); - } - } - } - catch (IOException e) { - throw e; - } - catch (Throwable t) { - Msg.error(this, "Get read-only object failed", t); - String msg = t.getMessage(); - if (msg == null) { - msg = t.toString(); - } - throw new IOException("Open failed: " + msg, t); - } + checkContentAndExclusiveCheckout(item); + + throw new ReadOnlyException("VT Session does not support read-only use"); } @Override public boolean isPrivateContentType() { - return true; + return false; } } diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java index 941d8f762a..a5c34db90a 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java @@ -26,6 +26,7 @@ import ghidra.feature.vt.api.correlator.program.ImpliedMatchProgramCorrelator; import ghidra.feature.vt.api.correlator.program.ManualMatchProgramCorrelator; import ghidra.feature.vt.api.impl.*; import ghidra.feature.vt.api.main.*; +import ghidra.feature.vt.api.util.VTSessionFileUtil; import ghidra.framework.data.DomainObjectAdapterDB; import ghidra.framework.model.*; import ghidra.framework.model.TransactionInfo.Status; @@ -36,26 +37,30 @@ import ghidra.program.database.map.AddressMap; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSet; import ghidra.program.model.listing.Program; -import ghidra.util.Msg; +import ghidra.util.*; import ghidra.util.exception.*; import ghidra.util.task.TaskLauncher; import ghidra.util.task.TaskMonitor; public class VTSessionDB extends DomainObjectAdapterDB implements VTSession { + private final static Field[] COL_FIELDS = new Field[] { StringField.INSTANCE }; private final static String[] COL_TYPES = new String[] { "Value" }; private final static Schema SCHEMA = new Schema(0, StringField.INSTANCE, "Key", COL_FIELDS, COL_TYPES); - private static final String PROGRAM_ID_PROPERTYLIST_NAME = "ProgramIDs"; - private static final String SOURCE_PROGRAM_ID_PROPERTY_KEY = "SourceProgramID"; - private static final String DESTINATION_PROGRAM_ID_PROPERTY_KEY = "DestinationProgramID"; + // Source and Destination Program IDs are retained within OptionsDB + static final String PROGRAM_ID_PROPERTYLIST_NAME = "ProgramIDs"; + static final String SOURCE_PROGRAM_ID_PROPERTY_KEY = "SourceProgramID"; + static final String DESTINATION_PROGRAM_ID_PROPERTY_KEY = "DestinationProgramID"; + private static final String UNUSED_DEFAULT_NAME = "Untitled"; private static final int EVENT_NOTIFICATION_DELAY = 500; - private static final int EVENT_BUFFER_SIZE = 100; - private static final long MANUAL_MATCH_SET_ID = 0; private static final long IMPLIED_MATCH_SET_ID = -1; + + // PropertyTable is used solely to retain DB version + // NOTE: OptionsDB already has a table named "Property Table" private static final String PROPERTY_TABLE_NAME = "PropertyTable"; private static final String DB_VERSION_PROPERTY_NAME = "DB_VERSION"; @@ -65,8 +70,11 @@ public class VTSessionDB extends DomainObjectAdapterDB implements VTSession { * 14-Nov-2019 - version 2 - Corrected fixed length indexing implementation causing * change in index table low-level storage for newly * created tables. + * 16-Feb-2024 - version 3 - No schema change. Version imposed to prevent older versions + * of Ghidra from opening session objects which may have been + * added to version controlled repository. */ - private static final int DB_VERSION = 2; + private static final int DB_VERSION = 3; /** * UPGRADE_REQUIRED_BFORE_VERSION should be changed to DB_VERSION any time the @@ -75,7 +83,7 @@ public class VTSessionDB extends DomainObjectAdapterDB implements VTSession { * if the data's version is >= UPGRADE_REQUIRED_BEFORE_VERSION and <= DB_VERSION. */ // NOTE: Schema upgrades are not currently supported - private static final int UPGRADE_REQUIRED_BEFORE_VERSION = 1; + private static final int UPGRADE_REQUIRED_BEFORE_VERSION = 3; private VTMatchSetTableDBAdapter matchSetTableAdapter; private AssociationDatabaseManager associationManager; @@ -89,40 +97,119 @@ public class VTSessionDB extends DomainObjectAdapterDB implements VTSession { private VTMatchSet impliedMatchSet; private boolean changeSetsModified = false; - private Table propertyTable; + private Table propertyTable; // used to retain DB version only + /** + * Factory method which constructs a new VTSessionDB using specified source and desitination + * programs. + * @param name name to be assigned to the resulting domain object file + * @param sourceProgram session source program within active project + * @param destinationProgram session destination program open for update within active project + * @param consumer object consumer resposible for the proper release of the returned instance. + * @return new {@link VTSessionDB} object + * @throws IOException if an IO error occurs + * @deprecated {@link #VTSessionDB(String, Program, Program, Object)} should be used instead + */ + @Deprecated(since = "11.1", forRemoval = true) public static VTSessionDB createVTSession(String name, Program sourceProgram, Program destinationProgram, Object consumer) throws IOException { + return new VTSessionDB(name, sourceProgram, destinationProgram, consumer); + } - VTSessionDB session = new VTSessionDB(new DBHandle(), consumer); + /** + * Construct a new VTSessionDB using specified source and desitination programs. + * @param name name to be assigned to the resulting domain object file + * @param sourceProgram session source program within active project + * @param destinationProgram session destination program open for update within active project + * @param consumer object consumer resposible for the proper release of the returned instance. + * @throws IOException if an IO error occurs + */ + public VTSessionDB(String name, Program sourceProgram, Program destinationProgram, + Object consumer) throws IOException { + super(new DBHandle(), UNUSED_DEFAULT_NAME, EVENT_NOTIFICATION_DELAY, consumer); - int ID = session.startTransaction("Constructing New Version Tracking Match Set"); + propertyTable = dbh.getTable(PROPERTY_TABLE_NAME); + + int ID = startTransaction("Constructing New Version Tracking Match Set"); try { - session.propertyTable = session.dbh.createTable(PROPERTY_TABLE_NAME, SCHEMA); - session.matchSetTableAdapter = VTMatchSetTableDBAdapter.createAdapter(session.dbh); - session.associationManager = - AssociationDatabaseManager.createAssociationManager(session.dbh, session); - session.matchTagAdapter = VTMatchTagDBAdapter.createAdapter(session.dbh); - session.initializePrograms(sourceProgram, destinationProgram); - session.createMatchSet( - new ManualMatchProgramCorrelator(sourceProgram, destinationProgram), + propertyTable = dbh.createTable(PROPERTY_TABLE_NAME, SCHEMA); + matchSetTableAdapter = VTMatchSetTableDBAdapter.createAdapter(dbh); + associationManager = AssociationDatabaseManager.createAssociationManager(dbh, this); + matchTagAdapter = VTMatchTagDBAdapter.createAdapter(dbh); + + initializePrograms(sourceProgram, destinationProgram, true); + + createMatchSet(new ManualMatchProgramCorrelator(sourceProgram, destinationProgram), MANUAL_MATCH_SET_ID); - session.createMatchSet( - new ImpliedMatchProgramCorrelator(sourceProgram, destinationProgram), + createMatchSet(new ImpliedMatchProgramCorrelator(sourceProgram, destinationProgram), IMPLIED_MATCH_SET_ID); - session.updateVersion(); + + updateVersion(); } finally { - session.endTransaction(ID, true); + endTransaction(ID, true); } + try { - session.addSynchronizedDomainObject(destinationProgram); + addSynchronizedDomainObject(destinationProgram); } catch (Exception e) { - session.close(); - throw new RuntimeException(e.getMessage(), e); + close(); + throw new RuntimeException(e); } - return session; + } + + /** + * Construct an existing VT session object and open with UPGRADE enabled. + * The caller (i.e., content handler) must ensure that project has exclusive access to + * the domain file before it was open and {@link DBHandle} supplied. + * @param dbHandle database handle + * @param monitor TaskMonitor that allows the open to be canceled. + * @param consumer the object that keeping the session open. + * @throws IOException if an error accessing the database occurs. + * @throws VersionException if database version does not match implementation, UPGRADE may be possible. + * @throws CancelledException if instantiation is canceled by monitor + */ + @SuppressWarnings("unused") + VTSessionDB(DBHandle dbHandle, TaskMonitor monitor, Object consumer) + throws VersionException, IOException, CancelledException { + super(dbHandle, UNUSED_DEFAULT_NAME, EVENT_NOTIFICATION_DELAY, consumer); + + // openMode forced to UPGRADE since we do not support read-only mode + // It is assumed we always have exclusive access to the underlying database + OpenMode openMode = OpenMode.UPGRADE; + + propertyTable = dbHandle.getTable(PROPERTY_TABLE_NAME); + + int storedVersion = getVersion(); + if (storedVersion > DB_VERSION) { + throw new VersionException(VersionException.NEWER_VERSION, false); + } + + // The following version logic holds true for DB_VERSION <= 3 which assume no additional + // DB index tables will be added when open for update/upgrade. This may not hold + // true for future revisions associated with table schema changes in which case the + // UPGRADE_REQUIRED_BEFORE_VERSION value should equal DB_VERSION. Current logic + // assumes no schema changes will be made during upgrade. + if (storedVersion < UPGRADE_REQUIRED_BEFORE_VERSION) { + if (openMode != OpenMode.UPGRADE) { // should always be open with UPGRADE mode + throw new VersionException( + "Version Tracking Sessions do not support schema upgrades.", + VersionException.OLDER_VERSION, true); + } + withTransaction("Update DBVersion", () -> updateVersion()); + clearUndo(false); + changed = true; + } + + // NOTE: code below will not make changes (no transaction is open) + // Additional supported required to facilitate schema change during upgrade if needed. + + matchSetTableAdapter = VTMatchSetTableDBAdapter.getAdapter(dbHandle, openMode, monitor); + associationManager = + AssociationDatabaseManager.getAssociationManager(dbHandle, this, openMode, monitor); + matchTagAdapter = VTMatchTagDBAdapter.getAdapter(dbHandle, openMode, monitor); + loadMatchSets(openMode, monitor); } private void updateVersion() throws IOException { @@ -131,47 +218,174 @@ public class VTSessionDB extends DomainObjectAdapterDB implements VTSession { propertyTable.putRecord(record); } - public static VTSessionDB getVTSession(DBHandle dbHandle, OpenMode openMode, Object consumer, - TaskMonitor monitor) throws VersionException, IOException { - - VTSessionDB session = new VTSessionDB(dbHandle, consumer); - int storedVersion = session.getVersion(); - - if (storedVersion > DB_VERSION) { - throw new VersionException(VersionException.NEWER_VERSION, false); + private int getVersion() throws IOException { + // DB Version was added in release (11/6/2012) + // if record does not exist return 0; + if (propertyTable == null) { + return 0; } - // The following version logic holds true for DB_VERSION=2 which assumes no additional - // DB index tables will be added when open for update/upgrade. This will not hold - // true for future revisions associated with table schema changes in which case the - // UPGRADE_REQUIRED_BEFORE_VERSION value should equal DB_VERSION. - if (storedVersion < UPGRADE_REQUIRED_BEFORE_VERSION) { - throw new VersionException("Version Tracking Sessions do not support schema upgrades."); + DBRecord record = propertyTable.getRecord(new StringField(DB_VERSION_PROPERTY_NAME)); + + if (record != null) { + String s = record.getString(0); + try { + return Integer.parseInt(s); + } + catch (NumberFormatException e) { + // just use default + } + } + return 0; + } + + @Override + protected void setDomainFile(DomainFile df) throws DomainObjectException { + DomainFolder parent = df.getParent(); + if (parent != null && sourceProgram == null) { + try { + openSourceAndDestinationPrograms(parent.getProjectData()); + } + catch (IOException e) { + throw new DomainObjectException(e); + } + } + super.setDomainFile(df); + } + + /** + * Open associated source and destination program files and complete session initialization. + * @param projectData active project data + * @throws IOException if source or destination program not found within specified project + * or an error occured while opening them (e.g., upgrade required). + */ + private void openSourceAndDestinationPrograms(ProjectData projectData) throws IOException { + String sourceProgramID = getSourceProgramID(); + String destinationProgramID = getDestinationProgramID(); + DomainFile sourceFile = projectData.getFileByID(sourceProgramID); + DomainFile destinationFile = projectData.getFileByID(destinationProgramID); + if (sourceFile == null) { + throw new IOException("Source program is missing for this Version Tracking Session!"); + } + if (destinationFile == null) { + throw new IOException( + "Destination program is missing for this Version Tracking Session!"); } - session.matchSetTableAdapter = - VTMatchSetTableDBAdapter.getAdapter(session.getDBHandle(), openMode, monitor); - session.associationManager = - AssociationDatabaseManager.getAssociationManager(dbHandle, session, openMode, monitor); - session.matchTagAdapter = - VTMatchTagDBAdapter.getAdapter(session.getDBHandle(), openMode, monitor); - session.loadMatchSets(openMode, monitor); - return session; + // Must ensure that destination program file can be updated + VTSessionFileUtil.validateDestinationProgramFile(destinationFile, true, + SystemUtilities.isInHeadlessMode()); + + VTSessionFileUtil.validateSourceProgramFile(sourceFile, true); + + sourceProgram = openProgram(sourceFile, true); + + if (sourceProgram != null) { + destinationProgram = openProgram(destinationFile, false); + } + + if (sourceProgram == null || destinationProgram == null) { + StringBuilder buffer = new StringBuilder( + "Session not opened because one or both programs did not open.\n"); + if (sourceProgram != null) { + sourceProgram.release(this); + sourceProgram = null; + } + else { + buffer.append("\tUnable to open source program \"" + sourceFile + "\"\n"); + } + + if (destinationProgram != null) { + destinationProgram.release(this); + destinationProgram = null; + } + else { + buffer.append("\tUnable to open destination program \"" + destinationFile + "\"\n"); + } + + throw new IOException(buffer.toString()); + } + + associationManager.sessionInitialized(); + + try { + addSynchronizedDomainObject(destinationProgram); + } + catch (Exception e) { + sourceProgram.release(this); + sourceProgram = null; + destinationProgram.release(this); + destinationProgram = null; + throw new IOException(e.getMessage()); + } + } + + private Program openProgram(DomainFile domainFile, boolean isSource) { + + String type = isSource ? "VT Source Program" : "VT Destination Program"; + + if (SystemUtilities.isInHeadlessMode()) { + try { + return (Program) domainFile.getDomainObject(this, false, false, TaskMonitor.DUMMY); + } + catch (CancelledException e) { + throw new AssertionError(e); // unexpected + } + catch (VersionException e) { + VersionExceptionHandler.showVersionError(null, domainFile.getName(), type, "open", + e); + } + catch (IOException e) { + Msg.showError(this, null, "Can't open " + type + ": " + domainFile.getName(), + e.getMessage()); + } + return null; + } + + // Headed GUI Mode + + OpenProgramTask openTask = new OpenProgramTask(domainFile, this); + openTask.setOpenPromptText("Open " + type); + + TaskLauncher.launch(openTask); + + OpenProgramRequest openProgram = openTask.getOpenProgram(); + return openProgram != null ? openProgram.getProgram() : null; + } + + public String getSourceProgramID() { + Options properties = getOptions(PROGRAM_ID_PROPERTYLIST_NAME); + return properties.getString(SOURCE_PROGRAM_ID_PROPERTY_KEY, ""); + } + + public String getDestinationProgramID() { + Options properties = getOptions(PROGRAM_ID_PROPERTYLIST_NAME); + return properties.getString(DESTINATION_PROGRAM_ID_PROPERTY_KEY, ""); } @SuppressWarnings("hiding") // this is from our constructor - private void initializePrograms(Program sourceProgram, Program destinationProgram) { - this.sourceProgram = sourceProgram; - this.destinationProgram = destinationProgram; - sourceProgram.addConsumer(this); - destinationProgram.addConsumer(this); - Options properties = getOptions(PROGRAM_ID_PROPERTYLIST_NAME); - DomainFile sourceDomainFile = sourceProgram.getDomainFile(); - properties.setString(SOURCE_PROGRAM_ID_PROPERTY_KEY, sourceDomainFile.getFileID()); - DomainFile destinationDomainFile = destinationProgram.getDomainFile(); - properties.setString(DESTINATION_PROGRAM_ID_PROPERTY_KEY, - destinationDomainFile.getFileID()); + private void initializePrograms(Program sourceProgram, Program destinationProgram, + boolean rememberProgramIds) throws IOException { + if (!destinationProgram.canSave()) { + throw new ReadOnlyException( + "VT Session destination program is read-only which prevents its use"); + } + + this.sourceProgram = sourceProgram; + sourceProgram.addConsumer(this); + + this.destinationProgram = destinationProgram; + destinationProgram.addConsumer(this); + + if (rememberProgramIds) { + Options properties = getOptions(PROGRAM_ID_PROPERTYLIST_NAME); + DomainFile sourceDomainFile = sourceProgram.getDomainFile(); + properties.setString(SOURCE_PROGRAM_ID_PROPERTY_KEY, sourceDomainFile.getFileID()); + DomainFile destinationDomainFile = destinationProgram.getDomainFile(); + properties.setString(DESTINATION_PROGRAM_ID_PROPERTY_KEY, + destinationDomainFile.getFileID()); + } } @Override @@ -202,119 +416,6 @@ public class VTSessionDB extends DomainObjectAdapterDB implements VTSession { sourceProgram.addConsumer(this); } - public String getSourceProgramID() { - Options properties = getOptions(PROGRAM_ID_PROPERTYLIST_NAME); - return properties.getString(SOURCE_PROGRAM_ID_PROPERTY_KEY, ""); - } - - public String getDestinationProgramID() { - Options properties = getOptions(PROGRAM_ID_PROPERTYLIST_NAME); - return properties.getString(DESTINATION_PROGRAM_ID_PROPERTY_KEY, ""); - } - - private VTSessionDB(DBHandle dbHandle, Object consumer) { - super(dbHandle, UNUSED_DEFAULT_NAME, EVENT_NOTIFICATION_DELAY, consumer); - propertyTable = dbHandle.getTable(PROPERTY_TABLE_NAME); - } - - public int getVersion() throws IOException { - // DB Version was added in release (11/6/2012) - // if record does not exist return 0; - if (propertyTable == null) { - return 0; - } - DBRecord record = propertyTable.getRecord(new StringField(DB_VERSION_PROPERTY_NAME)); - - if (record != null) { - String s = record.getString(0); - try { - return Integer.parseInt(s); - } - catch (NumberFormatException e) { - // just use default - } - } - return 0; - - } - - @Override - protected void setDomainFile(DomainFile df) { - super.setDomainFile(df); - DomainFolder parent = df.getParent(); - if (parent == null) { - return; - } - if (sourceProgram != null) { // source and destination are already open - return; - } - ProjectData projectData = parent.getProjectData(); - String sourceProgramID = getSourceProgramID(); - String destinationProgramID = getDestinationProgramID(); - DomainFile sourceFile = projectData.getFileByID(sourceProgramID); - DomainFile destinationFile = projectData.getFileByID(destinationProgramID); - if (sourceFile == null) { - throw new RuntimeException( - "Source program is missing for this Version Tracking Session!"); - } - if (destinationFile == null) { - throw new RuntimeException( - "Destination program is missing for this Version Tracking Session!"); - } - - sourceProgram = openProgram(sourceFile, true); - - if (sourceProgram != null) { - destinationProgram = openProgram(destinationFile, false); - } - - if (sourceProgram == null || destinationProgram == null) { - StringBuilder buffer = new StringBuilder( - "Session not opened because one or both programs did not open.\n"); - if (sourceProgram != null) { - sourceProgram.release(this); - sourceProgram = null; - } - else { - buffer.append("\tUnable to open source program \"" + sourceFile + "\"\n"); - } - - if (destinationProgram != null) { - destinationProgram.release(this); - destinationProgram = null; - } - else { - buffer.append("\tUnable to open destination program \"" + destinationFile + "\"\n"); - } - - throw new RuntimeException(buffer.toString()); - } - - associationManager.sessionInitialized(); - - try { - addSynchronizedDomainObject(destinationProgram); - } - catch (Exception e) { - sourceProgram.release(this); - destinationProgram.release(this); - throw new RuntimeException(e.getMessage()); - } - - } - - private Program openProgram(DomainFile domainFile, boolean isSource) { - - OpenProgramTask openTask = new OpenProgramTask(domainFile, this); - String type = isSource ? "(source program)" : "(destination program)"; - openTask.setOpenPromptText("Open " + type); - - TaskLauncher.launch(openTask); - - OpenProgramRequest openProgram = openTask.getOpenProgram(); - return openProgram != null ? openProgram.getProgram() : null; - } - @Override public void release(Object consumer) { super.release(consumer); diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/util/VTSessionFileUtil.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/util/VTSessionFileUtil.java new file mode 100644 index 0000000000..38cb78a76b --- /dev/null +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/util/VTSessionFileUtil.java @@ -0,0 +1,186 @@ +/* ### + * 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.feature.vt.api.util; + +import java.io.IOException; + +import ghidra.app.util.dialog.CheckoutDialog; +import ghidra.app.util.task.ProgramOpener; +import ghidra.feature.vt.api.db.VTSessionDB; +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainFolder; +import ghidra.framework.remote.User; +import ghidra.program.database.ProgramDB; +import ghidra.util.Msg; +import ghidra.util.SystemUtilities; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.*; + +/** + * {@link VTSessionFileUtil} provides methods for checking {@link VTSessionDB} source and + * destination program files prior to being opened and used during session instantiation. + */ +public class VTSessionFileUtil { + + // static utility class + private VTSessionFileUtil() { + } + + /** + * Validate a VT source program to ensure it meets minimum criteria to open with a VTSession. + * The following validation checks are performed: + *
    + *
  • file must correspond to a ProgramDB
  • + *
+ * If an error is thrown it is intended to be augmented for proper presentation. + * + * @param file VT Session source program domain file + * @param includeFilePathInError if true file path will be appended to any exception throw + * @throws IllegalArgumentException if any VT source program file criteria is not satisfied + */ + public static void validateSourceProgramFile(DomainFile file, boolean includeFilePathInError) + throws IllegalArgumentException { + String error = null; + if (!ProgramDB.class.isAssignableFrom(file.getDomainObjectClass())) { + error = "Source file does not correspond to a Program"; + } + if (error != null) { + if (includeFilePathInError) { + error += ":\n" + file.getPathname(); + } + throw new IllegalArgumentException(error); + } + } + + /** + * Validate a VT destination program to ensure it meets minimum criteria to open with a VTSession. + * GUI mode only: If file is versioned and not checked-out the user may be prompted to perform + * an optional checkout of the file. Prompting for checkout will not occur if this method + * is invoked from the Swing thread or operating in a headless mode. + * The following validation checks are performed: + *
    + *
  • file must correspond to a ProgramDB
  • + *
  • file must be contained within the active project
  • + *
  • file must not be marked read-only
  • + *
  • if file is versioned it must be checked-out (user may be prompted to do this)
  • + *
+ * If an error is thrown it is intended to be augmented for proper presentation. + * + * @param file VT Session destination program domain file + * @param includeFilePathInError if true file path will be appended to any exception throw + * @param silent if user interaction should not be performed. This should be true if + * filesystem lock is currently held. + * @throws IllegalArgumentException if any VT destination program file criteria is not satisfied + */ + public static void validateDestinationProgramFile(DomainFile file, + boolean includeFilePathInError, boolean silent) throws IllegalArgumentException { + String error = null; + if (!ProgramDB.class.isAssignableFrom(file.getDomainObjectClass())) { + error = "Destination file does not correspond to a Program"; + } + else { + DomainFolder folder = file.getParent(); + if (folder == null || !folder.isInWritableProject()) { + error = "Destination file must be from active project"; + } + else if (file.isReadOnly()) { + error = "Destination file must not be read-only"; + } + else if (file.isVersioned()) { + if (!silent) { + doOptionalDestinationProgramCheckout(file); + } + if (!file.isCheckedOut()) { + error = "Versioned destination file must be checked-out for update"; + } + } + } + if (error != null) { + if (includeFilePathInError) { + error += ":\n" + file.getPathname(); + } + throw new IllegalArgumentException(error); + } + } + + /** + * Determine if the specified {@link DomainFile} will permit update. + * @param file domain file + * @return true if file permits update else false + */ + public static boolean canUpdate(DomainFile file) { + DomainFolder folder = file.getParent(); + if (folder == null || !folder.isInWritableProject()) { + return false; + } + if (file.isReadOnly()) { + return false; + } + if (file.isVersioned()) { + return false; + } + return true; + } + + private static void doOptionalDestinationProgramCheckout(DomainFile file) { + + if (SystemUtilities.isInHeadlessMode() || !file.canCheckout()) { + return; + } + + User user = file.getParent().getProjectData().getUser(); + CheckoutDialog dialog = new CheckoutDialog(file, user); + dialog.setTitle("VT Destination Program not Checked Out"); + if (dialog.showDialog() == CheckoutDialog.CHECKOUT) { // uses Swing thread + CheckoutDestinationProgramTask task = + new CheckoutDestinationProgramTask(file, dialog.exclusiveCheckout()); + TaskLauncher.launch(task); + } + } + + private static class CheckoutDestinationProgramTask extends Task { + + private DomainFile file; + boolean exclusiveCheckout; + + CheckoutDestinationProgramTask(DomainFile file, boolean exclusiveCheckout) { + super("Checking Out " + file, true, true, true, true); + this.file = file; + this.exclusiveCheckout = exclusiveCheckout; + } + + @Override + public void run(TaskMonitor monitor) throws CancelledException { + monitor.setMessage("Checking Out " + file); + try { + if (!file.checkout(exclusiveCheckout, monitor)) { + Msg.showError(ProgramOpener.class, null, "Checkout Failed", + "Exclusive checkout failed for: " + file + + "\nOne or more users have file checked out!"); + } + } + catch (IOException e) { + Msg.showError(ProgramOpener.class, null, "Checkout Failed", + "Checkout failed for: " + file + "\n" + e.getMessage()); + } + catch (CancelledException e) { + // ignore + } + } + + } + +} diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTController.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTController.java index 4ddfb9bc80..ac14a51972 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTController.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTController.java @@ -46,7 +46,7 @@ public interface VTController extends VTSessionSupplier { @Override public VTSession getSession(); - public void openVersionTrackingSession(DomainFile domainFile); + public boolean openVersionTrackingSession(DomainFile domainFile); public void openVersionTrackingSession(VTSession session); diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTControllerImpl.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTControllerImpl.java index c200b88ae9..8a3deba1dd 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTControllerImpl.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTControllerImpl.java @@ -22,31 +22,36 @@ import java.util.*; import javax.swing.SwingUtilities; import docking.ActionContext; +import docking.widgets.OptionDialog; import ghidra.app.plugin.core.codebrowser.CodeViewerActionContext; import ghidra.app.plugin.core.colorizer.ColorizingService; import ghidra.feature.vt.api.db.VTAssociationDB; import ghidra.feature.vt.api.db.VTSessionDB; import ghidra.feature.vt.api.main.*; +import ghidra.feature.vt.api.util.VTSessionFileUtil; import ghidra.feature.vt.gui.duallisting.VTListingContext; import ghidra.feature.vt.gui.provider.markuptable.VTMarkupItemContext; import ghidra.feature.vt.gui.task.SaveTask; import ghidra.feature.vt.gui.task.VtTask; import ghidra.feature.vt.gui.util.MatchInfo; import ghidra.feature.vt.gui.util.MatchInfoFactory; +import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.data.DomainObjectAdapterDB; +import ghidra.framework.main.AppInfo; import ghidra.framework.main.SaveDataDialog; +import ghidra.framework.main.projectdata.actions.CheckoutsDialog; import ghidra.framework.model.*; import ghidra.framework.options.*; import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.ServiceProvider; +import ghidra.framework.store.ItemCheckoutStatus; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSetView; import ghidra.program.model.listing.*; import ghidra.program.model.symbol.Symbol; import ghidra.program.util.AddressCorrelation; import ghidra.program.util.ProgramLocation; -import ghidra.util.Msg; -import ghidra.util.SystemUtilities; +import ghidra.util.*; import ghidra.util.datastruct.WeakValueHashMap; import ghidra.util.exception.CancelledException; import ghidra.util.exception.VersionException; @@ -94,30 +99,193 @@ public class VTControllerImpl return session; } + private boolean checkSessionFileAccess(DomainFile domainFile) { + + DomainFolder folder = domainFile.getParent(); + if (folder == null || !folder.isInWritableProject()) { + Msg.showError(this, null, "Can't open VT Session: " + domainFile, + "VT Session file use limited to active project only."); + return false; + } + if (domainFile.isVersioned()) { + if (domainFile.isCheckedOut()) { + if (!domainFile.isCheckedOutExclusive()) { + Msg.showError(this, null, "Can't open VT Session: " + domainFile, + "VT Session file is checked-out but does not have exclusive access.\n" + + "You must undo checkout and re-checkout with exclusive access."); + return false; + } + if (domainFile.isReadOnly()) { + Msg.showError(this, null, "Can't open VT Session: " + domainFile, + "VT Session file is set read-only which prevents its use."); + return false; + } + return true; + } + return checkoutSession(domainFile); + } + else if (domainFile.isReadOnly()) { // non-versioned file + Msg.showError(this, null, "Can't open VT Session: " + domainFile, + "VT Session file is set read-only which prevents its use."); + return false; + } + return true; + } + + private boolean checkoutSession(DomainFile domainFile) { + + Project activeProject = AppInfo.getActiveProject(); + RepositoryAdapter repository = activeProject.getRepository(); + + if (repository != null) { + try { + ItemCheckoutStatus[] checkouts = domainFile.getCheckouts(); + if (checkouts.length != 0) { + int rc = OptionDialog.showOptionDialogWithCancelAsDefaultButton(null, + "Checkout VT Session", + "VT Session " + domainFile.getName() + " is NOT CHECKED OUT but " + + "is checked-out by another user.\n" + + "Opening VT Session requires an exclusive check out of this file.\n" + + "Do you want to view the list of active checkouts for this file?", + "View Checkout(s)..."); + if (rc != OptionDialog.OPTION_ONE) { + return false; + } + + CheckoutsDialog dialog = new CheckoutsDialog(plugin.getTool(), + repository.getUser(), domainFile, checkouts); + plugin.getTool().showDialog(dialog); + + return false; + + } + } + catch (IOException e) { + Msg.showError(this, null, "Checkout VT Session Failed: " + domainFile.getName(), + e.getMessage()); + return false; + } + } + + int rc = OptionDialog.showOptionDialogWithCancelAsDefaultButton(null, "Checkout VT Session", + "VT Session " + domainFile.getName() + " is NOT CHECKED OUT.\n" + + "Opening VT Session requires an exclusive check out of this file.\n" + + "Do you want to Check Out this file?", + "Checkout..."); + if (rc != OptionDialog.OPTION_ONE) { + return false; + } + + TaskLauncher.launchModal("Checkout VT Session", new MonitoredRunnable() { + + @Override + public void monitoredRun(TaskMonitor monitor) { + try { + domainFile.checkout(true, monitor); + } + catch (CancelledException e) { + // ignore + } + catch (IOException e) { + Msg.showError(this, null, "Checkout VT Session Failed: " + domainFile.getName(), + e.getMessage()); + } + } + }); + return domainFile.isCheckedOutExclusive(); + } + @Override - public void openVersionTrackingSession(DomainFile domainFile) { + public boolean openVersionTrackingSession(DomainFile domainFile) { + if (!VTSession.class.isAssignableFrom(domainFile.getDomainObjectClass())) { + throw new IllegalArgumentException("File does not correspond to a VTSession"); + } if (!checkForUnSavedChanges()) { - return; + return false; } try { - VTSessionDB newSession = - (VTSessionDB) domainFile.getDomainObject(this, true, true, TaskMonitor.DUMMY); - doOpenSession(newSession); - } - catch (VersionException e) { - Msg.showError(this, null, "Can't open domainFile " + domainFile.getName(), - e.getMessage()); + if (!checkSessionFileAccess(domainFile)) { + return false; + } + + VTSessionDB vtSessionDB = getVTSessionDB(domainFile, this); + if (vtSessionDB != null) { + try { + openVersionTrackingSession(vtSessionDB); + return true; + } + finally { + vtSessionDB.release(this); + } + } } catch (CancelledException e) { - Msg.error(this, "Got unexexped cancelled exception", e); + // ignore - return false + } + catch (VersionException e) { + VersionExceptionHandler.showVersionError(null, domainFile.getName(), "VT Session", + "open", e); } catch (IOException e) { - Msg.showError(this, null, "Can't open " + domainFile.getName(), e.getMessage()); + Msg.showError(this, null, "Can't open VT Session: " + domainFile.getName(), + e.getMessage()); } + return false; + } + + private static class OpenVTSessionTask extends Task { + + private final Object consumer; + private final DomainFile vtSessionFile; + + Exception exception; + VTSessionDB vtSessionDB; + + OpenVTSessionTask(DomainFile vtSessionFile, Object consumer) { + super("Opening VT Session", true, false, true, true); + this.vtSessionFile = vtSessionFile; + this.consumer = consumer; + } + + @Override + public void run(TaskMonitor monitor) throws CancelledException { + try { + vtSessionDB = + (VTSessionDB) vtSessionFile.getDomainObject(consumer, true, true, monitor); + } + catch (Exception e) { + exception = e; + } + } + } + + private VTSessionDB getVTSessionDB(DomainFile vtSessionFile, Object consumer) + throws IOException, VersionException, CancelledException { + + OpenVTSessionTask task = new OpenVTSessionTask(vtSessionFile, consumer); + + TaskLauncher.launch(task); + + if (task.exception != null) { + if (task.exception instanceof CancelledException ce) { + throw ce; + } + if (task.exception instanceof VersionException ve) { + throw ve; + } + if (task.exception instanceof IOException ioe) { + throw ioe; + } + throw new IOException("VTSessionDB failure", task.exception); + } + + return task.vtSessionDB; } @Override public void openVersionTrackingSession(VTSession newSession) { + // FIXME: new session wizard should have handled existing session before starting - + // should be no need for this check if (!checkForUnSavedChanges()) { return; } @@ -595,43 +763,79 @@ public class VTControllerImpl // Inner Classes //================================================================================================== + private void updateProgram(DomainFile file, boolean isSource) { + + String type = isSource ? "Source" : "Destination"; + Program newProgram; + try { + newProgram = (Program) file.getDomainObject(this, false, false, TaskMonitor.DUMMY); + } + catch (Exception e) { + Msg.showError(this, getParentComponent(), + "Error opening VT " + type + " Program: " + file, e); + return; + } + + if (isSource) { + session.updateSourceProgram(newProgram); + } + else { + session.updateDestinationProgram(newProgram); + } + +// List events = new ArrayList(); +// events.add(new DomainObjectChangeRecord(DomainObjectEvent.RESTORED)); +// domainObjectChanged(new DomainObjectChangedEvent(newProgram, events)); + matchInfoFactory.clearCache(); + fireSessionChanged(); + } + private class MyFolderListener extends DomainFolderListenerAdapter { @Override public void domainFileObjectReplaced(DomainFile file, DomainObject oldObject) { - /** - * Special handling for when a file is checked-in. The existing program has be moved - * to a proxy file (no longer in the project) so that it can be closed and the program - * re-opened with the new version after the check-in merge. - */ if (session == null) { return; } - if (session.getSourceProgram() != oldObject && - session.getDestinationProgram() != oldObject) { - return; - } - Program newProgram; - try { - newProgram = (Program) file.getDomainObject(this, false, false, TaskMonitor.DUMMY); - } - catch (Exception e) { - Msg.showError(this, getParentComponent(), "Error opening program " + file, e); + + if (session.getSourceProgram() == oldObject) { + updateProgram(file, true); return; } - if (oldObject == session.getSourceProgram()) { - session.updateSourceProgram(newProgram); + String type; + if (session == oldObject) { + type = "VT Session"; } - else if (oldObject == session.getDestinationProgram()) { - session.updateDestinationProgram(newProgram); + else if (session.getDestinationProgram() == oldObject) { + if (VTSessionFileUtil.canUpdate(file)) { + updateProgram(file, false); + return; + } + type = "Destination Program"; } -// List events = new ArrayList(); -// events.add(new DomainObjectChangeRecord(DomainObjectEvent.RESTORED)); -// domainObjectChanged(new DomainObjectChangedEvent(newProgram, events)); - matchInfoFactory.clearCache(); - fireSessionChanged(); + else { + return; + } + + // Session or destination program can no longer be saved to project so we + // have no choice but to close session. + + // Since we are already in the Swing thread we need to delay closing so we do + // not continue to block the Swing thread and the checkin which is in progress. + // This allows the DomainFile checkin to complete its processing first. + SwingUtilities.invokeLater(() -> { + + Msg.showInfo(this, plugin.getTool().getToolFrame(), "Closing VT Session", + type + " checkin has forced session close.\n" + + "You will be prompted to save any other changes if needed, after which\n" + + "you may reopen the VT Session."); + + closeVersionTrackingSession(); + + // NOTE: a future convenience could be added to attempt reopening of session + }); } } diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTPlugin.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTPlugin.java index c83da509d1..0dc78124f0 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTPlugin.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTPlugin.java @@ -216,10 +216,10 @@ public class VTPlugin extends Plugin { for (DomainFile domainFile : data) { if (domainFile != null && VTSession.class.isAssignableFrom(domainFile.getDomainObjectClass())) { - openVersionTrackingSession(domainFile); - return true; + return controller.openVersionTrackingSession(domainFile); } } + DomainFile programFile1 = null; DomainFile programFile2 = null; for (DomainFile domainFile : data) { @@ -249,10 +249,6 @@ public class VTPlugin extends Plugin { return false; } - private void openVersionTrackingSession(DomainFile domainFile) { - controller.openVersionTrackingSession(domainFile); - } - @Override public void readConfigState(SaveState saveState) { controller.readConfigState(saveState); @@ -274,20 +270,18 @@ public class VTPlugin extends Plugin { @Override public void readDataState(SaveState saveState) { String pathname = saveState.getString("PATHNAME", null); - String location = saveState.getString("PROJECT_LOCATION", null); - String projectName = saveState.getString("PROJECT_NAME", null); - if (location == null || projectName == null) { + if (pathname == null) { return; } - ProjectLocator url = new ProjectLocator(location, projectName); - - ProjectData projectData = tool.getProject().getProjectData(url); - if (projectData == null) { - Msg.showError(this, tool.getToolFrame(), "File Not Found", "Could not find " + url); + Project project = tool.getProject(); + if (project == null) { return; } - + ProjectData projectData = project.getProjectData(); DomainFile domainFile = projectData.getFile(pathname); + if (domainFile == null) { + return; + } controller.openVersionTrackingSession(domainFile); } @@ -298,21 +292,7 @@ public class VTPlugin extends Plugin { return; } DomainFile domainFile = session.getDomainFile(); - - String projectLocation = null; - String projectName = null; - String path = null; - ProjectLocator url = domainFile.getProjectLocator(); - if (url != null) { - projectLocation = url.getLocation(); - projectName = url.getName(); - path = domainFile.getPathname(); - } - - saveState.putString("PROJECT_LOCATION", projectLocation); - saveState.putString("PROJECT_NAME", projectName); - saveState.putString("PATHNAME", path); - + saveState.putString("PATHNAME", domainFile.getPathname()); } @Override diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/CreateNewSessionTask.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/CreateNewSessionTask.java index 70416f9237..e940e422c3 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/CreateNewSessionTask.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/CreateNewSessionTask.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +15,11 @@ */ package ghidra.feature.vt.gui.wizard; +import java.io.IOException; + +import docking.wizard.WizardState; import ghidra.feature.vt.api.db.VTSessionDB; -import ghidra.feature.vt.api.main.VTSession; import ghidra.feature.vt.gui.plugin.VTController; -import ghidra.framework.data.DomainObjectAdapterDB; import ghidra.framework.model.DomainFolder; import ghidra.program.model.listing.Program; import ghidra.util.InvalidNameException; @@ -28,11 +28,6 @@ import ghidra.util.exception.CancelledException; import ghidra.util.task.Task; import ghidra.util.task.TaskMonitor; -import java.awt.EventQueue; -import java.io.IOException; - -import docking.wizard.WizardState; - public class CreateNewSessionTask extends Task { private final WizardState state; private final VTController controller; @@ -45,57 +40,41 @@ public class CreateNewSessionTask extends Task { @Override public void run(TaskMonitor monitor) { - VTSession session = null; + VTSessionDB session = null; String name = null; try { Program sourceProgram = (Program) state.get(VTWizardStateKey.SOURCE_PROGRAM); Program destinationProgram = (Program) state.get(VTWizardStateKey.DESTINATION_PROGRAM); - session = - VTSessionDB.createVTSession("New Session", sourceProgram, destinationProgram, this); + session = new VTSessionDB("New Session", sourceProgram, destinationProgram, this); - DomainObjectAdapterDB dobj = null; - if (session instanceof DomainObjectAdapterDB) { - dobj = (DomainObjectAdapterDB) session; - } sourceProgram.release(controller.getTool()); destinationProgram.release(controller.getTool()); - if (dobj != null) { - name = (String) state.get(VTWizardStateKey.SESSION_NAME); - DomainFolder folder = (DomainFolder) state.get(VTWizardStateKey.NEW_SESSION_FOLDER); - try { - folder.createFile(name, dobj, monitor); - } - catch (InvalidNameException e) { - Msg.showError(this, null, "Invalid Domain Object Name", - "Please report this error; the name should have been checked already"); - } + + name = (String) state.get(VTWizardStateKey.SESSION_NAME); + DomainFolder folder = (DomainFolder) state.get(VTWizardStateKey.NEW_SESSION_FOLDER); + try { + folder.createFile(name, session, monitor); + } + catch (InvalidNameException e) { + Msg.showError(this, null, "Invalid Domain Object Name", + "Please report this error; the name should have been checked already"); } - final VTSession finalSession = session; - EventQueue.invokeLater(new Runnable() { - public void run() { - controller.openVersionTrackingSession(finalSession); - releaseDomainObject(finalSession); - } - }); + controller.openVersionTrackingSession(session); } catch (CancelledException e) { - // the user cancelled; just cleanup - releaseDomainObject(session); + // ignore } catch (IOException e) { - releaseDomainObject(session); - Msg.showError(this, null, "Failed to Create Session", "Failed to create db file: " + - name, e); + Msg.showError(this, null, "Failed to Create Session", + "Failed to create db file: " + name, e); + } + finally { + if (session != null) { + session.release(this); + } } } - private void releaseDomainObject(VTSession session) { - if (session == null) { - return; - } - - ((VTSessionDB) session).release(this); - } } diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/NewSessionPanel.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/NewSessionPanel.java index 60eda3641e..844433617e 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/NewSessionPanel.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/NewSessionPanel.java @@ -15,22 +15,10 @@ */ package ghidra.feature.vt.gui.wizard; -import java.awt.BorderLayout; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; +import java.awt.*; +import java.util.*; -import javax.swing.BorderFactory; -import javax.swing.Box; -import javax.swing.Icon; -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSeparator; -import javax.swing.JTextField; -import javax.swing.SwingConstants; +import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; @@ -38,22 +26,19 @@ import org.apache.commons.lang3.StringUtils; import docking.widgets.button.BrowseButton; import docking.widgets.label.GDLabel; -import docking.wizard.AbstractMageJPanel; -import docking.wizard.WizardPanelDisplayability; -import docking.wizard.WizardState; +import docking.wizard.*; import generic.theme.GIcon; import generic.theme.GThemeDefaults.Ids.Fonts; import generic.theme.Gui; import ghidra.app.util.task.OpenProgramRequest; import ghidra.app.util.task.OpenProgramTask; +import ghidra.feature.vt.api.util.VTSessionFileUtil; import ghidra.framework.main.DataTreeDialog; import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFolder; import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.listing.Program; -import ghidra.util.HelpLocation; -import ghidra.util.InvalidNameException; -import ghidra.util.StringUtilities; +import ghidra.util.*; import ghidra.util.task.TaskLauncher; /** @@ -309,28 +294,31 @@ public class NewSessionPanel extends AbstractMageJPanel { private String createVTSessionName(String sourceName, String destinationName) { // if together they are within the bounds just return session name with both full names - if (sourceName.length() + destinationName.length() <= 2 * VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH) { + if (sourceName.length() + destinationName.length() <= 2 * + VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH) { return "VT_" + sourceName + "_" + destinationName; } // give destination name all space not used by source name if (sourceName.length() < VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH) { int leftover = VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH - sourceName.length(); - destinationName = - StringUtilities.trimMiddle(destinationName, VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH + leftover); + destinationName = StringUtilities.trimMiddle(destinationName, + VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH + leftover); return "VT_" + sourceName + "_" + destinationName; } // give source name all space not used by destination name if (destinationName.length() < VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH) { int leftover = VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH - destinationName.length(); - sourceName = StringUtilities.trimMiddle(sourceName, VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH + leftover); + sourceName = StringUtilities.trimMiddle(sourceName, + VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH + leftover); return "VT_" + sourceName + "_" + destinationName; } // if both too long, shorten both of them sourceName = StringUtilities.trimMiddle(sourceName, VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH); - destinationName = StringUtilities.trimMiddle(destinationName, VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH); + destinationName = + StringUtilities.trimMiddle(destinationName, VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH); return "VT_" + sourceName + "_" + destinationName; } @@ -418,16 +406,17 @@ public class NewSessionPanel extends AbstractMageJPanel { state.put(VTWizardStateKey.NEW_SESSION_FOLDER, folder); } - private void openProgram(ProgramInfo programInfo) { + private boolean openProgram(ProgramInfo programInfo) { if (programInfo.hasProgram()) { - return; // already open + return true; // already open } OpenProgramTask openProgramTask = new OpenProgramTask(programInfo.getFile(), tool); new TaskLauncher(openProgramTask, tool.getActiveWindow()); OpenProgramRequest openProgram = openProgramTask.getOpenProgram(); programInfo.setProgram(openProgram != null ? openProgram.getProgram() : null); + return programInfo.hasProgram(); } @Override @@ -480,19 +469,25 @@ public class NewSessionPanel extends AbstractMageJPanel { DomainFile file = folder.getFile(name); if (file != null) { notifyListenersOfStatusMessage( - "'" + file.getPathname() + "' is the name of an existing domain file"); + "'" + file.getPathname() + "' is the name of an existing project file"); return false; } - openProgram(sourceProgramInfo); - if (!sourceProgramInfo.hasProgram()) { + // Known Issue: Opening programs before comitted to using them (i.e., Next is clicked) seems + // premature and will subject user to prompts about possible checkout and/or upgrades + // with possible slow re-disassembly (see GP-4151) + + if (!isValidDestinationProgramFile() || !isValidSourceProgramFile()) { + return false; + } + + if (!openProgram(sourceProgramInfo)) { notifyListenersOfStatusMessage( "Can't open source program " + sourceProgramInfo.getName()); return false; } - openProgram(destinationProgramInfo); - if (!destinationProgramInfo.hasProgram()) { + if (!openProgram(destinationProgramInfo)) { notifyListenersOfStatusMessage( "Can't open destination program " + destinationProgramInfo.getName()); return false; @@ -502,6 +497,29 @@ public class NewSessionPanel extends AbstractMageJPanel { return true; } + private boolean isValidSourceProgramFile() { + try { + VTSessionFileUtil.validateSourceProgramFile(sourceProgramInfo.file, false); + } + catch (Exception e) { + notifyListenersOfStatusMessage(e.getMessage()); + return false; + } + return true; + } + + private boolean isValidDestinationProgramFile() { + try { + VTSessionFileUtil.validateDestinationProgramFile(destinationProgramInfo.file, false, + false); + } + catch (Exception e) { + notifyListenersOfStatusMessage(e.getMessage()); + return false; + } + return true; + } + @Override public void addDependencies(WizardState state) { // none diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/VTNewSessionWizardManager.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/VTNewSessionWizardManager.java index c5786ecc08..f56f1212f2 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/VTNewSessionWizardManager.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/VTNewSessionWizardManager.java @@ -44,8 +44,7 @@ public class VTNewSessionWizardManager extends AbstractMagePanelManager> createPanels() { - List> panels = - new ArrayList<>(); + List> panels = new ArrayList<>(); panels.add(new NewSessionPanel(controller.getTool())); panels.add(new PreconditionsPanel(this)); panels.add(new SummaryPanel()); diff --git a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTAddToSessionTest.java b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTAddToSessionTest.java index 7ea050ff5e..0f8cab350a 100644 --- a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTAddToSessionTest.java +++ b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTAddToSessionTest.java @@ -115,9 +115,8 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest { @Test public void testAddToSessionNoSelectionUnlimitedAddresses() throws Exception { - session = - VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager", - sourceProgram, destinationProgram, this); + session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager", + sourceProgram, destinationProgram, this); String sessionName = "Untitled"; @@ -170,9 +169,8 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest { @Test public void testAddToSessionNoSelectionLimitAddressesToEntireProgram() throws Exception { - session = - VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager", - sourceProgram, destinationProgram, this); + session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager", + sourceProgram, destinationProgram, this); String sessionName = "Untitled"; @@ -231,9 +229,8 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest { @Test public void testAddToSessionNoSelectionLimitAddressesToMyOwn() throws Exception { - session = - VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager", - sourceProgram, destinationProgram, this); + session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager", + sourceProgram, destinationProgram, this); String sessionName = "Untitled"; @@ -292,9 +289,8 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest { @Test public void testAddToSessionNoSelectionLimitAddressesToMyOwnChanged() throws Exception { - session = - VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager", - sourceProgram, destinationProgram, this); + session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager", + sourceProgram, destinationProgram, this); String sessionName = "Untitled"; @@ -366,9 +362,8 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest { @Test public void testAddToSessionWithSelectionLimitAddressesToEntireProgram() throws Exception { - session = - VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager", - sourceProgram, destinationProgram, this); + session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager", + sourceProgram, destinationProgram, this); String sessionName = "Untitled"; @@ -429,9 +424,8 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest { @Test public void testAddToSessionWithSelectionLimitAddressesToSelection() throws Exception { - session = - VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager", - sourceProgram, destinationProgram, this); + session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager", + sourceProgram, destinationProgram, this); String sessionName = "Untitled"; @@ -492,9 +486,8 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest { @Test public void testAddToSessionWithSelectionLimitAddressesToMyOwn() throws Exception { - session = - VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager", - sourceProgram, destinationProgram, this); + session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager", + sourceProgram, destinationProgram, this); String sessionName = "Untitled"; @@ -568,9 +561,8 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest { @Test public void testAddToSessionWithSelectionLimitAddressesToMyOwnThenBackNext() throws Exception { - session = - VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager", - sourceProgram, destinationProgram, this); + session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager", + sourceProgram, destinationProgram, this); String sessionName = "Untitled"; @@ -671,9 +663,8 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest { public void testAddToSessionResultingInNoMatchesFound() throws Exception { setErrorGUIEnabled(true); - session = - VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager", - sourceProgram, destinationProgram, this); + session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager", + sourceProgram, destinationProgram, this); String sessionName = "Untitled"; diff --git a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchAcceptTest.java b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchAcceptTest.java index 96f3d968c6..40d6046b74 100644 --- a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchAcceptTest.java +++ b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchAcceptTest.java @@ -78,9 +78,8 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest { plugin = getPlugin(tool, VTPlugin.class); controller = new VTControllerImpl(plugin); - session = - VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager", - sourceProgram, destinationProgram, this); + session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager", + sourceProgram, destinationProgram, this); runSwing(() -> controller.openVersionTrackingSession(session)); diff --git a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchApplyFunctionSignatureTest.java b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchApplyFunctionSignatureTest.java index ae0b8c6eda..e8ba71432f 100644 --- a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchApplyFunctionSignatureTest.java +++ b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchApplyFunctionSignatureTest.java @@ -81,9 +81,8 @@ public class VTMatchApplyFunctionSignatureTest extends AbstractGhidraHeadedInteg VTPlugin plugin = getPlugin(tool, VTPlugin.class); controller = new VTControllerImpl(plugin); - session = - VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager", - sourceProgram, destinationProgram, this); + session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager", + sourceProgram, destinationProgram, this); runSwing(() -> controller.openVersionTrackingSession(session)); @@ -390,8 +389,7 @@ public class VTMatchApplyFunctionSignatureTest extends AbstractGhidraHeadedInteg } @Test - public void testApplyMatch_ReplaceSignature_CustomSourceAndDest() - throws Exception { + public void testApplyMatch_ReplaceSignature_CustomSourceAndDest() throws Exception { useMatch("0x00401040", "0x00401040"); @@ -442,8 +440,7 @@ public class VTMatchApplyFunctionSignatureTest extends AbstractGhidraHeadedInteg } @Test - public void testApplyMatch_ReplaceSignature_NormalSourceCustomDest() - throws Exception { + public void testApplyMatch_ReplaceSignature_NormalSourceCustomDest() throws Exception { useMatch("0x00401040", "0x00401040"); @@ -666,9 +663,8 @@ public class VTMatchApplyFunctionSignatureTest extends AbstractGhidraHeadedInteg env.release(destinationProgram); destinationProgram = createToyDestinationProgram();// env.getProgram("helloProgram"); // get a program without cdecl - session = - VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager", - sourceProgram, destinationProgram, this); + session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager", + sourceProgram, destinationProgram, this); runSwing(() -> controller.openVersionTrackingSession(session)); useMatch("0x00401040", "0x00010938"); diff --git a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchApplyTest.java b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchApplyTest.java index c8ffcb69b7..324d8f6ef4 100644 --- a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchApplyTest.java +++ b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchApplyTest.java @@ -94,9 +94,8 @@ public class VTMatchApplyTest extends AbstractGhidraHeadedIntegrationTest { plugin = getPlugin(tool, VTPlugin.class); controller = new VTControllerImpl(plugin); - session = - VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager", - sourceProgram, destinationProgram, this); + session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager", + sourceProgram, destinationProgram, this); runSwing(() -> controller.openVersionTrackingSession(session)); diff --git a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/api/AbstractCorrelatorTest.java b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/api/AbstractCorrelatorTest.java index 9ae0b43285..a23a64a34a 100644 --- a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/api/AbstractCorrelatorTest.java +++ b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/api/AbstractCorrelatorTest.java @@ -79,8 +79,7 @@ public abstract class AbstractCorrelatorTest extends AbstractGhidraHeadedIntegra protected void exerciseFunctionsForFactory(final VTProgramCorrelatorFactory factory, AddressSetView sourceSetThatShouldBeFound) throws Exception { String name = factory.getName(); - VTSession session = - VTSessionDB.createVTSession(name, sourceProgram, destinationProgram, this); + VTSession session = new VTSessionDB(name, sourceProgram, destinationProgram, this); try { int sessionTransaction = session.startTransaction(name); @@ -145,8 +144,7 @@ public abstract class AbstractCorrelatorTest extends AbstractGhidraHeadedIntegra protected void exercisePreciseMatchesForFactory(VTProgramCorrelatorFactory factory, Map map) throws Exception { String name = factory.getName(); - VTSession session = - VTSessionDB.createVTSession(name, sourceProgram, destinationProgram, this); + VTSession session = new VTSessionDB(name, sourceProgram, destinationProgram, this); try { int sessionTransaction = session.startTransaction(name); diff --git a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/api/markupitem/AbstractVTMarkupItemTest.java b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/api/markupitem/AbstractVTMarkupItemTest.java index 4e6bcdfe87..bf98cad7eb 100644 --- a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/api/markupitem/AbstractVTMarkupItemTest.java +++ b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/api/markupitem/AbstractVTMarkupItemTest.java @@ -348,7 +348,7 @@ public abstract class AbstractVTMarkupItemTest extends AbstractGhidraHeadedInteg } protected VTSessionDB createNewSession() throws Exception { - return VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager", + return new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager", sourceProgram, destinationProgram, this); } diff --git a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTBaseTestCase.java b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTBaseTestCase.java index 6b53f155a7..0e58b47074 100644 --- a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTBaseTestCase.java +++ b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTBaseTestCase.java @@ -81,8 +81,7 @@ public class VTBaseTestCase extends AbstractGenericTest { } public VTSessionDB createVTSession() throws IOException { - return VTSessionDB.createVTSession("Test DB", sourceProgram, destinationProgram, - VTTestUtils.class); + return new VTSessionDB("Test DB", sourceProgram, destinationProgram, VTTestUtils.class); } public static int getRandomInt() { diff --git a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/gui/VTTestEnv.java b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/gui/VTTestEnv.java index 5ebbe71619..3604916774 100644 --- a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/gui/VTTestEnv.java +++ b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/gui/VTTestEnv.java @@ -69,7 +69,7 @@ public class VTTestEnv extends TestEnv { sourceProgram = getProgram(sourceProgramName); destinationProgram = getProgram(destinationProgramName); - session = VTSessionDB.createVTSession("Test", sourceProgram, destinationProgram, getTool()); + session = new VTSessionDB("Test", sourceProgram, destinationProgram, getTool()); VTProgramCorrelator correlator = factory.createCorrelator(sourceProgram, sourceProgram.getMemory(), destinationProgram, destinationProgram.getMemory(), null); @@ -111,7 +111,7 @@ public class VTTestEnv extends TestEnv { } private VTSessionDB createAndOpenVTSession() throws IOException { - session = VTSessionDB.createVTSession("Test", sourceProgram, destinationProgram, getTool()); + session = new VTSessionDB("Test", sourceProgram, destinationProgram, getTool()); runSwing(() -> controller.openVersionTrackingSession(session), false); diff --git a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/gui/plugin/StubVTController.java b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/gui/plugin/StubVTController.java index b0b1320518..65d08a2688 100644 --- a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/gui/plugin/StubVTController.java +++ b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/gui/plugin/StubVTController.java @@ -53,7 +53,7 @@ public class StubVTController implements VTController { } @Override - public void openVersionTrackingSession(DomainFile domainFile) { + public boolean openVersionTrackingSession(DomainFile domainFile) { throw new UnsupportedOperationException(); } diff --git a/Ghidra/Framework/DB/src/main/java/db/Transaction.java b/Ghidra/Framework/DB/src/main/java/db/Transaction.java index 892e84f542..f1a48fc24c 100644 --- a/Ghidra/Framework/DB/src/main/java/db/Transaction.java +++ b/Ghidra/Framework/DB/src/main/java/db/Transaction.java @@ -50,8 +50,8 @@ public abstract class Transaction implements AutoCloseable { /** * End this transaction if currently active. - * @param commit true if changes shuold be commited, false if all changes in this transaction - * shuold be discarded (i.e., rollback). If this is a "sub-transaction" and commit is false, + * @param commit true if changes should be commited, false if all changes in this transaction + * should be discarded (i.e., rollback). If this is a "sub-transaction" and commit is false, * the larger transaction will rollback upon completion. * @return true if changes have been commited or false if nothing to commit or commit parameter * was specified as false. @@ -115,5 +115,5 @@ public abstract class Transaction implements AutoCloseable { endTransaction(commit); } } - + } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java index 2b75e7162a..bbaff1ff36 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java @@ -60,7 +60,7 @@ public class DomainFileProxy implements DomainFile { } DomainFileProxy(String name, String parentPath, DomainObjectAdapter doa, int version, - String fileID, ProjectLocator projectLocation) { + String fileID, ProjectLocator projectLocation) throws IOException { this(name, doa); this.parentPath = parentPath; diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java index e82ed39f0e..b3d0bd6a42 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java @@ -94,7 +94,6 @@ public abstract class DomainObjectAdapter implements DomainObject { consumers = new ArrayList(); consumers.add(consumer); if (!UserData.class.isAssignableFrom(getClass())) { - // UserData instances do not utilize DomainFile storage domainFile = new DomainFileProxy(name, this); } } @@ -185,7 +184,12 @@ public abstract class DomainObjectAdapter implements DomainObject { return temporary; } - protected void setDomainFile(DomainFile df) { + /** + * Set the {@link DomainFile} associated with this instance. + * @param df domain file + * @throws DomainObjectException if a severe failure occurs during the operation. + */ + protected void setDomainFile(DomainFile df) throws DomainObjectException { if (df == null) { throw new IllegalArgumentException("DomainFile must not be null"); } @@ -197,7 +201,6 @@ public abstract class DomainObjectAdapter implements DomainObject { domainFile = df; fireEvent(new DomainObjectChangeRecord(DomainObjectEvent.FILE_CHANGED, oldDf, df)); fileChangeListeners.invoke().domainFileChanged(this); - } protected void close() { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java index 21be6cf89b..2f06823cf5 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java @@ -528,6 +528,9 @@ public class GhidraFileData { projectData.clearDomainObject(getPathname()); // generate IOException Throwable cause = e.getCause(); + if (cause == null) { + cause = e; + } if (cause instanceof IOException) { throw (IOException) cause; } @@ -831,9 +834,12 @@ public class GhidraFileData { } /** - * Returns whether the object is read-only. From a framework point of view a read-only object - * can never be changed. - * @return true if read-only + * Returns whether this file is explicitly marked as read-only. This method is only supported + * by the local file system and does not apply to a versioned file that is not checked-out. + * A versioned file that is not checked-out will always return false, while a + * {@link DomainFileProxy} will always return true. + * From a framework point of view a read-only file can never be changed. + * @return true if this file is marked read-only */ boolean isReadOnly() { synchronized (fileSystem) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java index 8057272720..a6aaf0a3a3 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java @@ -158,7 +158,6 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener { toolFrame.addWindowListener(windowListener); AppInfo.setFrontEndTool(this); - AppInfo.setActiveProject(getProject()); initFrontEndOptions(); } @@ -408,7 +407,6 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener { configureToolAction.setEnabled(true); setProject(project); - AppInfo.setActiveProject(project); plugin.setActiveProject(project); firePluginEvent(new ProjectPluginEvent(getClass().getSimpleName(), project)); } @@ -616,7 +614,6 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener { // Treat setVisible(false) as a dispose, as this is the only time we should be hidden AppInfo.setFrontEndTool(null); - AppInfo.setActiveProject(null); dispose(); } } @@ -645,9 +642,8 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener { return isConfigurable(); } }; - MenuData menuData = - new MenuData(new String[] { ToolConstants.MENU_FILE, "Install Extensions" }, null, - CONFIGURE_GROUP); + MenuData menuData = new MenuData( + new String[] { ToolConstants.MENU_FILE, "Install Extensions" }, null, CONFIGURE_GROUP); menuData.setMenuSubGroup(CONFIGURE_GROUP + 2); installExtensionsAction.setMenuBarData(menuData); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java index 3f590db7c3..d2c5fa2016 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java @@ -331,9 +331,12 @@ public interface DomainFile extends Comparable { public void setReadOnly(boolean state) throws IOException; /** - * Returns whether the object is read-only. From a framework point of view a read-only object - * can never be changed. - * @return true if read-only + * Returns whether this file is explicitly marked as read-only. This method is only supported + * by the local file system and does not apply to a versioned file that is not checked-out. + * A versioned file that is not checked-out will always return false, while a + * {@link DomainFileProxy} will always return true. + * From a framework point of view a read-only file can never be changed. + * @return true if this file is marked read-only */ public boolean isReadOnly(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java index 064f7c7244..e0d7f24541 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java @@ -27,6 +27,7 @@ import org.jdom.output.XMLOutputter; import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.data.DefaultProjectData; import ghidra.framework.data.TransientDataManager; +import ghidra.framework.main.AppInfo; import ghidra.framework.model.*; import ghidra.framework.options.SaveState; import ghidra.framework.project.tool.GhidraToolTemplate; @@ -291,16 +292,16 @@ public class DefaultProject implements Project { throw new IOException("Invalid Ghidra URL specified: " + url); } - ProjectData projectData = otherViewsMap.get(url); - if (projectData == null) { - projectData = openProjectView(url); + ProjectData viewedProjectData = otherViewsMap.get(url); + if (viewedProjectData == null) { + viewedProjectData = openProjectView(url); } - if (projectData != null && visible && visibleViews.add(url)) { + if (viewedProjectData != null && visible && visibleViews.add(url)) { notifyVisibleViewAdded(url); } - return projectData; + return viewedProjectData; } } @@ -378,6 +379,11 @@ public class DefaultProject implements Project { synchronized (otherViewsMap) { isClosed = true; + // Clear active project if this is the current active project. + if (AppInfo.getActiveProject() == this) { + AppInfo.setActiveProject(null); + } + for (DefaultProjectData dataMgr : otherViewsMap.values()) { if (dataMgr != null) { dataMgr.close(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProjectManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProjectManager.java index 557b3fcc59..4a02e26917 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProjectManager.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProjectManager.java @@ -28,6 +28,7 @@ import ghidra.framework.GenericRunInfo; import ghidra.framework.ToolUtils; import ghidra.framework.client.*; import ghidra.framework.data.TransientDataManager; +import ghidra.framework.main.AppInfo; import ghidra.framework.model.*; import ghidra.framework.preferences.Preferences; import ghidra.framework.protocol.ghidra.GhidraURL; @@ -111,6 +112,8 @@ public class DefaultProjectManager implements ProjectManager { lastOpenedProject = projectLocator; updatePreferences(); } + + AppInfo.setActiveProject(currentProject); return currentProject; } @@ -138,6 +141,7 @@ public class DefaultProjectManager implements ProjectManager { try { currentProject = new DefaultProject(this, projectLocator, resetOwner); + AppInfo.setActiveProject(currentProject); if (doRestore) { currentProject.restore(); } @@ -166,6 +170,7 @@ public class DefaultProjectManager implements ProjectManager { } } } + AppInfo.setActiveProject(null); return null; }