Extensions installed through the GUI now get put in the user settings

directory (rather than installation directory)
This commit is contained in:
Ryan Kurtz 2020-06-05 15:25:51 -04:00 committed by adamopolous
parent 3ea1c2bb2b
commit d70c6c256f
11 changed files with 116 additions and 80 deletions

View File

@ -29,11 +29,17 @@ option on the project file menu.</p>
<h2>Dialog Components</h2>
<h3>Extensions List</h3>
<blockquote>
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:
<ul>
<li>Extensions Installation Directory: Contains any extensions that have been installed. This is located at <i>[installation dir]/Ghidra/Extensions/</i></li>
<li>Extension Installation Directories: Contains any extensions that have been installed. The directories are located at:</li>
<ul>
<li><i>[user dir]/.ghidra/.ghidra-[version]/Extensions</i> - Installed/uninstalled from this dialog</li>
<li><i>[installation dir]/Ghidra/Extensions/</i> - Installed/uninstalled from filesystem manually</li>
</ul>
<li>Extensions Archive Directory: This is where all archive files (zips) are stored. It is located at <i>[installation dir]/Extensions/Ghidra/</i></li>
</ul>
<p><b>Note: </b> 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.</p>
</blockquote>
<h3>Description Panel</h3>

View File

@ -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));
}

View File

@ -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);

View File

@ -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<ExtensionDetails, List<Exte
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
if (Application.inSingleJarMode()) {
if (Application.inSingleJarMode() || SystemUtilities.isInDevelopmentMode()) {
return false;
}
@ -104,6 +106,14 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, List<Exte
return false;
}
// Do not allow GUI uninstallation of extensions manually installed in installation
// directory
if (extension.getInstallPath() != null && FileUtilities.isPathContainedWithin(
Application.getApplicationLayout().getApplicationInstallationDir().getFile(false),
new File(extension.getInstallPath()))) {
return false;
}
return (columnIndex == INSTALLED_COL);
}
@ -123,7 +133,13 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, List<Exte
// If the user does not have write permissions on the installation dir, they cannot
// install.
ResourceFile installDir = Application.getApplicationLayout().getExtensionInstallationDir();
ResourceFile installDir =
Application.getApplicationLayout().getExtensionInstallationDirs().get(0);
if (!installDir.exists() && !installDir.mkdir()) {
Msg.showError(this, null, "Directory Error",
"Cannot install/uninstall extensions: Failed to create extension installation directory.\n" +
"See the \"Ghidra Extension Notes\" section of the Ghidra Installation Guide for more information.");
}
if (!installDir.canWrite()) {
Msg.showError(this, null, "Permissions Error",
"Cannot install/uninstall extensions: Invalid write permissions on installation directory.\n" +

View File

@ -123,7 +123,12 @@ public class ExtensionTableProvider extends DialogComponentProvider {
// Don't let the user attempt to install anything if they don't have write
// permissions on the installation dir.
ResourceFile installDir =
Application.getApplicationLayout().getExtensionInstallationDir();
Application.getApplicationLayout().getExtensionInstallationDirs().get(0);
if (!installDir.exists() && !installDir.mkdir()) {
Msg.showError(this, null, "Directory Error",
"Cannot install/uninstall extensions: Failed to create extension installation directory.\n" +
"See the \"Ghidra Extension Notes\" section of the Ghidra Installation Guide for more information.");
}
if (!installDir.canWrite()) {
Msg.showError(this, null, "Permissions Error",
"Cannot install/uninstall extensions: Invalid write permissions on installation directory.\n" +

View File

@ -133,26 +133,34 @@ public class ExtensionUtils {
ApplicationLayout layout = Application.getApplicationLayout();
if (layout.getExtensionInstallationDir() == null ||
!layout.getExtensionInstallationDir().exists()) {
return Collections.emptySet();
}
// The set to return;
Set<ExtensionDetails> 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<ResourceFile> propFiles = findExtensionPropertyFiles(installDir, includeUninstalled);
for (ResourceFile propFile : propFiles) {
ExtensionDetails details = createExtensionDetailsFromPropertyFile(propFile);
for (ResourceFile installDir : layout.getExtensionInstallationDirs()) {
if (!installDir.isDirectory()) {
continue;
}
List<ResourceFile> 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);

View File

@ -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:<br>
* Returns a prioritized list of directories where Ghidra extensions are installed. These
* should be at the following locations:<br>
* <ul>
* <li><code>[user settings dir]/Extensions</code></li>
* <li><code>[application install dir]/Ghidra/Extensions</code></li>
* <li><code>ghidra/Ghidra/Extensions</code> (development mode)</li>
* </ul>
*
* @return the install folder, or null if can't be determined
*/
protected ResourceFile findExtensionInstallationDirectory() {
protected List<ResourceFile> findExtensionInstallationDirectories() {
List<ResourceFile> 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;
}
}

View File

@ -75,9 +75,9 @@ public class GhidraJarApplicationLayout extends GhidraApplicationLayout {
}
@Override
protected ResourceFile findExtensionInstallationDirectory() {
protected List<ResourceFile> findExtensionInstallationDirectories() {
ResourceFile extensionInstallDir = new ResourceFile(
ApplicationLayout.class.getResource("/_Root/Ghidra/Extensions").toExternalForm());
return extensionInstallDir;
return List.of(extensionInstallDir);
}
}

View File

@ -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<ResourceFile> findExtensionInstallationDirectories() {
File installDir = new File(getUserTempDir(), "ExtensionInstallDir");
return new ResourceFile(installDir);
return List.of(new ResourceFile(installDir));
}
@Override

View File

@ -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<ResourceFile> 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<ResourceFile> getExtensionInstallationDirs() {
return extensionInstallationDirs;
}
/**

View File

@ -18,7 +18,7 @@
<h1>Ghidra Installation Guide</h1>
<p>
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.
</p>
@ -363,17 +363,14 @@ can be found in the <i>&lt;GhidraInstallDir&gt;</i>/Extensions directory.</p>
</ol>
</li>
<li>
Installing or uninstalling Ghidra extensions may fail if the user does not have write
permissions to <i>&lt;GhidraInstallDir&gt;</i>. 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
<i>&lt;UserDir&gt;</i>/.ghidra/.ghidra-[version]/Extensions.
</li>
<li>
<p>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.<p>
<p>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.<p>
<p>To install an extension in these cases, simply extract the desired Ghidra extension archive
file(s) to the <i>&lt;GhidraInstallDir&gt;</i>/Ghidra/Extensions directory. For example, on
Linux or macOS:</p>