From d70c6c256f37c7aa70dec8550aa459bc430bb371 Mon Sep 17 00:00:00 2001 From: Ryan Kurtz Date: Fri, 5 Jun 2020 15:25:51 -0400 Subject: [PATCH] Extensions installed through the GUI now get put in the user settings directory (rather than installation directory) --- .../help/topics/FrontEndPlugin/Extensions.htm | 10 ++- .../util/extensions/ExtensionUtilsTest.java | 13 +-- .../remote/GhidraServerApplicationLayout.java | 3 +- .../dialog/ExtensionTableModel.java | 20 ++++- .../dialog/ExtensionTableProvider.java | 7 +- .../plugintool/dialog/ExtensionUtils.java | 80 ++++++++++--------- .../java/ghidra/GhidraApplicationLayout.java | 22 ++--- .../ghidra/GhidraJarApplicationLayout.java | 4 +- .../ghidra/GhidraTestApplicationLayout.java | 5 +- .../application/ApplicationLayout.java | 15 ++-- GhidraDocs/InstallationGuide.html | 17 ++-- 11 files changed, 116 insertions(+), 80 deletions(-) diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Extensions.htm b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Extensions.htm index e2f08c0864..8b38eaa4f7 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Extensions.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Extensions.htm @@ -29,11 +29,17 @@ option on the project file menu.

Dialog Components

Extensions List

-The list of extensions is populated when the dialog is launched. To build the list, Ghidra looks in two locations: +The list of extensions is populated when the dialog is launched. To build the list, Ghidra looks in several locations: +

Note: Extensions that have been installed directly into the Ghidra installation directory cannot be uninstalled +from this dialog. They must be manually removed from the filesystem.

Description Panel

diff --git a/Ghidra/Features/Base/src/test/java/ghidra/util/extensions/ExtensionUtilsTest.java b/Ghidra/Features/Base/src/test/java/ghidra/util/extensions/ExtensionUtilsTest.java index 7284d2bc86..5f0485e644 100644 --- a/Ghidra/Features/Base/src/test/java/ghidra/util/extensions/ExtensionUtilsTest.java +++ b/Ghidra/Features/Base/src/test/java/ghidra/util/extensions/ExtensionUtilsTest.java @@ -15,8 +15,7 @@ */ package ghidra.util.extensions; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import java.io.*; import java.util.Set; @@ -57,7 +56,9 @@ public class ExtensionUtilsTest extends AbstractGenericTest { // we start with a clean slate). If they're not empty, CORRECT THE SITUATION. if (!checkCleanInstall()) { FileUtilities.deleteDir(gLayout.getExtensionArchiveDir().getFile(false)); - FileUtilities.deleteDir(gLayout.getExtensionInstallationDir().getFile(false)); + for (ResourceFile installDir : gLayout.getExtensionInstallationDirs()) { + FileUtilities.deleteDir(installDir.getFile(false)); + } } createExtensionDirs(); @@ -248,7 +249,7 @@ public class ExtensionUtilsTest extends AbstractGenericTest { } } - ResourceFile installDir = gLayout.getExtensionInstallationDir(); + ResourceFile installDir = gLayout.getExtensionInstallationDirs().get(0); if (!installDir.exists()) { if (!installDir.mkdir()) { throw new IOException("Failed to create extension installation directory for test"); @@ -260,7 +261,7 @@ public class ExtensionUtilsTest extends AbstractGenericTest { * Verifies that the installation folder is empty. */ private boolean checkCleanInstall() { - ResourceFile[] files = gLayout.getExtensionInstallationDir().listFiles(); + ResourceFile[] files = gLayout.getExtensionInstallationDirs().get(0).listFiles(); return (files == null || files.length == 0); } @@ -271,7 +272,7 @@ public class ExtensionUtilsTest extends AbstractGenericTest { * @param name the name of the installed extension */ private void checkDirtyInstall(String name) { - ResourceFile[] files = gLayout.getExtensionInstallationDir().listFiles(); + ResourceFile[] files = gLayout.getExtensionInstallationDirs().get(0).listFiles(); assertTrue(files.length >= 1); assertTrue(files[0].getName().equals(name)); } diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServerApplicationLayout.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServerApplicationLayout.java index 9d6248463b..ad44b21f93 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServerApplicationLayout.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServerApplicationLayout.java @@ -17,6 +17,7 @@ package ghidra.server.remote; import java.io.FileNotFoundException; import java.io.IOException; +import java.util.Collections; import ghidra.framework.ApplicationProperties; import ghidra.util.SystemUtilities; @@ -51,7 +52,7 @@ public class GhidraServerApplicationLayout extends ApplicationLayout { // Extension directories extensionArchiveDir = null; - extensionInstallationDir = null; + extensionInstallationDirs = Collections.emptyList(); // User directories (don't let anything use the user home directory...there may not be one) userTempDir = ApplicationUtilities.getDefaultUserTempDir(applicationProperties); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/ExtensionTableModel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/ExtensionTableModel.java index 49b839aa5b..8361138de1 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/ExtensionTableModel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/ExtensionTableModel.java @@ -17,6 +17,7 @@ package ghidra.framework.plugintool.dialog; import java.awt.Color; import java.awt.Component; +import java.io.File; import java.util.ArrayList; import java.util.List; @@ -33,6 +34,7 @@ import ghidra.util.exception.CancelledException; import ghidra.util.table.column.AbstractGColumnRenderer; import ghidra.util.table.column.GColumnRenderer; import ghidra.util.task.TaskMonitor; +import utilities.util.FileUtilities; /** * Model for the {@link ExtensionTablePanel}. This defines 5 columns for displaying information in @@ -95,7 +97,7 @@ class ExtensionTableModel extends ThreadedTableModel extensions = new HashSet<>(); // Find all extension.properties or extension.properties.uninstalled files in // the install directory and create a ExtensionDetails object for each. - ResourceFile installDir = layout.getExtensionInstallationDir(); - List propFiles = findExtensionPropertyFiles(installDir, includeUninstalled); - for (ResourceFile propFile : propFiles) { - - ExtensionDetails details = createExtensionDetailsFromPropertyFile(propFile); + for (ResourceFile installDir : layout.getExtensionInstallationDirs()) { + if (!installDir.isDirectory()) { + continue; + } + List propFiles = + findExtensionPropertyFiles(installDir, includeUninstalled); + for (ResourceFile propFile : propFiles) { - // We found this extension in the installation directory, so set the install path - // property and add to the final set. - details.setInstallPath(propFile.getParentFile().getAbsolutePath()); - extensions.add(details); + ExtensionDetails details = createExtensionDetailsFromPropertyFile(propFile); + + // We found this extension in the installation directory, so set the install path + // property and add to the final set. + details.setInstallPath(propFile.getParentFile().getAbsolutePath()); + if (!extensions.contains(details)) { + extensions.add(details); + } + else { + Msg.warn(null, + "Skipping extension \"" + details.getName() + "\" found at " + + details.getInstallPath() + + ". Extension by that name installed in higher priority location."); + } + } } return extensions; @@ -219,8 +227,13 @@ public class ExtensionUtils { * @return true if installed */ public static boolean isInstalled(String extensionName) { - return new File(Application.getApplicationLayout().getExtensionInstallationDir() + - File.separator + extensionName).exists(); + for (ResourceFile installDir : Application.getApplicationLayout() + .getExtensionInstallationDirs()) { + if (new ResourceFile(installDir, extensionName).exists()) { + return true; + } + } + return false; } /** @@ -267,16 +280,15 @@ public class ExtensionUtils { return false; } + File installDir = new ResourceFile( + Application.getApplicationLayout().getExtensionInstallationDirs().get(0), + extension.getName()).getFile(false); + if (extension.getArchivePath() == null) { // Special Case: If the archive path is null then this must be an extension that // was installed from an external location, then uninstalled. In this case, there // should be a Module.manifest.uninstalled and extension.properties.uninstalled // present. If so, just restore them. If not, there's a problem. - - String installPath = Application.getApplicationLayout().getExtensionInstallationDir() + - File.separator + extension.getName(); - File installDir = new File(installPath); - if (installDir.exists()) { return restoreStateFiles(installDir); } @@ -299,19 +311,13 @@ public class ExtensionUtils { // the GUI then tries to reinstall it without restarting Ghidra, the extension hasn't actually // been removed yet; just the manifest file has been renamed. In this case we don't need to go through // the full install process of unzipping or copying files to the install location. All we need - // to do is rename the manifest file from Module.manifest.uninstall back to Module.manifest. - String installPath = Application.getApplicationLayout().getExtensionInstallationDir() + File.separator + - extension.getName(); - File installDir = new File(installPath); - + // to do is rename the manifest file from Module.manifest.uninstall back to Module.manifest. if (installDir.exists()) { return restoreStateFiles(installDir); } if (install(file)) { - extension.setInstallPath( - Application.getApplicationLayout().getExtensionInstallationDir() + File.separator + - extension.getName()); + extension.setInstallPath(installDir + File.separator + extension.getName()); return true; } @@ -554,7 +560,9 @@ public class ExtensionUtils { File errorFile = e.getErrorFile(); if (errorFile != null) { // Get the root of the extension in the install location. - ResourceFile installDir = Application.getApplicationLayout().getExtensionInstallationDir(); + ResourceFile installDir = Application.getApplicationLayout() + .getExtensionInstallationDirs() + .get(0); // Get the root directory of the extension (strip off the install folder location and // grab the first part of the remaining path). @@ -755,7 +763,7 @@ public class ExtensionUtils { File newDir = null; try { newDir = - new File(Application.getApplicationLayout().getExtensionInstallationDir() + + new File(Application.getApplicationLayout().getExtensionInstallationDirs().get(0) + File.separator + extension.getName()); FileUtilities.deleteDir(newDir, monitor); FileUtilities.copyDir(extension, newDir, monitor); @@ -788,11 +796,10 @@ public class ExtensionUtils { ExtensionExceptionType.ZIP_ERROR); } - if (layout.getExtensionInstallationDir() == null || - !layout.getExtensionInstallationDir().exists()) { + ResourceFile installDir = layout.getExtensionInstallationDirs().get(0); + if (installDir == null || !installDir.exists()) { throw new ExtensionException( - "Extension installation directory is not valid: " + - layout.getExtensionInstallationDir(), + "Extension installation directory is not valid: " + installDir, ExtensionExceptionType.INVALID_INSTALL_LOCATION); } @@ -803,8 +810,7 @@ public class ExtensionUtils { ZipArchiveEntry entry = entries.nextElement(); - String filePath = - (layout.getExtensionInstallationDir() + File.separator + entry.getName()); + String filePath = installDir + File.separator + entry.getName(); File file = new File(filePath); diff --git a/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraApplicationLayout.java b/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraApplicationLayout.java index ba35977fd2..c2507869ec 100644 --- a/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraApplicationLayout.java +++ b/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraApplicationLayout.java @@ -62,7 +62,7 @@ public class GhidraApplicationLayout extends ApplicationLayout { getApplicationInstallationDir()); // Extensions - extensionInstallationDir = findExtensionInstallationDirectory(); + extensionInstallationDirs = findExtensionInstallationDirectories(); extensionArchiveDir = findExtensionArchiveDirectory(); // Patch directory @@ -217,17 +217,20 @@ public class GhidraApplicationLayout extends ApplicationLayout { } /** - * Returns the directory where all Ghidra extension archives should be - * installed. This should be at the following location:
+ * Returns a prioritized list of directories where Ghidra extensions are installed. These + * should be at the following locations:
*
    + *
  • [user settings dir]/Extensions
  • *
  • [application install dir]/Ghidra/Extensions
  • *
  • ghidra/Ghidra/Extensions (development mode)
  • *
* * @return the install folder, or null if can't be determined */ - protected ResourceFile findExtensionInstallationDirectory() { + protected List findExtensionInstallationDirectories() { + List dirs = new ArrayList<>(); + // Would like to find a better way to do this, but for the moment this seems the // only solution. We want to get the 'Extensions' directory in ghidra, but there's // no way to retrieve that directory directly. We can only get the full set of @@ -237,13 +240,14 @@ public class GhidraApplicationLayout extends ApplicationLayout { ResourceFile rootDir = getApplicationRootDirs().iterator().next(); File temp = new File(rootDir.getFile(false), "Extensions"); if (temp.exists()) { - return new ResourceFile(temp); + dirs.add(new ResourceFile(temp)); } - - return null; + } + else { + dirs.add(new ResourceFile(new File(userSettingsDir, "Extensions"))); + dirs.add(new ResourceFile(applicationInstallationDir, "Ghidra/Extensions")); } - ResourceFile installDir = findGhidraApplicationInstallationDir(); - return new ResourceFile(installDir, "Ghidra/Extensions"); + return dirs; } } diff --git a/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraJarApplicationLayout.java b/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraJarApplicationLayout.java index 0a06f63a3b..7466ab4061 100644 --- a/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraJarApplicationLayout.java +++ b/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraJarApplicationLayout.java @@ -75,9 +75,9 @@ public class GhidraJarApplicationLayout extends GhidraApplicationLayout { } @Override - protected ResourceFile findExtensionInstallationDirectory() { + protected List findExtensionInstallationDirectories() { ResourceFile extensionInstallDir = new ResourceFile( ApplicationLayout.class.getResource("/_Root/Ghidra/Extensions").toExternalForm()); - return extensionInstallDir; + return List.of(extensionInstallDir); } } diff --git a/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraTestApplicationLayout.java b/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraTestApplicationLayout.java index 51d2305b5b..e8d49222bf 100644 --- a/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraTestApplicationLayout.java +++ b/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraTestApplicationLayout.java @@ -16,6 +16,7 @@ package ghidra; import java.io.*; +import java.util.List; import generic.jar.ResourceFile; @@ -51,9 +52,9 @@ public class GhidraTestApplicationLayout extends GhidraApplicationLayout { } @Override - protected ResourceFile findExtensionInstallationDirectory() { + protected List findExtensionInstallationDirectories() { File installDir = new File(getUserTempDir(), "ExtensionInstallDir"); - return new ResourceFile(installDir); + return List.of(new ResourceFile(installDir)); } @Override diff --git a/Ghidra/Framework/Utility/src/main/java/utility/application/ApplicationLayout.java b/Ghidra/Framework/Utility/src/main/java/utility/application/ApplicationLayout.java index aa2c46937e..50e58a44d5 100644 --- a/Ghidra/Framework/Utility/src/main/java/utility/application/ApplicationLayout.java +++ b/Ghidra/Framework/Utility/src/main/java/utility/application/ApplicationLayout.java @@ -17,8 +17,7 @@ package utility.application; import java.io.File; import java.io.IOException; -import java.util.Collection; -import java.util.Map; +import java.util.*; import generic.jar.ResourceFile; import ghidra.framework.ApplicationProperties; @@ -44,7 +43,7 @@ public abstract class ApplicationLayout { protected File userSettingsDir; protected ResourceFile patchDir; protected ResourceFile extensionArchiveDir; - protected ResourceFile extensionInstallationDir; + protected List extensionInstallationDirs; /** * Gets the application properties from the application layout @@ -121,13 +120,13 @@ public abstract class ApplicationLayout { } /** - * Returns the application Extensions installation folder. + * Returns an {@link List ordered list} of the application Extensions installation directories. * - * @return the application Extensions installation directory. Could be null if the - * {@link ApplicationLayout} does not support application Extensions. + * @return an {@link List ordered list} of the application Extensions installation directories. + * Could be empty if the {@link ApplicationLayout} does not support application Extensions. */ - public final ResourceFile getExtensionInstallationDir() { - return extensionInstallationDir; + public final List getExtensionInstallationDirs() { + return extensionInstallationDirs; } /** diff --git a/GhidraDocs/InstallationGuide.html b/GhidraDocs/InstallationGuide.html index fcbdcd7063..f7f5b5e72d 100644 --- a/GhidraDocs/InstallationGuide.html +++ b/GhidraDocs/InstallationGuide.html @@ -18,7 +18,7 @@

Ghidra Installation Guide

-The installation information provided is effective as of Ghidra 9.1 and is subject to change with +The installation information provided is effective as of Ghidra 9.2 and is subject to change with future releases.

@@ -363,17 +363,14 @@ can be found in the <GhidraInstallDir>/Extensions directory.

  • - Installing or uninstalling Ghidra extensions may fail if the user does not have write - permissions to <GhidraInstallDir>. This may occur if the user is running Ghidra - from a shared installation location. In this situation, the owner of the Ghidra installation - directory will be responsible for managing what Ghidra extensions are available for that - particular installation of Ghidra. + Extensions installed from the Ghidra front-end GUI get installed at + <UserDir>/.ghidra/.ghidra-[version]/Extensions.
  • -

    It is possible to install and uninstall Ghidra extensions manually when the Ghidra front-end - GUI is not available. This may be required if a system administrator is managing the Ghidra - extensions of a shared Ghidra installation on behalf of a user, or if a user wishes to install - an extension into a Ghidra installation that is only ever used headlessly.

    +

    It is possible to install Ghidra extensions directly into the Ghidra installation directory. + This may be required if a system administrator is managing extensions for multiple users that + all use a shared installation of Ghidra. It may also be more convenient to manage extensions + this way if a Ghidra installation is only ever used headlessly.

    To install an extension in these cases, simply extract the desired Ghidra extension archive file(s) to the <GhidraInstallDir>/Ghidra/Extensions directory. For example, on Linux or macOS: