Updated to handle checking for pre-installed extensions

This commit is contained in:
dragonmacher
2026-04-03 11:23:00 -04:00
parent 96a3445676
commit 11a9ae4ca0
14 changed files with 1112 additions and 188 deletions
@@ -0,0 +1,333 @@
/* ###
* 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.framework.project.tool;
import static org.junit.Assert.*;
import java.awt.Window;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.jar.*;
import javax.tools.*;
import org.junit.Before;
import org.junit.Test;
import docking.DialogComponentProvider;
import generic.jar.ResourceFile;
import ghidra.GhidraClassLoader;
import ghidra.framework.Application;
import ghidra.framework.ApplicationConfiguration;
import ghidra.framework.project.extensions.ExtensionInstaller;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.TestEnv;
import ghidra.util.Msg;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.extensions.ExtensionDetails;
import ghidra.util.extensions.ExtensionUtils;
import ghidra.util.task.TaskMonitor;
import util.CollectionUtils;
import utilities.util.FileUtilities;
import utility.application.ApplicationLayout;
import utility.module.ModuleUtilities;
public class ExtensionManagerIntegrationTest extends AbstractGhidraHeadedIntegrationTest {
private ApplicationLayout appLayout;
@Override
protected ApplicationConfiguration createApplicationConfiguration() {
// This is a workaround to get the ClassSearcher to use the custom extensions class loader,
// which is required, since the standard class loader's classpath was set by the time this
// code gets run. Without this, the standard class loader cannot find our new extension.
// The classpath is normally managed by the GhidraLauncher in a non-test environment.
System.setProperty(GhidraClassLoader.ENABLE_RESTRICTED_EXTENSIONS_PROPERTY,
Boolean.TRUE.toString());
return super.createApplicationConfiguration();
}
@Before
public void setup() throws IOException {
appLayout = Application.getApplicationLayout();
ExtensionUtils.clearCache();
deleteExtensionDirs();
createExtensionDirs();
}
@Test
public void testNewExtensionPromptsUser() throws Exception {
//
// Create a new extension with a plugin
//
File srcExtensionFolder = createExternalExtensionInFolder();
assertTrue(ExtensionInstaller.install(srcExtensionFolder));
//
// Update classpath the extension (this is normally done by the GhidraLauncher)
//
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
assertEquals(1, extensions.size());
ExtensionDetails extension = CollectionUtils.get(extensions);
File extensionFolder = extension.getInstallDir();
String jarFile = extensionFolder + File.separator + "lib/TestHelloWorldExtension.jar";
String cp = System.getProperty("java.class.path");
System.setProperty("java.class.path", cp + File.pathSeparator + jarFile);
setInstanceField("hasSearched", ClassSearcher.class, false);
ClassSearcher.search(TaskMonitor.DUMMY);
//
// Launch the tool and verify the user is prompted to load the new plugins
//
String title = "New Plugins Found!";
launchTool(title);
DialogComponentProvider dialog = waitForDialogComponent(title);
pressButtonByText(dialog, "Yes", false);
DialogComponentProvider installerDialog = waitForDialogComponent(title);
close(installerDialog);
}
private void launchTool(String title) throws Exception {
/*
Launching the tool with new extensions will show a modal dialog. Tool startup is slow
enough that we can't use the normal wait mechanism, as it will timeout.
*/
TestEnv env = new TestEnv();
runSwing(() -> {
env.launchDefaultTool();
}, false);
int total = 0;
int max = 20_000;
int sleepyTime = 250;
while (total < max) {
sleep(sleepyTime);
total += sleepyTime;
Window w = getWindow(title);
if (w != null) {
return;
}
}
}
private File createExternalExtensionInFolder() throws Exception {
File externalFolder = createTempDirectory("TestExtensionParentDir");
return doCreateExternalExtensionInFolder(externalFolder, "TextExtension");
}
private File doCreateExternalExtensionInFolder(File externalFolder, String extensionName)
throws Exception {
String version = Application.getApplicationVersion();
ResourceFile root = new ResourceFile(new ResourceFile(externalFolder), extensionName);
assertTrue(FileUtilities.mkdirs(root.getFile(false)));
// Have to add a prop file so this will be recognized as an extension
File propFile = new ResourceFile(root, "extension.properties").getFile(false);
assertTrue(propFile.createNewFile());
Properties props = new Properties();
props.put("name", extensionName);
props.put("description", "This is a description for " + extensionName);
props.put("author", "First Last");
props.put("createdOn", new SimpleDateFormat("MM/dd/yyyy").format(new Date()));
props.put("version", version);
try (OutputStream os = new FileOutputStream(propFile)) {
props.store(os, null);
}
File manifest = new ResourceFile(root, ModuleUtilities.MANIFEST_FILE_NAME).getFile(false);
manifest.createNewFile();
createJarWithPluginClass(root);
Msg.debug(this, "extension dir: " + root);
return root.getFile(false);
}
private void createJarWithPluginClass(ResourceFile root) throws Exception {
ResourceFile lib = new ResourceFile(root, "lib");
assertTrue(FileUtilities.mkdirs(lib.getFile(false)));
String jarName = "TestHelloWorldExtension.jar";
File jarFile = createExtensionLibJar(jarName);
Msg.debug(this, "wrote jar: " + jarFile);
File libFile = new File(lib.getFile(false), jarName);
FileUtilities.copyFile(jarFile, libFile, false, TaskMonitor.DUMMY);
}
private File createExtensionLibJar(String jarName) throws Exception {
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
File tempFile = createTempFileForTest();
File tempParent = tempFile.getParentFile();
File jarFile = new File(tempParent, jarName);
tempFile.renameTo(jarFile);
FileOutputStream fos = new FileOutputStream(jarFile);
JarOutputStream jos = new JarOutputStream(fos, manifest);
File classDir = compileExtensionPlugin();
Msg.debug(this, "class file: " + classDir);
File[] files = classDir.listFiles((dir, name) -> name.endsWith(".class"));
File classFile = files[0];
JarEntry dirEntry = new JarEntry(classDir.getName() + "/");
jos.putNextEntry(dirEntry);
jos.closeEntry();
JarEntry fileEntry = new JarEntry(dirEntry.getName() + classFile.getName());
jos.putNextEntry(fileEntry);
try (FileInputStream fis = new FileInputStream(classFile)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
jos.write(buffer, 0, bytesRead);
}
}
jos.closeEntry();
jos.close();
fos.close();
Msg.debug(this, "jar file: " + jarFile);
return jarFile;
}
private File compileExtensionPlugin() throws Exception {
// 1. Get the system Java compiler
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
System.err.println("JDK not found. Make sure you are running a JDK, not a JRE.");
fail();
return null;
}
File sourceFile = File.createTempFile("tmp", ".tmp");
File dir = sourceFile.getParentFile();
File packageDir = new File(dir, "testplugin");
packageDir.mkdirs();
File packagedSource = new File(packageDir, "TestHelloWorldPlugin.java");
FileUtilities.writeStringToFile(packagedSource,
"""
package testplugin;
import ghidra.app.ExamplesPluginPackage;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
//@formatter:off
@PluginInfo(
status = PluginStatus.RELEASED,
packageName = ExamplesPluginPackage.NAME,
category = PluginCategoryNames.EXAMPLES,
shortDescription = "Displays 'Hello World'",
description = "Test plugin"
)
//@formatter:on
public class TestHelloWorldPlugin extends Plugin {
public TestHelloWorldPlugin(PluginTool tool) {
super(tool);
}
}
""");
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
StandardJavaFileManager fileManager =
compiler.getStandardFileManager(diagnostics, null, null);
Iterable<? extends JavaFileObject> compilationUnits =
fileManager.getJavaFileObjects(packagedSource);
// 5. Create a compilation task
JavaCompiler.CompilationTask task = compiler.getTask(
null, // output writer (uses System.err if null)
fileManager, // file manager (uses standard if null)
diagnostics, // diagnostic listener (uses System.err if null)
null, // options (pass null for no options)
null, // classes for annotation processing
compilationUnits // source files
);
// 6. Execute the compilation task
boolean success = task.call();
// 7. Process the diagnostics
if (success) {
System.out.println("Compilation successful.");
}
else {
System.out.println("Compilation failed.");
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
System.out.println(diagnostic.getKind() + ": " + diagnostic.getMessage(null) +
" (Line: " + diagnostic.getLineNumber() + ")");
}
}
fileManager.close();
return packageDir;
}
private void createExtensionDirs() throws IOException {
ResourceFile extensionDir = appLayout.getExtensionArchiveDir();
if (!extensionDir.exists()) {
if (!extensionDir.mkdir()) {
throw new IOException("Failed to create extension archive directory for test");
}
}
ResourceFile installDir = appLayout.getExtensionInstallationDirs().get(0);
if (!installDir.exists()) {
if (!installDir.mkdir()) {
throw new IOException("Failed to create extension installation directory for test");
}
}
}
private void deleteExtensionDirs() {
FileUtilities.deleteDir(appLayout.getExtensionArchiveDir().getFile(false));
for (ResourceFile installDir : appLayout.getExtensionInstallationDirs()) {
FileUtilities.deleteDir(installDir.getFile(false));
}
}
}
@@ -0,0 +1,304 @@
/* ###
* 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.framework.project.tool;
import static org.junit.Assert.*;
import java.io.IOException;
import java.util.*;
import org.jdom2.Element;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import org.junit.Before;
import org.junit.Test;
import generic.jar.ResourceFile;
import ghidra.framework.Application;
import ghidra.framework.project.tool.testplugins.TestExtensionHello2Plugin;
import ghidra.framework.project.tool.testplugins.TestExtensionHelloPlugin;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.util.extensions.ExtensionUtils;
import ghidra.util.xml.GenericXMLOutputter;
import ghidra.util.xml.XmlUtilities;
import utilities.util.FileUtilities;
import utility.application.ApplicationLayout;
public class ExtensionManagerTest extends AbstractGhidraHeadedIntegrationTest {
private ApplicationLayout appLayout;
private FakeToolExtensionsEnabledState extensionsState;
private ToolExtensionsStatusManager extensionManager;
@Before
public void setup() throws IOException {
appLayout = Application.getApplicationLayout();
ExtensionUtils.clearCache();
deleteExtensionDirs();
createExtensionDirs();
extensionsState = new FakeToolExtensionsEnabledState();
extensionManager = new ToolExtensionsStatusManager(extensionsState);
}
@Test
public void testNewExtensionPromptsUser() throws Exception {
// install new extension with a plugin
extensionsState.addExtension("FooExtension", TestExtensionHelloPlugin.class);
// call extension manager using xml with no knowledge of the new extension
Element element = XmlUtilities.fromString("""
<ROOT>
<EXTENSIONS>
</EXTENSIONS>
</ROOT>
""");
extensionManager.restoreFromXml(element);
// verify user is prompted to add new plugins
extensionManager.checkForNewExtensions();
assertTrue(extensionsState.didPrompt());
}
@Test
public void testKnownExtensionDoesNotPromptUser() throws Exception {
// install extension with a plugin
extensionsState.addExtension("FooExtension", TestExtensionHelloPlugin.class);
// call extension manager with xml describing the extension
//@formatter:off
Element element = XmlUtilities.fromString(
"""
<ROOT>
<EXTENSIONS>
<EXTENSION NAME="FooExtension">
<PLUGIN CLASS="ghidra.framework.project.tool.testplugins.TestExtensionHelloPlugin" />
</EXTENSION>
</EXTENSIONS>
</ROOT>
""");
//@formatter:on
extensionManager.restoreFromXml(element);
// verify no prompt
extensionManager.checkForNewExtensions();
assertFalse(extensionsState.didPrompt());
}
@Test
public void testKnownExetension_OldStyleXml_UninstalledPlugins_PromptsUser() throws Exception {
//
// Tests that old xml describing a known extension triggers a prompt. The old style xml
// does not contain plugin names. Thus, we have no way of knowing if there are new plugins,
// so we prompt. Tests support for tools with the old xml migrating to the new xml.
//
// install extension with a plugin
extensionsState.addExtension("FooExtension", TestExtensionHelloPlugin.class);
// Call extension manager with old style xml describing the extension. This knows about the
// extension, but not the plugins inside.
Element element = XmlUtilities.fromString("""
<ROOT>
<EXTENSIONS>
<EXTENSION NAME="FooExtension" />
</EXTENSIONS>
</ROOT>
""");
extensionManager.restoreFromXml(element);
// verify user is prompted to add new plugins
extensionManager.checkForNewExtensions();
assertTrue(extensionsState.didPrompt());
}
@Test
public void testKnownExetension_OldStyleXml_AllPluginsInstalled_DoesNotPromptUser()
throws Exception {
//
// Given old style xml with knowledge of an extension, all extension plugins are installed,
// the user is not prompted to install plugins. Tests support for tools with the old xml
// migrating to the new xml.
//
// install extension with a plugin
extensionsState.addExtension("FooExtension", TestExtensionHelloPlugin.class);
// mark plugin as installed
extensionsState.setInstalled(TestExtensionHelloPlugin.class);
// Call extension manager with old style xml describing the extension. This knows about the
// extension, but not the plugins inside.
Element element = XmlUtilities.fromString("""
<ROOT>
<EXTENSIONS>
<EXTENSION NAME="FooExtension" />
</EXTENSIONS>
</ROOT>
""");
extensionManager.restoreFromXml(element);
// verify no prompt, since the plugins in the extension have already been installed
extensionManager.checkForNewExtensions();
assertFalse(extensionsState.didPrompt());
}
@Test
public void testKnownExtension_UpdatedWithNewPlugins_PromptsUser() throws Exception {
//
// Given an extension known in the xml, with a list of known plugins, and an updated version
// of that extension is installed, the user is prompted to add plugins.
//
// install an extension with one known plugin and one new plugin
extensionsState.addExtension("FooExtension", TestExtensionHelloPlugin.class,
TestExtensionHello2Plugin.class);
// call extension manager with new style xml that only knows about the original plugin
//@formatter:off
Element element = XmlUtilities.fromString(
"""
<ROOT>
<EXTENSIONS>
<EXTENSION NAME="FooExtension">
<PLUGIN CLASS="ghidra.framework.project.tool.testplugins.TestExtensionHelloPlugin.class" />
</EXTENSION>
</EXTENSIONS>
</ROOT>
""");
//@formatter:on
extensionManager.restoreFromXml(element);
// verify user is prompted to add new plugins
extensionManager.checkForNewExtensions();
assertTrue(extensionsState.didPrompt());
}
@Test
public void testSaveToXml_NoExtensions() throws Exception {
Element rootElement = new Element("ROOT");
extensionManager.saveToXml(rootElement);
String expectedXml =
"""
<ROOT>
<EXTENSIONS />
</ROOT>
""".trim();
String actualXml = toString(rootElement);
assertEquals(expectedXml, actualXml);
}
private String toString(Element e) {
XMLOutputter outputter = GenericXMLOutputter.getInstance();
Format format = outputter.getFormat();
format.setLineSeparator(System.lineSeparator());
return outputter.outputString(e);
}
@Test
public void testSaveToXml_OneExtension() throws Exception {
extensionsState.addExtension("FooExtension", TestExtensionHelloPlugin.class);
Element rootElement = new Element("ROOT");
extensionManager.saveToXml(rootElement);
//@formatter:off
String expectedXml =
"""
<ROOT>
<EXTENSIONS>
<EXTENSION NAME="FooExtension">
<PLUGIN CLASS="ghidra.framework.project.tool.testplugins.TestExtensionHelloPlugin" />
</EXTENSION>
</EXTENSIONS>
</ROOT>
""".trim();
//@formatter:on
String actualXml = toString(rootElement);
assertEquals(expectedXml, actualXml);
}
private void createExtensionDirs() throws IOException {
ResourceFile extensionDir = appLayout.getExtensionArchiveDir();
if (!extensionDir.exists()) {
if (!extensionDir.mkdir()) {
throw new IOException("Failed to create extension archive directory for test");
}
}
ResourceFile installDir = appLayout.getExtensionInstallationDirs().get(0);
if (!installDir.exists()) {
if (!installDir.mkdir()) {
throw new IOException("Failed to create extension installation directory for test");
}
}
}
private void deleteExtensionDirs() {
FileUtilities.deleteDir(appLayout.getExtensionArchiveDir().getFile(false));
for (ResourceFile installDir : appLayout.getExtensionInstallationDirs()) {
FileUtilities.deleteDir(installDir.getFile(false));
}
}
private class FakeToolExtensionsEnabledState implements ExtensionsEnabledState {
private Map<String, Set<Class<?>>> extensions = new HashMap<>();
private Set<Class<?>> installedPlugins = new HashSet<>();
private boolean didPrompt = false;
@Override
public Map<String, Set<Class<?>>> getAllKnownExtensions() {
return extensions;
}
@Override
public void removeInstalledPlugins(Set<Class<?>> plugins) {
plugins.removeAll(installedPlugins);
}
@Override
public void propmtToConfigureNewPlugins(Set<Class<?>> plugins) {
didPrompt = true;
}
void addExtension(String name, Class<?>... classes) {
extensions.put(name, Set.of(classes));
}
boolean didPrompt() {
return didPrompt;
}
void setInstalled(Class<TestExtensionHelloPlugin> c) {
installedPlugins.add(c);
}
}
}
@@ -0,0 +1,40 @@
/* ###
* 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.framework.project.tool.testplugins;
import ghidra.app.ExamplesPluginPackage;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.util.Msg;
//@formatter:off
@PluginInfo(
status = PluginStatus.RELEASED,
packageName = ExamplesPluginPackage.NAME,
category = PluginCategoryNames.EXAMPLES,
shortDescription = "Displays 'Hello World'",
description = "Test plugin"
)
//@formatter:on
public class TestExtensionHello2Plugin extends Plugin {
public TestExtensionHello2Plugin(PluginTool tool) {
super(tool);
Msg.info(this, "Test Hello World constructed!");
}
}
@@ -0,0 +1,44 @@
/* ###
* 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.framework.project.tool.testplugins;
import ghidra.app.ExamplesPluginPackage;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.util.Msg;
//@formatter:off
@PluginInfo(
status = PluginStatus.RELEASED,
packageName = ExamplesPluginPackage.NAME,
category = PluginCategoryNames.EXAMPLES,
shortDescription = "Displays 'Hello World'",
description = "Test plugin"
)
//@formatter:on
public class TestExtensionHelloPlugin extends Plugin {
/*
A plugin for testing extension loading
*/
public TestExtensionHelloPlugin(PluginTool tool) {
super(tool);
Msg.info(this, "Test Hello World constructed!");
}
}
@@ -268,8 +268,13 @@ public class ExtensionDetails implements Comparable<ExtensionDetails> {
}
/**
* {@return true if this extension is installed under an installation folder or inside of a
* source control repository folder}
* Returns true if this extension is installed under an installation folder or inside of a
* source control repository folder.
* <p>
* In a repo, extension modules live under repo/Ghidra/Extensions. In an installation, there
* may exist pre-installed extensions under installDir/Ghidra/Extensions.
*
* @return true if this extension is installed under an installation folder
*/
public boolean isInstalledInInstallationFolder() {
if (installDir == null) {
@@ -284,7 +289,10 @@ public class ExtensionDetails implements Comparable<ExtensionDetails> {
return false;
}
// extDirs.get(0) is the user extension dir
// extDirs.get(0) is the user extension dir (e.g., '.config/ghidra/ghidra<version>/Extensions
// The remaining dirs are of the form:
// <repo>/Ghidra/Extensions
// <install dir>/Ghidra/Extensions
return extDirs.stream()
.skip(1)
.anyMatch(
@@ -242,8 +242,8 @@ public class ExtensionUtils {
}
Set<ExtensionDetails> results = new HashSet<>();
findExtensionsInZips(archiveFiles, results);
findExtensionsInFolder(archiveDir.getFile(false), results);
findExtensionsInArchiveZips(archiveFiles, results);
findExtensionsInArchiveSubfolder(archiveDir.getFile(false), results);
return results;
}
@@ -258,7 +258,7 @@ public class ExtensionUtils {
}
}
public static ExtensionDetails createExtensionDetailsFromArchive(ResourceFile resourceFile) {
private static ExtensionDetails createExtensionDetailsFromArchive(ResourceFile resourceFile) {
File file = resourceFile.getFile(false);
if (!isZip(file)) {
@@ -279,7 +279,7 @@ public class ExtensionUtils {
return null;
}
private static void findExtensionsInZips(ResourceFile[] archiveFiles,
private static void findExtensionsInArchiveZips(ResourceFile[] archiveFiles,
Set<ExtensionDetails> results) {
for (ResourceFile file : archiveFiles) {
ExtensionDetails extension = ExtensionUtils.createExtensionDetailsFromArchive(file);
@@ -318,7 +318,7 @@ public class ExtensionUtils {
return null;
}
private static void findExtensionsInFolder(File dir, Set<ExtensionDetails> results) {
private static void findExtensionsInArchiveSubfolder(File dir, Set<ExtensionDetails> results) {
List<File> propFiles = findExtensionPropertyFiles(dir);
for (File propFile : propFiles) {
ExtensionDetails extension = ExtensionUtils.createExtensionFromProperties(propFile);
@@ -326,8 +326,8 @@ public class ExtensionUtils {
continue;
}
// We found this extension in the installation directory, so set the archive path
// property and add to the final set.
// We found this extension in the installation archive directory, so set the archive
// path property and add to the final set.
File extDir = propFile.getParentFile();
extension.setArchivePath(extDir.getAbsolutePath());
@@ -92,7 +92,8 @@ public class XmlUtilities {
int codePoint = xml.codePointAt(offset);
offset += Character.charCount(codePoint);
if ((codePoint < ' ') && (codePoint != 0x09) && (codePoint != 0x0A) && (codePoint != 0x0D)) {
if ((codePoint < ' ') && (codePoint != 0x09) && (codePoint != 0x0A) &&
(codePoint != 0x0D)) {
continue;
}
if (codePoint >= 0x7F) {
@@ -189,10 +190,10 @@ public class XmlUtilities {
/**
* Convert a String into a JDOM {@link Element}.
*
* @param s
* @return
* @throws JDOMException
* @throws IOException
* @param s the xml string
* @return an element
* @throws JDOMException if there is an exception building the element
* @throws IOException if there is an exception building the element
*/
public static Element fromString(String s) throws JDOMException, IOException {
SAXBuilder sax = createSecureSAXBuilder(false, false);
@@ -547,7 +548,7 @@ public class XmlUtilities {
/**
* Parses the given string into a boolean value. Acceptable inputs are
* y,n,true,fase. A null input string will return false (useful if optional
* y,n,true,false. A null input string will return false (useful if optional
* boolean attribute is false by default)
*
* @param boolStr the string to parse into a boolean value
@@ -4,9 +4,9 @@
* 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.
@@ -78,7 +78,7 @@ public class ExtensionInstaller {
}
Extensions extensions = ExtensionUtils.getAllInstalledExtensions();
if (checkForConflictWithDevelopmentExtension(extension, extensions)) {
if (checkForConflictWithPreInstalledExtension(extension, extensions)) {
return false;
}
@@ -235,7 +235,7 @@ public class ExtensionInstaller {
buffy.toString());
}
private static boolean checkForConflictWithDevelopmentExtension(ExtensionDetails newExtension,
private static boolean checkForConflictWithPreInstalledExtension(ExtensionDetails newExtension,
Extensions extensions) {
String name = newExtension.getName();
@@ -94,7 +94,8 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
return false;
}
// Do not allow GUI removal of extensions manually installed in installation directory.
// Do not allow GUI removal of extensions manually installed in installation directory or
// in a repo directory.
ExtensionDetails extension = getSelectedExtension(rowIndex);
if (extension.isInstalledInInstallationFolder()) {
return false;
@@ -0,0 +1,43 @@
/* ###
* 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.framework.project.tool;
import java.util.Map;
import java.util.Set;
/**
* An interface to help describe extensions' enable state for a given tool.
*/
public interface ExtensionsEnabledState {
/**
* {@return a map of all known extensions to a set of their plugins}
*/
public Map<String, Set<Class<?>>> getAllKnownExtensions();
/**
* All plugins installed in the current tool will be removed from the given set. This allows the
* client to have a set of plugins that are not currently installed.
* @param allPlugins the plugins set to update
*/
public void removeInstalledPlugins(Set<Class<?>> allPlugins);
/**
* Shows a window to prompt the user to configure any new extension plugins.
* @param newPlugins the new extension plugins
*/
public void propmtToConfigureNewPlugins(Set<Class<?>> newPlugins);
}
@@ -44,7 +44,7 @@ public class GhidraTool extends PluginTool {
private FileOpenDropHandler fileOpenDropHandler;
private DockingAction configureToolAction;
private ExtensionManager extensionManager;
private ToolExtensionsStatusManager extensionManager;
private boolean hasBeenShown;
/**
@@ -72,9 +72,10 @@ public class GhidraTool extends PluginTool {
* extension manager.
* @return the extension manager
*/
private ExtensionManager getExtensionManager() {
private ToolExtensionsStatusManager getExtensionManager() {
if (extensionManager == null) {
extensionManager = new ExtensionManager(this);
ToolExtensionsEnabledState state = new ToolExtensionsEnabledState(this);
extensionManager = new ToolExtensionsStatusManager(state);
}
return extensionManager;
}
@@ -17,52 +17,59 @@ package ghidra.framework.project.tool;
import java.io.File;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import org.jdom2.Element;
import docking.widgets.OptionDialog;
import generic.json.Json;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.dialog.PluginInstallerDialog;
import ghidra.framework.plugintool.util.PluginDescription;
import ghidra.util.NumericUtilities;
import ghidra.util.SystemUtilities;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.extensions.ExtensionDetails;
import ghidra.util.extensions.ExtensionUtils;
import ghidra.util.xml.XmlUtilities;
import utilities.util.FileUtilities;
/**
* A class to manage saving and restoring of known extension used by this tool.
* The default extension state for a {@link PluginTool}.
*/
class ExtensionManager {
private static final String EXTENSION_ATTRIBUTE_NAME_ENCODED = "ENCODED_NAME";
private static final String EXTENSION_ATTRIBUTE_NAME = "NAME";
private static final String EXTENSIONS_XML_NAME = "EXTENSIONS";
private static final String EXTENSION_ELEMENT_NAME = "EXTENSION";
class ToolExtensionsEnabledState implements ExtensionsEnabledState {
private PluginTool tool;
private Set<Class<?>> newExtensionPlugins = new HashSet<>();
ExtensionManager(PluginTool tool) {
ToolExtensionsEnabledState(PluginTool tool) {
this.tool = tool;
}
void checkForNewExtensions() {
if (newExtensionPlugins.isEmpty()) {
return;
@Override
public Map<String, Set<Class<?>>> getAllKnownExtensions() {
Set<ExtensionDetails> extensions = getExtensions();
if (extensions.isEmpty()) {
return Map.of();
}
propmtToConfigureNewPlugins(newExtensionPlugins);
newExtensionPlugins.clear();
Map<String, Set<Class<?>>> plugins = new HashMap<>();
Set<PluginPath> pluginPaths = getAllPluginPaths();
for (ExtensionDetails extension : extensions) {
Set<Class<?>> classes = findPluginsLoadedFromExtension(extension, pluginPaths);
plugins.put(extension.getName(), classes);
}
return plugins;
}
private void propmtToConfigureNewPlugins(Set<Class<?>> plugins) {
@Override
public void removeInstalledPlugins(Set<Class<?>> plugins) {
List<Plugin> activePlugins = tool.getManagedPlugins();
for (Plugin plugin : activePlugins) {
Class<? extends Plugin> clazz = plugin.getClass();
plugins.remove(clazz);
}
}
@Override
public void propmtToConfigureNewPlugins(Set<Class<?>> plugins) {
// Offer the user a chance to configure any newly discovered plugins
int option = OptionDialog.showYesNoDialog(tool.getToolFrame(), "New Plugins Found!",
"New extension plugins detected. Would you like to configure them?");
@@ -74,130 +81,7 @@ class ExtensionManager {
}
}
void saveToXml(Element xml) {
Set<ExtensionDetails> installedExtensions = ExtensionUtils.getActiveInstalledExtensions();
Element extensionsParent = new Element(EXTENSIONS_XML_NAME);
for (ExtensionDetails ext : installedExtensions) {
Element child = new Element(EXTENSION_ELEMENT_NAME);
String name = ext.getName();
if (XmlUtilities.hasInvalidXMLCharacters(name)) {
child.setAttribute(EXTENSION_ATTRIBUTE_NAME_ENCODED, NumericUtilities
.convertBytesToString(name.getBytes(StandardCharsets.UTF_8)));
}
else {
child.setAttribute(EXTENSION_ATTRIBUTE_NAME, name);
}
extensionsParent.addContent(child);
}
xml.addContent(extensionsParent);
}
void restoreFromXml(Element xml) {
Set<ExtensionDetails> installedExtensions = getExtensions();
if (installedExtensions.isEmpty()) {
return;
}
Set<String> knownExtensionNames = getKnownExtensions(xml);
Set<ExtensionDetails> newExtensions = new HashSet<>(installedExtensions);
for (ExtensionDetails ext : installedExtensions) {
if (knownExtensionNames.contains(ext.getName())) {
newExtensions.remove(ext);
}
}
// Get a list of all plugins contained in those extensions. If there are none, then either
// none of the extensions has any plugins, or Ghidra hasn't been restarted since installing
// the extension(s), so none of the plugin classes have been loaded. In either case, there
// is nothing more to do.
Set<Class<?>> newPlugins = findLoadedPlugins(newExtensions);
newExtensionPlugins.addAll(newPlugins);
}
private Set<ExtensionDetails> getExtensions() {
Set<ExtensionDetails> installedExtensions = ExtensionUtils.getActiveInstalledExtensions();
return installedExtensions.stream()
.filter(e -> !e.isInstalledInInstallationFolder())
.collect(Collectors.toSet());
}
private Set<String> getKnownExtensions(Element xml) {
Set<String> knownExtensionNames = new HashSet<>();
Element extensionsParent = xml.getChild(EXTENSIONS_XML_NAME);
if (extensionsParent == null) {
return knownExtensionNames;
}
Iterator<?> it = extensionsParent.getChildren(EXTENSION_ELEMENT_NAME).iterator();
while (it.hasNext()) {
Element child = (Element) it.next();
String encodedValue = child.getAttributeValue(EXTENSION_ATTRIBUTE_NAME_ENCODED);
if (encodedValue != null) {
byte[] bytes = NumericUtilities.convertStringToBytes(encodedValue);
String decoded = new String(bytes, StandardCharsets.UTF_8);
knownExtensionNames.add(decoded);
}
else {
String name = child.getAttributeValue(EXTENSION_ATTRIBUTE_NAME);
knownExtensionNames.add(name);
}
}
return knownExtensionNames;
}
/**
* Finds all {@link PluginDescription} objects that match a given set of plugin classes. This
* effectively tells the caller which of the given plugins have been loaded by the class loader.
* <p>
* Note that this method does not take path/package information into account when finding
* plugins; in the example above, if there is more than one plugin with the name "FooPlugin",
* only one will be found (the one found is not guaranteed to be the first).
*
* @param plugins the list of plugin classes to search for
* @return list of plugin descriptions
*/
private List<PluginDescription> getPluginDescriptions(Set<Class<?>> plugins) {
// First define the list of plugin descriptions to return
List<PluginDescription> descriptions = new ArrayList<>();
// Get all plugins that have been loaded
PluginsConfiguration pluginsConfiguration = tool.getPluginsConfiguration();
List<PluginDescription> allPluginDescriptions =
pluginsConfiguration.getManagedPluginDescriptions();
// see if an entry exists in the list of all loaded plugins
for (Class<?> plugin : plugins) {
String pluginName = plugin.getSimpleName();
Optional<PluginDescription> desc = allPluginDescriptions.stream()
.filter(d -> (pluginName.equals(d.getName())))
.findAny();
if (desc.isPresent()) {
descriptions.add(desc.get());
}
}
return descriptions;
}
private static Set<Class<?>> findLoadedPlugins(Set<ExtensionDetails> extensions) {
Set<PluginPath> pluginPaths = getPluginPaths();
Set<Class<?>> extensionPlugins = new HashSet<>();
for (ExtensionDetails extension : extensions) {
Set<Class<?>> classes = findPluginsLoadedFromExtension(extension, pluginPaths);
extensionPlugins.addAll(classes);
}
return extensionPlugins;
}
private static Set<PluginPath> getPluginPaths() {
private static Set<PluginPath> getAllPluginPaths() {
Set<PluginPath> paths = new HashSet<>();
List<Class<? extends Plugin>> plugins = ClassSearcher.getClasses(Plugin.class);
for (Class<? extends Plugin> plugin : plugins) {
@@ -206,6 +90,33 @@ class ExtensionManager {
return paths;
}
private static Set<ExtensionDetails> getExtensions() {
Set<ExtensionDetails> installedExtensions = ExtensionUtils.getActiveInstalledExtensions();
return installedExtensions.stream()
.filter(e -> !isRepoExtension(e))
.collect(Collectors.toSet());
}
/**
* We wish to ignore extension modules that live in the repo installation dir. This keeps
* developers from getting prompted while developing.
* @param e the extension
* @return true if not a development extension
*/
private static boolean isRepoExtension(ExtensionDetails e) {
// Repo extensions live in a known installation folder in development mode. They do not
// exist in a release.
if (SystemUtilities.isInDevelopmentMode()) {
if (e.isInstalledInInstallationFolder()) {
// Checking for a build file is an easy way to find repo extensions
File dir = e.getInstallDir();
File buildFile = new File(dir, "build.gradle");
return buildFile.exists();
}
}
return false;
}
/**
* Finds all plugin classes loaded from a particular extension folder.
* <p>
@@ -245,6 +156,42 @@ class ExtensionManager {
return result;
}
/**
* Finds all {@link PluginDescription} objects that match a given set of plugin classes. This
* effectively tells the caller which of the given plugins have been loaded by the class loader.
* <p>
* Note that this method does not take path/package information into account when finding
* plugins; in the example above, if there is more than one plugin with the name "FooPlugin",
* only one will be found (the one found is not guaranteed to be the first).
*
* @param plugins the list of plugin classes to search for
* @return list of plugin descriptions
*/
private List<PluginDescription> getPluginDescriptions(Set<Class<?>> plugins) {
// First define the list of plugin descriptions to return
List<PluginDescription> descriptions = new ArrayList<>();
// Get all plugins that have been loaded
PluginsConfiguration pluginsConfiguration = tool.getPluginsConfiguration();
List<PluginDescription> allPluginDescriptions =
pluginsConfiguration.getManagedPluginDescriptions();
// see if an entry exists in the list of all loaded plugins
for (Class<?> plugin : plugins) {
String pluginName = plugin.getSimpleName();
Optional<PluginDescription> desc = allPluginDescriptions.stream()
.filter(d -> (pluginName.equals(d.getName())))
.findAny();
if (desc.isPresent()) {
descriptions.add(desc.get());
}
}
return descriptions;
}
private static class PluginPath {
private Class<? extends Plugin> pluginClass;
private String pluginLocation;
@@ -0,0 +1,203 @@
/* ###
* 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.framework.project.tool;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jdom2.Element;
import ghidra.util.NumericUtilities;
import ghidra.util.Swing;
import ghidra.util.xml.XmlUtilities;
/**
* A class to manage saving and restoring of known extension used by a tool.
*/
class ToolExtensionsStatusManager {
private static final String XML_TAG_EXTENSIONS = "EXTENSIONS";
private static final String XML_TAG_EXTENSION = "EXTENSION";
private static final String XML_TAG_PLUGIN = "PLUGIN";
private static final String XML_ATTR_EXTENSION_NAME_ENCODED = "ENCODED_NAME";
private static final String XML_ATTR_EXTENSION_NAME = "NAME";
private static final String XML_ATTR_EXTENSION_PLUGIN_CLASS = "CLASS";
private Set<Class<?>> newExtensionPlugins = new HashSet<>();
private ExtensionsEnabledState extensionsState;
ToolExtensionsStatusManager(ExtensionsEnabledState extensionState) {
this.extensionsState = extensionState;
}
void checkForNewExtensions() {
if (newExtensionPlugins.isEmpty()) {
return;
}
// Run later to not block the opening of the tool.
Swing.runLater(() -> {
extensionsState.propmtToConfigureNewPlugins(newExtensionPlugins);
newExtensionPlugins.clear();
});
}
void saveToXml(Element xml) {
Map<String, Set<Class<?>>> pluginsByExtension =
extensionsState.getAllKnownExtensions();
Element extensionsParent = new Element(XML_TAG_EXTENSIONS);
Set<Entry<String, Set<Class<?>>>> entries = pluginsByExtension.entrySet();
for (Entry<String, Set<Class<?>>> entry : entries) {
String name = entry.getKey();
Element extensionsElement = new Element(XML_TAG_EXTENSION);
setExtensionName(extensionsElement, name);
extensionsParent.addContent(extensionsElement);
Set<Class<?>> plugins = entry.getValue();
for (Class<?> clazz : plugins) {
String className = clazz.getName();
Element pluginElement = new Element(XML_TAG_PLUGIN);
pluginElement.setAttribute(XML_ATTR_EXTENSION_PLUGIN_CLASS, className);
extensionsElement.addContent(pluginElement);
}
}
xml.addContent(extensionsParent);
}
void restoreFromXml(Element xml) {
/*
1) Grab all extension plugins currently found on the classpath. This will include all
old and new extensions.
2) Grab all previously known extensions and plugins.
3) Find all entirely new extensions or extensions that have new plugins added.
4) Filter plugins by those already installed.
5) Save the new extension plugins for later user prompting when saving to xml.
*/
Map<String, Set<Class<?>>> extensionPlugins = extensionsState.getAllKnownExtensions();
Map<String, ExtensionMemento> xmlMementosByName = getKnownExtensions(xml);
Set<String> names = extensionPlugins.keySet();
Set<String> newExtensions = new HashSet<>(names);
for (String name : names) {
ExtensionMemento xmlMemento = xmlMementosByName.get(name);
if (xmlMemento == null) {
continue; // new extension
}
// The extension is known. If it doesn't have new plugins, then we can remove it from
// the new extensions, assuming it has not changed.
if (!hasNewPlugins(xmlMemento, extensionPlugins)) {
newExtensions.remove(name);
}
}
Set<Class<?>> newPlugins = newExtensions.stream()
.map(name -> extensionPlugins.get(name)) // classes by extension name
.flatMap(set -> set.stream()) // map all sets to a single stream
.collect(Collectors.toSet());
extensionsState.removeInstalledPlugins(newPlugins);
// Get all plugins contained in the 'new' extensions. If there are none, then
// either none of the extensions has any plugins, or Ghidra hasn't been restarted since
// installing the extension(s), so none of the plugin classes have been loaded.
newExtensionPlugins.addAll(newPlugins);
}
private static boolean hasNewPlugins(ExtensionMemento xmlMemento,
Map<String, Set<Class<?>>> pluginsByExtensionName) {
// If the xml memento is empty, it is either the old style xml that did not save plugin
// names or is the new style xml, but the extension did not previously have any plugins. In
// this case, we want to prompt the user if there are plugins to install.
Set<String> xmlClassNames = xmlMemento.pluginClassNames();
Set<Class<?>> cpPluginClasses = pluginsByExtensionName.get(xmlMemento.name());
if (xmlClassNames.isEmpty() && !cpPluginClasses.isEmpty()) {
return true;
}
List<String> cpNames = cpPluginClasses.stream()
.map(c -> c.getName())
.collect(Collectors.toList());
cpNames.removeAll(xmlClassNames);
return !cpNames.isEmpty();
}
private static Map<String, ExtensionMemento> getKnownExtensions(Element xml) {
Set<ExtensionMemento> mementos = new HashSet<>();
Element extensionsParent = xml.getChild(XML_TAG_EXTENSIONS);
if (extensionsParent == null) {
return Map.of();
}
for (Element child : extensionsParent.getChildren(XML_TAG_EXTENSION)) {
List<Element> plugins = child.getChildren(XML_TAG_PLUGIN);
Set<String> pluginClasses = getExtensionPluginClasses(plugins);
String extensionName = readExtensionName(child);
mementos.add(new ExtensionMemento(extensionName, pluginClasses));
}
return mementos.stream()
.collect(Collectors.toMap(
ExtensionMemento::name,
Function.identity()));
}
private static Set<String> getExtensionPluginClasses(List<Element> plugins) {
Set<String> pluginNames = new HashSet<>();
for (Element element : plugins) {
String className = element.getAttributeValue(XML_ATTR_EXTENSION_PLUGIN_CLASS);
pluginNames.add(className);
}
return pluginNames;
}
private static String readExtensionName(Element element) {
String encodedValue = element.getAttributeValue(XML_ATTR_EXTENSION_NAME_ENCODED);
if (encodedValue != null) {
byte[] bytes = NumericUtilities.convertStringToBytes(encodedValue);
return new String(bytes, StandardCharsets.UTF_8);
}
return element.getAttributeValue(XML_ATTR_EXTENSION_NAME);
}
private static void setExtensionName(Element element, String name) {
if (XmlUtilities.hasInvalidXMLCharacters(name)) {
element.setAttribute(XML_ATTR_EXTENSION_NAME_ENCODED, NumericUtilities
.convertBytesToString(name.getBytes(StandardCharsets.UTF_8)));
}
else {
element.setAttribute(XML_ATTR_EXTENSION_NAME, name);
}
}
private static record ExtensionMemento(String name, Set<String> pluginClassNames) {
}
}
@@ -4,9 +4,9 @@
* 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.
@@ -48,10 +48,6 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
private ApplicationLayout appLayout;
/*
* Create dummy archive and installation folders in the temp space that we can populate
* with extensions.
*/
@Before
public void setup() throws IOException {
@@ -60,16 +56,10 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
setErrorGUIEnabled(false);
// clear static caching of extensions
ExtensionUtils.clearCache();
appLayout = Application.getApplicationLayout();
FileUtilities.deleteDir(appLayout.getExtensionArchiveDir().getFile(false));
for (ResourceFile installDir : appLayout.getExtensionInstallationDirs()) {
FileUtilities.deleteDir(installDir.getFile(false));
}
ExtensionUtils.clearCache();
deleteExtensionDirs();
createExtensionDirs();
}
@@ -239,7 +229,8 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
}
@Test
public void testCleanupUninstalledExtions_WithExtensionMarkedForUninstall() throws Exception {
public void testCleanupUninstalledExtensions_WithExtensionMarkedForUninstall()
throws Exception {
File externalFolder = createExternalExtensionInFolder();
assertTrue(ExtensionInstaller.install(externalFolder));
@@ -256,7 +247,8 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
}
@Test
public void testCleanupUninstalledExtions_SomeExtensionMarkedForUninstall() throws Exception {
public void testCleanupUninstalledExtensions_SomeExtensionMarkedForUninstall()
throws Exception {
List<File> extensionFolders = createTwoExternalExtensionsInFolder();
assertTrue(ExtensionInstaller.install(extensionFolders.get(0)));
@@ -280,7 +272,7 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
}
@Test
public void testCleanupUninstalledExtions_NoExtensionsMarkedForUninstall() throws Exception {
public void testCleanupUninstalledExtensions_NoExtensionsMarkedForUninstall() throws Exception {
File externalFolder = createExternalExtensionInFolder();
assertTrue(ExtensionInstaller.install(externalFolder));
@@ -426,14 +418,14 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
/*
Create a zip file that looks something like this:
/
{Extension Name 1}/
extension.properties
{Extension Name 2}/
extension.properties
*/
errorsExpected(() -> {
@@ -521,6 +513,13 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
}
}
private void deleteExtensionDirs() {
FileUtilities.deleteDir(appLayout.getExtensionArchiveDir().getFile(false));
for (ResourceFile installDir : appLayout.getExtensionInstallationDirs()) {
FileUtilities.deleteDir(installDir.getFile(false));
}
}
/*
* Verifies that the installation folder is empty.
*/