GP-4294 - Fixed exception looking for extensions when running headlessly in fat jar mode

This commit is contained in:
dragonmacher 2024-02-06 17:01:02 -05:00
parent dffb5fd859
commit 359faba77a
3 changed files with 49 additions and 35 deletions

View file

@ -62,9 +62,12 @@ public class BuildGhidraJarScript extends GhidraScript {
builder.addExcludedFileExtension(".pdf"); builder.addExcludedFileExtension(".pdf");
File installDir = Application.getInstallationDirectory().getFile(true); File installDir = Application.getInstallationDirectory().getFile(true);
builder.buildJar(new File(installDir, "ghidra.jar"), null, monitor); File file = new File(installDir, "ghidra.jar");
builder.buildJar(file, null, monitor);
// uncomment the following line to create a src zip for debugging. // uncomment the following line to create a src zip for debugging.
// builder.buildSrcZip(new File(installDir, "GhidraSrc.zip"), monitor); // builder.buildSrcZip(new File(installDir, "GhidraSrc.zip"), monitor);
println("Finsished writing jar: " + file);
} }
} }

View file

@ -59,16 +59,16 @@ import ghidra.util.task.TaskMonitor;
import utilities.util.FileUtilities; import utilities.util.FileUtilities;
/** /**
* The class used kick-off and interact with headless processing. All headless options have been * The class used kick-off and interact with headless processing. All headless options have been
* broken out into their own class: {@link HeadlessOptions}. This class is intended to be used * broken out into their own class: {@link HeadlessOptions}. This class is intended to be used
* one of two ways: * one of two ways:
* <ul> * <ul>
* <li>Used by {@link AnalyzeHeadless} to perform headless analysis based on arguments specified * <li>Used by {@link AnalyzeHeadless} to perform headless analysis based on arguments specified
* on the command line.</li> * on the command line.</li>
* <li>Used by another tool as a library to perform headless analysis.</li> * <li>Used by another tool as a library to perform headless analysis.</li>
* </ul> * </ul>
* <p> * <p>
* Note: This class is not thread safe. * Note: This class is not thread safe.
*/ */
public class HeadlessAnalyzer { public class HeadlessAnalyzer {
@ -84,16 +84,16 @@ public class HeadlessAnalyzer {
private FileSystemService fsService; private FileSystemService fsService;
/** /**
* Gets a headless analyzer, initializing the application if necessary with the specified * Gets a headless analyzer, initializing the application if necessary with the specified
* logging parameters. An {@link IllegalStateException} will be thrown if the application has * logging parameters. An {@link IllegalStateException} will be thrown if the application has
* already been initialized or a headless analyzer has already been retrieved. In these cases, * already been initialized or a headless analyzer has already been retrieved. In these cases,
* the headless analyzer should be gotten with {@link HeadlessAnalyzer#getInstance()}. * the headless analyzer should be gotten with {@link HeadlessAnalyzer#getInstance()}.
* *
* @param logFile The desired application log file. If null, the default application log file * @param logFile The desired application log file. If null, the default application log file
* will be used (see {@link Application#initializeLogging}). * will be used (see {@link Application#initializeLogging}).
* @param scriptLogFile The desired scripting log file. If null, the default scripting log file * @param scriptLogFile The desired scripting log file. If null, the default scripting log file
* will be used (see {@link Application#initializeLogging}). * will be used (see {@link Application#initializeLogging}).
* @param useLog4j true if log4j is to be used; otherwise, false. If this class is being used by * @param useLog4j true if log4j is to be used; otherwise, false. If this class is being used by
* another tool as a library, using log4j might interfere with that tool. * another tool as a library, using log4j might interfere with that tool.
* @return An instance of a new headless analyzer. * @return An instance of a new headless analyzer.
* @throws IllegalStateException if an application or headless analyzer instance has already been initialized. * @throws IllegalStateException if an application or headless analyzer instance has already been initialized.
@ -103,7 +103,7 @@ public class HeadlessAnalyzer {
boolean useLog4j) throws IllegalStateException, IOException { boolean useLog4j) throws IllegalStateException, IOException {
// Prevent more than one headless analyzer from being instantiated. Too much about it // Prevent more than one headless analyzer from being instantiated. Too much about it
// messes with global system settings, so under the current design of Ghidra, allowing // messes with global system settings, so under the current design of Ghidra, allowing
// more than one to exist could result in unpredictable behavior. // more than one to exist could result in unpredictable behavior.
if (instance != null) { if (instance != null) {
throw new IllegalStateException( throw new IllegalStateException(
@ -141,7 +141,7 @@ public class HeadlessAnalyzer {
/** /**
* Gets a headless analyzer instance, with the assumption that the application has already been * Gets a headless analyzer instance, with the assumption that the application has already been
* initialized. If this is called before the application has been initialized, it will * initialized. If this is called before the application has been initialized, it will
* initialize the application with no logging. * initialize the application with no logging.
* *
* @return An instance of a new headless analyzer. * @return An instance of a new headless analyzer.
@ -151,7 +151,7 @@ public class HeadlessAnalyzer {
public static HeadlessAnalyzer getInstance() throws IOException { public static HeadlessAnalyzer getInstance() throws IOException {
// Prevent more than one headless analyzer from being instantiated. Too much about it // Prevent more than one headless analyzer from being instantiated. Too much about it
// messes with global system settings, so under the current design of Ghidra, allowing // messes with global system settings, so under the current design of Ghidra, allowing
// more than one to exist could result in unpredictable behavior. // more than one to exist could result in unpredictable behavior.
if (instance != null) { if (instance != null) {
return instance; return instance;
@ -185,8 +185,10 @@ public class HeadlessAnalyzer {
layout = new GhidraApplicationLayout(); layout = new GhidraApplicationLayout();
} }
catch (IOException e) { catch (IOException e) {
Msg.debug(HeadlessAnalyzer.class,
"Unable to load the standard Ghidra application layout. " + e.getMessage() +
". Attempting to load the Ghidra Jar application layout.");
layout = new GhidraJarApplicationLayout(); layout = new GhidraJarApplicationLayout();
} }
return layout; return layout;
} }
@ -201,7 +203,7 @@ public class HeadlessAnalyzer {
// Ghidra URL handler registration. There's no harm in doing this more than once. // Ghidra URL handler registration. There's no harm in doing this more than once.
Handler.registerHandler(); Handler.registerHandler();
// Ensure that we are running in "headless mode", preventing Swing-based methods from // Ensure that we are running in "headless mode", preventing Swing-based methods from
// running (causing headless operation to lose focus). // running (causing headless operation to lose focus).
System.setProperty("java.awt.headless", "true"); System.setProperty("java.awt.headless", "true");
System.setProperty(SystemUtilities.HEADLESS_PROPERTY, Boolean.TRUE.toString()); System.setProperty(SystemUtilities.HEADLESS_PROPERTY, Boolean.TRUE.toString());
@ -244,12 +246,12 @@ public class HeadlessAnalyzer {
* <li>perform auto-analysis if not disabled</li> * <li>perform auto-analysis if not disabled</li>
* <li>execute ordered list of post-scripts</li> * <li>execute ordered list of post-scripts</li>
* </ol> * </ol>
* If no import files or directories have been specified the ordered list * If no import files or directories have been specified the ordered list
* of pre/post scripts will be executed once. * of pre/post scripts will be executed once.
* *
* @param ghidraURL ghidra URL for existing server repository and optional * @param ghidraURL ghidra URL for existing server repository and optional
* folder path * folder path
* @param filesToImport directories and files to be imported (null or empty * @param filesToImport directories and files to be imported (null or empty
* is acceptable if we are in -process mode) * is acceptable if we are in -process mode)
* @throws IOException if there was an IO-related problem * @throws IOException if there was an IO-related problem
* @throws MalformedURLException specified URL is invalid * @throws MalformedURLException specified URL is invalid
@ -370,16 +372,16 @@ public class HeadlessAnalyzer {
* <li>perform auto-analysis if not disabled</li> * <li>perform auto-analysis if not disabled</li>
* <li>execute ordered list of post-scripts</li> * <li>execute ordered list of post-scripts</li>
* </ol> * </ol>
* If no import files or directories have been specified the ordered list * If no import files or directories have been specified the ordered list
* of pre/post scripts will be executed once. * of pre/post scripts will be executed once.
* *
* @param projectLocation directory path of project * @param projectLocation directory path of project
* If project exists it will be opened, otherwise it will be created. * If project exists it will be opened, otherwise it will be created.
* @param projectName project name * @param projectName project name
* @param rootFolderPath root folder for imports * @param rootFolderPath root folder for imports
* @param filesToImport directories and files to be imported (null or empty is acceptable if * @param filesToImport directories and files to be imported (null or empty is acceptable if
* we are in -process mode) * we are in -process mode)
* @throws IOException if there was an IO-related problem. If caused by a failure to obtain a * @throws IOException if there was an IO-related problem. If caused by a failure to obtain a
* write-lock on the project the exception cause will a {@code LockException}. * write-lock on the project the exception cause will a {@code LockException}.
*/ */
public void processLocal(String projectLocation, String projectName, String rootFolderPath, public void processLocal(String projectLocation, String projectName, String rootFolderPath,
@ -475,7 +477,7 @@ public class HeadlessAnalyzer {
/** /**
* Checks to see if the most recent analysis timed out. * Checks to see if the most recent analysis timed out.
* *
* @return true if the most recent analysis timed out; otherwise, false. * @return true if the most recent analysis timed out; otherwise, false.
*/ */
public boolean checkAnalysisTimedOut() { public boolean checkAnalysisTimedOut() {
return analysisTimedOut; return analysisTimedOut;
@ -766,7 +768,7 @@ public class HeadlessAnalyzer {
Class<?> c = Class.forName(className, true, classLoaderForDotClassScripts); Class<?> c = Class.forName(className, true, classLoaderForDotClassScripts);
if (GhidraScript.class.isAssignableFrom(c)) { if (GhidraScript.class.isAssignableFrom(c)) {
// No issues, but return null, which signifies we don't actually have a // No issues, but return null, which signifies we don't actually have a
// ResourceFile to associate with the script name // ResourceFile to associate with the script name
return null; return null;
} }
@ -962,9 +964,9 @@ public class HeadlessAnalyzer {
* @param fileAbsolutePath Path of the file to analyze. * @param fileAbsolutePath Path of the file to analyze.
* @param program The program to analyze. * @param program The program to analyze.
* @return true if the program file should be kept. If analysis or scripts have marked * @return true if the program file should be kept. If analysis or scripts have marked
* the program as temporary changes should not be saved. Returns false in * the program as temporary changes should not be saved. Returns false in
* these cases: * these cases:
* - One of the scripts sets the Headless Continuation Option to "ABORT_AND_DELETE" or * - One of the scripts sets the Headless Continuation Option to "ABORT_AND_DELETE" or
* "CONTINUE_THEN_DELETE". * "CONTINUE_THEN_DELETE".
*/ */
private boolean analyzeProgram(String fileAbsolutePath, Program program) { private boolean analyzeProgram(String fileAbsolutePath, Program program) {
@ -1154,7 +1156,7 @@ public class HeadlessAnalyzer {
Msg.info(this, "REPORT: Processing project file: " + domFile.getPathname()); Msg.info(this, "REPORT: Processing project file: " + domFile.getPathname());
// This method already takes into account whether the user has set the "noanalysis" // This method already takes into account whether the user has set the "noanalysis"
// flag or not // flag or not
keepFile = analyzeProgram(domFile.getPathname(), program) || readOnlyFile; keepFile = analyzeProgram(domFile.getPathname(), program) || readOnlyFile;
@ -1237,7 +1239,7 @@ public class HeadlessAnalyzer {
if (!readOnlyFile) { // can't change anything if read-only file if (!readOnlyFile) { // can't change anything if read-only file
// Undo checkout of it is still checked-out and either the file is to be // Undo checkout of it is still checked-out and either the file is to be
// deleted, or we just checked it out and file changes have been committed // deleted, or we just checked it out and file changes have been committed
if (domFile.isCheckedOut()) { if (domFile.isCheckedOut()) {
if (!keepFile || if (!keepFile ||
@ -1521,14 +1523,14 @@ public class HeadlessAnalyzer {
try { try {
// Perform the load. Note that loading 1 file may result in more than 1 thing getting // Perform the load. Note that loading 1 file may result in more than 1 thing getting
// loaded. // loaded.
loadResults = loadPrograms(fsrl, folderPath); loadResults = loadPrograms(fsrl, folderPath);
Msg.info(this, "IMPORTING: Loaded " + (loadResults.size() - 1) + " additional files"); Msg.info(this, "IMPORTING: Loaded " + (loadResults.size() - 1) + " additional files");
primary = loadResults.getPrimary(); primary = loadResults.getPrimary();
Program primaryProgram = primary.getDomainObject(); Program primaryProgram = primary.getDomainObject();
// Make sure we are allowed to save ALL programs to the project. If not, save none and // Make sure we are allowed to save ALL programs to the project. If not, save none and
// fail. // fail.
if (!options.readOnly) { if (!options.readOnly) {
for (Loaded<Program> loaded : loadResults) { for (Loaded<Program> loaded : loadResults) {
@ -1549,7 +1551,7 @@ public class HeadlessAnalyzer {
// TODO: Analyze non-primary programs (GP-2965). // TODO: Analyze non-primary programs (GP-2965).
boolean doSave = analyzeProgram(fsrl.toString(), primaryProgram) && !options.readOnly; boolean doSave = analyzeProgram(fsrl.toString(), primaryProgram) && !options.readOnly;
// The act of marking the program as temporary by a script will signal // The act of marking the program as temporary by a script will signal
// us to discard any changes // us to discard any changes
if (!doSave) { if (!doSave) {
loadResults.forEach(e -> e.getDomainObject().setTemporary(true)); loadResults.forEach(e -> e.getDomainObject().setTemporary(true));
@ -1776,7 +1778,7 @@ public class HeadlessAnalyzer {
return; return;
default: default:
// Just continue // Just continue
} }
runScriptsList(options.postScripts, options.postScriptFileMap, scriptState, runScriptsList(options.postScripts, options.postScriptFileMap, scriptState,
@ -1834,7 +1836,7 @@ public class HeadlessAnalyzer {
} }
/** /**
* Ghidra project class required to gain access to specialized project constructor * Ghidra project class required to gain access to specialized project constructor
* for URL connection. * for URL connection.
*/ */
private static class HeadlessProject extends DefaultProject { private static class HeadlessProject extends DefaultProject {

View file

@ -23,11 +23,11 @@ import java.util.*;
import generic.jar.ResourceFile; import generic.jar.ResourceFile;
import ghidra.framework.ApplicationProperties; import ghidra.framework.ApplicationProperties;
import ghidra.framework.GModule; import ghidra.framework.GModule;
import utility.application.ApplicationLayout; import ghidra.util.Msg;
import utility.module.ModuleUtilities; import utility.module.ModuleUtilities;
/** /**
* The Ghidra jar application layout defines the customizable elements of the Ghidra application's * The Ghidra jar application layout defines the customizable elements of the Ghidra application's
* directory structure when running in "single jar mode." * directory structure when running in "single jar mode."
*/ */
public class GhidraJarApplicationLayout extends GhidraApplicationLayout { public class GhidraJarApplicationLayout extends GhidraApplicationLayout {
@ -51,7 +51,11 @@ public class GhidraJarApplicationLayout extends GhidraApplicationLayout {
protected Collection<ResourceFile> findGhidraApplicationRootDirs() { protected Collection<ResourceFile> findGhidraApplicationRootDirs() {
List<ResourceFile> dirs = new ArrayList<>(); List<ResourceFile> dirs = new ArrayList<>();
String appPropPath = "/_Root/Ghidra/" + ApplicationProperties.PROPERTY_FILE; String appPropPath = "/_Root/Ghidra/" + ApplicationProperties.PROPERTY_FILE;
URL appPropUrl = ApplicationLayout.class.getResource(appPropPath); URL appPropUrl = getClass().getResource(appPropPath);
if (appPropUrl == null) {
throw new IllegalStateException(
"The Ghidra Jar must have an application.properties file at " + appPropPath);
}
ResourceFile rootDir = fromUrl(appPropUrl).getParentFile(); ResourceFile rootDir = fromUrl(appPropUrl).getParentFile();
dirs.add(rootDir); dirs.add(rootDir);
return dirs; return dirs;
@ -79,7 +83,12 @@ public class GhidraJarApplicationLayout extends GhidraApplicationLayout {
@Override @Override
protected List<ResourceFile> findExtensionInstallationDirectories() { protected List<ResourceFile> findExtensionInstallationDirectories() {
URL extensionInstallUrl = ApplicationLayout.class.getResource("/_Root/Ghidra/Extensions"); String path = "/_Root/Ghidra/Extensions";
URL extensionInstallUrl = getClass().getResource(path);
if (extensionInstallUrl == null) {
Msg.debug(this, "No Extensions dir found at " + path);
return List.of();
}
ResourceFile extensionInstallDir = fromUrl(extensionInstallUrl); ResourceFile extensionInstallDir = fromUrl(extensionInstallUrl);
return Collections.singletonList(extensionInstallDir); return Collections.singletonList(extensionInstallDir);
} }
@ -94,7 +103,7 @@ public class GhidraJarApplicationLayout extends GhidraApplicationLayout {
String urlString = url.toExternalForm(); String urlString = url.toExternalForm();
try { try {
// Decode the URL to replace things like %20 with real spaces. // Decode the URL to replace things like %20 with real spaces.
// Note: can't use URLDecoder.decode(String, Charset) because Utility must be // Note: can't use URLDecoder.decode(String, Charset) because Utility must be
// Java 1.8 compatible. // Java 1.8 compatible.
urlString = URLDecoder.decode(urlString, "UTF-8"); urlString = URLDecoder.decode(urlString, "UTF-8");
} }