From 514c04906b4be7e30d997bc23e4bb20cdfda9edf Mon Sep 17 00:00:00 2001 From: ghidragon <106987263+ghidragon@users.noreply.github.com> Date: Tue, 5 Dec 2023 18:33:17 -0500 Subject: [PATCH] GP-4040 added ability for scripts to open programs that need to be upgraded, with ask options --- .../AskValuesExampleScript.java | 2 +- .../java/ghidra/app/script/GhidraScript.java | 91 +++++++++++++++++-- .../features/base/values/GhidraValuesMap.java | 14 ++- .../base/values/ProgramFileValue.java | 35 +++++-- .../base/values/ProgramFileValueTest.java | 20 ++-- .../AutoVersionTrackingScript.java | 23 ++--- 6 files changed, 142 insertions(+), 43 deletions(-) diff --git a/Ghidra/Features/Base/ghidra_scripts/AskValuesExampleScript.java b/Ghidra/Features/Base/ghidra_scripts/AskValuesExampleScript.java index ab90f04894..f6d24e929a 100644 --- a/Ghidra/Features/Base/ghidra_scripts/AskValuesExampleScript.java +++ b/Ghidra/Features/Base/ghidra_scripts/AskValuesExampleScript.java @@ -75,7 +75,7 @@ public class AskValuesExampleScript extends GhidraScript { // show up in the tool and when you release the consumer, it will be closed. // NOTE: if you call getProgram() more than once, the consumer will be added multiple times // and you must release it multiple times - Program program = values.getProgram("Other Program", this, state.getTool()); + Program program = values.getProgram("Other Program", this, state.getTool(), true); println("Name = " + name); println("Count = " + age); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java index 8ca84a937c..605edbd772 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java @@ -2732,7 +2732,9 @@ public abstract class GhidraScript extends FlatProgramAPI { /** * Returns a Program, using the title parameter for guidance. The actual behavior of the - * method depends on your environment, which can be GUI or headless. + * method depends on your environment, which can be GUI or headless. If in headless mode, + * the program will not be upgraded (see {@link #askProgram(String, boolean)} if you want + * more control). In GUI mode, the user will be prompted to upgrade. *
* Regardless of environment -- if script arguments have been set, this method will use the * next argument in the array and advance the array index so the next call to an ask method @@ -2757,20 +2759,75 @@ public abstract class GhidraScript extends FlatProgramAPI { * * @param title the title of the pop-up dialog (in GUI mode) or the variable name (in * headless mode) - * @return the user-selected Program with this script as the consumer or null if a program was - * not selected. NOTE: It is very important that the program instance returned by this method - * ALWAYS be properly released when no longer needed. The script which invoked this method must be + * @return the user-selected Program with this script as the consumer if a program was + * selected. Null is returned if a program is not selected. NOTE: It is very important that + * the program instance returned by this method ALWAYS be properly released when no longer + * needed. The script which invoked this method must be * specified as the consumer upon release (i.e., {@code program.release(this) } - failure to * properly release the program may result in improper project disposal. If the program was * opened by the tool, the tool will be a second consumer responsible for its own release. - * @throws VersionException if the Program is out-of-date from the version of GHIDRA + * @throws VersionException if the Program is out-of-date from the version of Ghidra and an + * upgrade was not been performed. In non-headless mode, the user will have already been + * notified via a popup dialog. * @throws IOException if there is an error accessing the Program's DomainObject - * @throws CancelledException if the operation is cancelled + * @throws CancelledException if the program open operation is cancelled * @throws IllegalArgumentException if in headless mode, there was a missing or invalid program * specified in the .properties file */ public Program askProgram(String title) throws VersionException, IOException, CancelledException { + return askProgram(title, false); + } + + /** + * Returns a Program, using the title parameter for guidance with the option to upgrade + * if needed. The actual behavior of the method depends on your environment, which can be + * GUI or headless. You can control whether or not the program is allowed to upgrade via + * the {@code upgradeIfNeeded} parameter. + *
+ * Regardless of environment -- if script arguments have been set, this method will use the + * next argument in the array and advance the array index so the next call to an ask method + * will get the next argument. If there are no script arguments and a .properties file + * sharing the same base name as the Ghidra Script exists (i.e., Script1.properties for + * Script1.java), then this method will then look there for the String value to return. + * The method will look in the .properties file by searching for a property name that is the + * title String parameter. If that property name exists and its value represents a valid + * program, then the .properties value will be used in the following way: + *
    + *
  1. In the GUI environment, this method displays a popup dialog that prompts the user + * to select a program.
  2. + *
  3. In the headless environment, if a .properties file sharing the same base name as the + * Ghidra Script exists (i.e., Script1.properties for Script1.java), then this method + * looks there for the name of the program to return. The method will look in the + * .properties file by searching for a property name equal to the 'title' parameter. If + * that property name exists and its value represents a valid Program in the project, + * then that value is returned. Otherwise, an Exception is thrown if there is an + * invalid or missing .properties value.
  4. + *
+ * + * + * @param title the title of the pop-up dialog (in GUI mode) or the variable name (in + * headless mode) + * @param upgradeIfNeeded if true, program will be upgraded if needed and possible. If false, + * the program will only be upgraded after first prompting the user. In headless mode, it will + * attempt to upgrade only if the parameter is true. + * @return the user-selected Program with this script as the consumer if a program was + * selected. Null is returned if a program is not selected. NOTE: It is very important that + * the program instance returned by this method ALWAYS be properly released when no longer + * needed. The script which invoked this method must be + * specified as the consumer upon release (i.e., {@code program.release(this) } - failure to + * properly release the program may result in improper project disposal. If the program was + * opened by the tool, the tool will be a second consumer responsible for its own release. + * @throws VersionException if the Program is out-of-date from the version of GHIDRA and an + * upgrade was not been performed. In non-headless mode, the user will have already been + * notified via a popup dialog. + * @throws IOException if there is an error accessing the Program's DomainObject + * @throws CancelledException if the program open operation is cancelled + * @throws IllegalArgumentException if in headless mode, there was a missing or invalid program + * specified in the .properties file + */ + public Program askProgram(String title, boolean upgradeIfNeeded) + throws VersionException, IOException, CancelledException { DomainFile choice = loadAskValue(this::parseDomainFile, title); if (!isRunningHeadless()) { @@ -2779,7 +2836,7 @@ public abstract class GhidraScript extends FlatProgramAPI { DataTreeDialog dtd = new DataTreeDialog(null, title, DataTreeDialog.OPEN); dtd.show(); if (dtd.wasCancelled()) { - throw new CancelledException(); + return null; } return dtd.getDomainFile(); @@ -2790,7 +2847,7 @@ public abstract class GhidraScript extends FlatProgramAPI { return null; } - Program p = (Program) choice.getDomainObject(this, false, false, monitor); + Program p = doOpenProgram(choice, upgradeIfNeeded); PluginTool tool = state.getTool(); if (tool == null) { @@ -2802,6 +2859,24 @@ public abstract class GhidraScript extends FlatProgramAPI { return p; } + private Program doOpenProgram(DomainFile domainFile, boolean upgradeIfNeeded) + throws CancelledException, IOException, VersionException { + + try { + return (Program) domainFile.getDomainObject(this, upgradeIfNeeded, false, monitor); + } + catch (VersionException e) { + if (isRunningHeadless()) { + throw e; + } + // in Gui mode, ask the user if they would like to upgrade + if (VersionExceptionHandler.isUpgradeOK(null, domainFile, "Open ", e)) { + return (Program) domainFile.getDomainObject(this, true, false, monitor); + } + throw e; + } + } + /** * Parses a DomainFile from a string. * diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/GhidraValuesMap.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/GhidraValuesMap.java index b35d0c5192..a536e03ba1 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/GhidraValuesMap.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/GhidraValuesMap.java @@ -209,17 +209,23 @@ public class GhidraValuesMap extends GValuesMap { * @param consumer the consumer to be used to open the program * @param tool if non-null, the program will also be opened in the given tool. Note: the * program will only be added to the tool once even if this method is called multiple times. - * @return the project folder value - * @throws VersionException if the Program being opened is an older version than the + * @param upgradeIfNeeded if true, program will be upgraded if needed and possible. If false, + * the program will only be upgraded after first prompting the user. In headless mode, it will + * attempt to upgrade only if the parameter is true. + * @return an opened program with the given consumer for the selected domain file or null if + * no program was selected. + * @throws VersionException if the Program is out-of-date from the version of GHIDRA and an + * upgrade was not been performed. In non-headless mode, the user will have already been + * notified via a popup dialog. * current Ghidra Program version. * @throws IOException if there is an error accessing the Program's DomainObject * @throws CancelledException if the operation is cancelled * @throws IllegalArgumentException if the name hasn't been defined as a project folder type */ - public Program getProgram(String name, Object consumer, Tool tool) + public Program getProgram(String name, Object consumer, Tool tool, boolean upgradeIfNeeded) throws VersionException, IOException, CancelledException { ProgramFileValue programFileValue = getValue(name, ProgramFileValue.class, "Program"); - return programFileValue.openProgram(consumer, tool, monitor); + return programFileValue.openProgram(consumer, tool, upgradeIfNeeded, monitor); } /** diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProgramFileValue.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProgramFileValue.java index 1a7d455699..f5fb158461 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProgramFileValue.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProgramFileValue.java @@ -27,6 +27,8 @@ import ghidra.framework.main.DataTreeDialog; import ghidra.framework.model.DomainFile; import ghidra.framework.model.Project; import ghidra.program.model.listing.Program; +import ghidra.util.SystemUtilities; +import ghidra.util.VersionExceptionHandler; import ghidra.util.exception.CancelledException; import ghidra.util.exception.VersionException; import ghidra.util.task.TaskMonitor; @@ -89,23 +91,27 @@ public class ProgramFileValue extends ProjectFileValue { *

* @param consumer the consumer to be used to open the program * @param tool optional tool that if non-null, the program will also be opened in the tool + * @param upgradeIfNeeded if true, program will be upgraded if needed and possible. If false, + * the program will only be upgraded after first prompting the user. In headless mode, it will + * attempt to upgrade only if the parameter is true. * @param monitor task monitor for cancelling the open program. - * @return a program for the current program value value. If the current program file value - * is null, then null will be returned. - * @throws VersionException if the Program being opened is an older version than the + * @return a program for the currently selected program file. If no file chosen, returns null + * @throws VersionException if the Program is out-of-date from the version of GHIDRA and an + * upgrade was not been performed. In non-headless mode, the user will have already been + * notified via a popup dialog. * current Ghidra Program version. * @throws IOException if there is an error accessing the Program's DomainObject * @throws CancelledException if the operation is cancelled */ - public Program openProgram(Object consumer, Tool tool, TaskMonitor monitor) - throws VersionException, IOException, CancelledException { + public Program openProgram(Object consumer, Tool tool, boolean upgradeIfNeeded, + TaskMonitor monitor) throws VersionException, IOException, CancelledException { DomainFile domainFile = getValue(); if (domainFile == null) { return null; } - Program program = (Program) domainFile.getDomainObject(consumer, true, false, monitor); + Program program = doOpenProgram(domainFile, consumer, upgradeIfNeeded, monitor); if (tool != null && program != null) { tool.getService(ProgramManager.class).openProgram(program); @@ -113,4 +119,21 @@ public class ProgramFileValue extends ProjectFileValue { return program; } + private Program doOpenProgram(DomainFile domainFile, Object consumer, boolean upgradeIfNeeded, + TaskMonitor monitor) throws VersionException, IOException, CancelledException { + + try { + return (Program) domainFile.getDomainObject(consumer, upgradeIfNeeded, false, monitor); + } + catch (VersionException e) { + if (SystemUtilities.isInHeadlessMode()) { + throw e; + } + if (VersionExceptionHandler.isUpgradeOK(null, domainFile, "Open ", e)) { + return (Program) domainFile.getDomainObject(consumer, true, false, monitor); + } + throw e; + } + } + } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProgramFileValueTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProgramFileValueTest.java index 3ec8026472..258d81f248 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProgramFileValueTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProgramFileValueTest.java @@ -36,7 +36,7 @@ public class ProgramFileValueTest extends AbstractValueIntegrationTest { values.setProgram(NAME, programA); assertTrue(values.hasValue(NAME)); - assertEquals(programA, values.getProgram(NAME, this, null)); + assertEquals(programA, values.getProgram(NAME, this, null, true)); } @Test @@ -46,12 +46,12 @@ public class ProgramFileValueTest extends AbstractValueIntegrationTest { assertTrue(values.isDefined(NAME)); assertTrue(values.hasValue(NAME)); - assertEquals(programA, values.getProgram(NAME, this, null)); + assertEquals(programA, values.getProgram(NAME, this, null, true)); values.setProgram(NAME, programB); assertTrue(values.hasValue(NAME)); - assertEquals(programB, values.getProgram(NAME, this, null)); + assertEquals(programB, values.getProgram(NAME, this, null, true)); values.setProgram(NAME, null); assertFalse(values.hasValue(NAME)); @@ -96,7 +96,7 @@ public class ProgramFileValueTest extends AbstractValueIntegrationTest { pressOk(); assertFalse(values.hasValue(NAME)); - assertNull(values.getProgram(NAME, this, null)); + assertNull(values.getProgram(NAME, this, null, true)); } @Test @@ -109,7 +109,7 @@ public class ProgramFileValueTest extends AbstractValueIntegrationTest { pressOk(); assertTrue(values.hasValue(NAME)); - assertEquals(programA, values.getProgram(NAME, this, null)); + assertEquals(programA, values.getProgram(NAME, this, null, true)); } @Test @@ -121,7 +121,7 @@ public class ProgramFileValueTest extends AbstractValueIntegrationTest { pressOk(); assertTrue(values.hasValue(NAME)); - assertEquals(programA, values.getProgram(NAME, this, null)); + assertEquals(programA, values.getProgram(NAME, this, null, true)); } @Test @@ -134,7 +134,7 @@ public class ProgramFileValueTest extends AbstractValueIntegrationTest { pressOk(); assertTrue(values.hasValue(NAME)); - assertEquals(programB, values.getProgram(NAME, this, null)); + assertEquals(programB, values.getProgram(NAME, this, null, true)); } @Test @@ -149,7 +149,7 @@ public class ProgramFileValueTest extends AbstractValueIntegrationTest { setProjectFileOnProjectTree(values.getAbstractValue(NAME), programA.getDomainFile()); pressOk(); - Program p = values.getProgram(NAME, this, tool); + Program p = values.getProgram(NAME, this, tool, true); allOpenPrograms = programManagerService.getAllOpenPrograms(); assertEquals(1, allOpenPrograms.length); @@ -164,9 +164,9 @@ public class ProgramFileValueTest extends AbstractValueIntegrationTest { setProjectFileOnProjectTree(values.getAbstractValue(NAME), programA.getDomainFile()); pressOk(); - Program p1 = values.getProgram(NAME, this, null); + Program p1 = values.getProgram(NAME, this, null, true); assertEquals(2, programA.getConsumerList().size()); - Program p2 = values.getProgram(NAME, this, null); + Program p2 = values.getProgram(NAME, this, null, true); assertEquals(p1, p2); assertEquals(3, programA.getConsumerList().size()); p1.release(this); diff --git a/Ghidra/Features/VersionTracking/ghidra_scripts/AutoVersionTrackingScript.java b/Ghidra/Features/VersionTracking/ghidra_scripts/AutoVersionTrackingScript.java index 8934e5fb00..72241ced58 100644 --- a/Ghidra/Features/VersionTracking/ghidra_scripts/AutoVersionTrackingScript.java +++ b/Ghidra/Features/VersionTracking/ghidra_scripts/AutoVersionTrackingScript.java @@ -115,17 +115,19 @@ public class AutoVersionTrackingScript extends GhidraScript { }); startupValues = askValues("Enter Auto Version Tracking Information", - "Changing these options will not change the corresponding tool options", - startupValues); + "Changing these options will not change the corresponding tool options", startupValues); DomainFolder folder = startupValues.getProjectFolder("Version Tracking Session Folder"); String name = startupValues.getString("Version Tracking Session Name"); boolean isCurrentProgramSourceProg = startupValues.getBoolean("Check if current program is the Source Program"); - Program otherProgram = - startupValues.getProgram("Please select the other program", this, state.getTool()); + // setting auto upgrade to isHeadless, will cause headless uses to auto upgrade, but in + // Gui mode, will prompt before upgrading. + boolean autoUpgradeIfNeeded = isRunningHeadless(); + Program otherProgram = startupValues.getProgram("Please select the other program", this, + state.getTool(), autoUpgradeIfNeeded); if (isCurrentProgramSourceProg) { sourceProgram = currentProgram; @@ -140,15 +142,12 @@ public class AutoVersionTrackingScript extends GhidraScript { 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); } @@ -157,7 +156,7 @@ public class AutoVersionTrackingScript extends GhidraScript { GhidraValuesMap optionsMap = createDefaultOptions(); // if running script in GUI get options from user and update the vtOptions with them - if(!isRunningHeadless()) { + if (!isRunningHeadless()) { optionsMap = getOptionsFromUser(); } @@ -177,12 +176,10 @@ public class AutoVersionTrackingScript extends GhidraScript { ToolOptions vtOptions = setToolOptionsFromOptionsMap(optionsMap); - AutoVersionTrackingTask autoVtTask = - new AutoVersionTrackingTask(session, vtOptions); + AutoVersionTrackingTask autoVtTask = new AutoVersionTrackingTask(session, vtOptions); TaskLauncher.launch(autoVtTask); - // 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. @@ -191,7 +188,6 @@ public class AutoVersionTrackingScript extends GhidraScript { session.save(); } - println(autoVtTask.getStatusMsg()); otherProgram.release(this); } @@ -257,8 +253,7 @@ public class AutoVersionTrackingScript extends GhidraScript { GhidraValuesMap optionsValues = createDefaultOptions(); optionsValues = askValues("Enter Auto Version Tracking Options", - "These options will not be saved to your current tool options.", - optionsValues); + "These options will not be saved to your current tool options.", optionsValues); return optionsValues; }