[java-identity-provider] 02/02: IDP-1683 Substantial rewrite of Plugin Installer

Rod Widdowson rdw at steadingsoftware.com
Sat Oct 10 12:55:45 UTC 2020


This is an automated email from the git hooks/post-receive script.

rdw pushed a commit to branch main
in repository java-identity-provider.

View the commit online:
http://git.shibboleth.net/view/?p=java-identity-provider.git;a=commit;h=747befa40847abd385b2940fda06502c51e495c0

commit 747befa40847abd385b2940fda06502c51e495c0
Author: Rod Widdowson <rdw at steadingsoftware.com>
AuthorDate: Sat Oct 10 13:51:32 2020 +0100

    IDP-1683 Substantial rewrite of Plugin Installer
    
    https://issues.shibboleth.net/jira/browse/IDP-1683
    
    Add transactional support for update and install.
    
    Recast the lifetimes of the ClasspathLoaders to allow
    Module enabling to work with an active ClasspathLoader.
    
    Do the Module enable/disable thing (transactionally).
    
    Move the Module probing to inside the transactional scope
    and after the uninstall part of an upgrade, also give the
    Service loader a ClasspathLoader which contains the plugins.
---
 .../idp/installer/plugin/impl/PluginInstaller.java | 309 ++++++++++++++-------
 .../plugin/impl/PluginInstallerSupport.java        |  34 +++
 .../plugin/impl/RollbackPluginInstall.java         |  15 +-
 .../idp/installer/plugin/impl/RollbackTester.java  |   2 +-
 .../src/test/resources/credentials/truststore.asc  | 137 +++++++++
 5 files changed, 380 insertions(+), 117 deletions(-)

diff --git a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstaller.java b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstaller.java
index cc6de0ffe..651d118a2 100644
--- a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstaller.java
+++ b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstaller.java
@@ -122,8 +122,8 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
     /** What to use to download things. */
     private HttpClient httpClient;
  
-    /** Files that were copied - to handle rollback. */
-    @Nonnull private List<Path> copiedFiles = new ArrayList<>();
+    /** Dumping space for renamed files. */
+    @Nonnull private Path renamePath;
 
     /** What was installed - this is setup by {@link #loadCopiedFiles()}. */
     @Nullable private List<String> installedContents;
@@ -134,6 +134,12 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
     /** The Module Context. */
     @NonnullAfterInit private ModuleContext moduleContext;
 
+    /** The "plugins" classpath loader. AutoClosed. */
+    private URLClassLoader installedPluginLoader;
+
+    /** The "plugin under construction" classpath loader. AutoClosed. */
+    private URLClassLoader installingPluginLoader;
+
     /** The securiotyParams for the module context. */
     private HttpClientSecurityParameters securityParams;
 
@@ -222,18 +228,21 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
         unpack(base, fileName);
         setupPluginId();
         checkSignature(base, fileName);
-        getDescription();
+        setupDescriptionFromDistribution();
         LOG.info("Installing Plugin {} version {}.{}.{}", pluginId,
                 description.getMajorVersion(),description.getMinorVersion(), description.getPatchVersion());
 
-        checkRequiredModules();
-        
         final Path myWebApp = idpHome.resolve("dist").resolve("edit-webapp-" + pluginId);
 
-        uninstallOld(myWebApp);
-        installWebapp(myWebApp);
-        InstallerSupport.setReadOnly(myWebApp, true);
-        saveCopiedFiles();
+        try (final RollbackPluginInstall rollBack = new RollbackPluginInstall(moduleContext)) {
+            uninstallOld(rollBack, myWebApp);
+
+            checkRequiredModules();
+            installNew(rollBack, myWebApp);
+
+            saveCopiedFiles(rollBack.getFilesCopied());
+            rollBack.completed();
+        }
 
         final BuildWar builder = new BuildWar(idpHome);
         try {
@@ -266,65 +275,61 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
                 throw new BuildException(e);
             }
         }
-        for (final String content: getInstalledContents()) {
-            final Path p = Path.of(content);
-            if (!Files.exists(p)) {
-                continue;
+        if (getVersionFromContents() == null) {
+            LOG.warn("Installed contents for {} not found", pluginId);
+        } else {
+            for (final String content: getInstalledContents()) {
+                final Path p = Path.of(content);
+                if (!Files.exists(p)) {
+                    continue;
+                }
+                try {
+                    InstallerSupport.setReadOnly(p, false);
+                    Files.deleteIfExists(p);
+                } catch (final IOException e) {
+                    LOG.warn("Could not delete {}, deferring the delete", content, e);
+                    p.toFile().deleteOnExit();
+                }
             }
+
+            final BuildWar builder = new BuildWar(idpHome);
             try {
-                InstallerSupport.setReadOnly(p, false);
-                Files.deleteIfExists(p);
-            } catch (final IOException e) {
-                LOG.warn("Could not delete {}, deferring the delete", content, e);
-                p.toFile().deleteOnExit();
+                builder.initialize();
+            } catch (final ComponentInitializationException e) {
+                throw new BuildException(e);
             }
+            builder.execute();
+            LOG.info("Removed resources for {} from the war", pluginId);
         }
-
-        final BuildWar builder = new BuildWar(idpHome);
-        try {
-            builder.initialize();
-        } catch (final ComponentInitializationException e) {
-            throw new BuildException(e);
-        }
-        builder.execute();
-        LOG.info("Removed resources for {} from the war", pluginId);
     }
 
     /** Get hold of the {@link IdPPlugin} for this plugin.
      * @throws BuildException if badness is happens.
      */
-    private void getDescription() throws BuildException {
-        final List<URL> urls = new ArrayList<>();
-        final Path libDir = distribution.resolve("edit-webapp").resolve("WEB-INF").resolve("lib");
-
-        try (final DirectoryStream<Path> libDirPaths = Files.newDirectoryStream(libDir)){
-            for (final Path jar : libDirPaths) {
-                urls.add(jar.toUri().toURL());
-            }
-           try (final URLClassLoader loader = new URLClassLoader(urls.toArray(URL[]::new))){
-
-               final ServiceLoader<IdPPlugin> plugins = ServiceLoader.load(IdPPlugin.class, loader);
-               final Optional<IdPPlugin> first = plugins.findFirst();
-               if (first.isEmpty()) {
-                   LOG.error("No Plugin services found in plugin distribution");
-                   throw new BuildException("No Plugin services found in plugin distribution");
-               }
-               for (final IdPPlugin plugin:plugins) {
-                   LOG.debug("Found Service announcing itself as {}", plugin.getPluginId() );
-                   if (pluginId.equals(plugin.getPluginId())) {
-                       description = plugin;
-                       return;
-                   }
-                   LOG.trace("Did not match {}", pluginId);
-               }
-               LOG.error("Looking in plugin distibution for a plugin called {}, but found a plugin called {}.", 
-                      pluginId, first.get().getPluginId());
+    private void setupDescriptionFromDistribution() throws BuildException {
+       final ServiceLoader<IdPPlugin> plugins;
+       try {
+           plugins = ServiceLoader.load(IdPPlugin.class, getDistributionLoader());
+       } catch (final IOException e) {
+           LOG.error("Error loading descritpion");
+           throw new BuildException(e);
+       }
+       final Optional<IdPPlugin> first = plugins.findFirst();
+       if (first.isEmpty()) {
+           LOG.error("No Plugin services found in plugin distribution");
+           throw new BuildException("No Plugin services found in plugin distribution");
+       }
+       for (final IdPPlugin plugin:plugins) {
+           LOG.debug("Found Service announcing itself as {}", plugin.getPluginId() );
+           if (pluginId.equals(plugin.getPluginId())) {
+               description = plugin;
+               return;
            }
-           throw new BuildException("Could not locate PluginDescription");
-        } catch (final IOException e) {
-            LOG.error("Could not get description of {} from {}", pluginId, libDir, e);
-            throw new BuildException(e);
-        }
+           LOG.trace("Did not match {}", pluginId);
+       }
+       LOG.error("Looking in plugin distibution for a plugin called {}, but found a plugin called {}.",
+              pluginId, first.get().getPluginId());
+       throw new BuildException("Could not locate PluginDescription");
     }
 
     /** What files were installed to webapp for this plugin?
@@ -350,25 +355,25 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
      * @throws BuildException if any required modules are missing or disabled
      */
     private void checkRequiredModules() throws BuildException {
-        
-        // TODO: maybe this belongs in the CLI rather than the utility class?
-        // OTOH if the update process is recoverable then a failed update
-        // should rollback the uninstall of the original...
-        
         final Set<String> requiredModules = new HashSet<>(description.getRequiredModules());
         
-        final Iterator<IdPModule> modules = ServiceLoader.load(IdPModule.class).iterator();
-        while (modules.hasNext() && !requiredModules.isEmpty()) {
-            try {
-                final IdPModule module = modules.next();
-                if (requiredModules.contains(module.getId())) {
-                    if (module.isEnabled(moduleContext)) {
-                        requiredModules.remove(module.getId());
+        try (final URLClassLoader loader = getInstalledPluginLoader()) {
+            final Iterator<IdPModule> modules = ServiceLoader.load(IdPModule.class).iterator();
+            while (modules.hasNext() && !requiredModules.isEmpty()) {
+                try {
+                    final IdPModule module = modules.next();
+                    if (requiredModules.contains(module.getId())) {
+                        if (module.isEnabled(moduleContext)) {
+                            requiredModules.remove(module.getId());
+                        }
                     }
+                } catch (final ServiceConfigurationError e) {
+                    LOG.error("Unable to instantiate IdPModule", e);
                 }
-            } catch (final ServiceConfigurationError e) {
-                LOG.error("Unable to instantiate IdPModule", e);
             }
+        } catch (final IOException e) {
+            LOG.error("Error looking for required modules");
+            throw new BuildException(e);
         }
         
         if (!requiredModules.isEmpty()) {
@@ -379,28 +384,77 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
 
     /** Copy the webapp folder from the distribution to the per plugin
      * location inside dist.
+     * @param rollBack Roll Back Context
      * @param myWebApp Where to put it.
      * @throws BuildException if badness is detected.
      */
-    private void installWebapp(final Path myWebApp) throws BuildException {
+    private void installNew(final RollbackPluginInstall rollBack, final Path myWebApp) throws BuildException {
         final Path from = distribution.resolve("edit-webapp");
         if (PluginInstallerSupport.detectDuplicates(from, myWebApp)) {
             throw new BuildException("Install would overwrite filess");
         }
-        PluginInstallerSupport.copyWithLogging(from, myWebApp, copiedFiles);
+        PluginInstallerSupport.copyWithLogging(from, myWebApp, rollBack.getFilesCopied());
+        InstallerSupport.setReadOnly(myWebApp, true);
+
+        String moduleId = null;
+        try {
+            for (final IdPModule module: description.getDisableOnRemoval()) {
+                moduleId = module.getId();
+                if (!module.isEnabled(moduleContext)) {
+                    module.enable(moduleContext);
+                    rollBack.getModulesEnabled().add(module);
+                }
+            }
+        } catch (final ModuleException e) {
+            LOG.error("Error enabling {}", moduleId);
+            throw new BuildException(e);
+        }
     }
 
     /** Uninstall the old version of the plugin.
-     * @param myWebApp where to delete */
-    private void uninstallOld(final Path myWebApp) {
-        InstallerSupport.setReadOnly(myWebApp, false);
-        PluginInstallerSupport.deleteTree(myWebApp);
+     * @param rollback Rollback Context
+     * @param myWebApp where to delete 
+     * @throws BuildException on IO or module errors */
+    private void uninstallOld(final RollbackPluginInstall rollback, final Path myWebApp) throws BuildException {
+        final IdPPlugin oldPlugin = getInstalledPlugin(pluginId);
+        if (oldPlugin == null) {
+            LOG.debug("{} not installed. No modules disabled", pluginId);
+        } else {
+            String moduleId = null;
+            try {
+                for (final IdPModule module: oldPlugin.getDisableOnRemoval()) {
+                    moduleId = module.getId();
+                    if (module.isEnabled(moduleContext)) {
+                        module.disable(moduleContext, true);
+                        rollback.getModulesDisabled().add(module);
+                    }
+                }
+            } catch (final ModuleException e) {
+                LOG.error("Error disabling {}", moduleId);
+                throw new BuildException(e);
+            }
+        }
+
+        if (getVersionFromContents() == null) {
+            LOG.debug("{} not installed. files renamed", pluginId);
+        } else {
+            InstallerSupport.setReadOnly(myWebApp, false);
+            try {
+                PluginInstallerSupport.renameToTree(myWebApp, renamePath,
+                        getInstalledContents(),
+                        rollback.getFilesRenamedAway());
+            } catch (final IOException e) {
+                LOG.error("Error uninstalling plugin");
+                throw new BuildException(e);
+            }
+        }
     }
 
     /** Stream the copy list to a property file and empty it.
+     * @param copiedFiles The copied files
      * @throws BuildException If we hit an IO exception
      */
-    private void saveCopiedFiles() throws BuildException {
+    private void saveCopiedFiles(final List<Path> copiedFiles) throws BuildException {
         try {
             final Path parent = idpHome.resolve("dist").resolve("plugin-contents");
             Files.createDirectories(parent);
@@ -416,7 +470,6 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
             try (final BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(outFile))) {
                 props.store(out, "Files Copied "  + Instant.now());
             }
-            copiedFiles = new ArrayList<>();
         } catch (final IOException e) {
             LOG.error("Error saving list of copied files.", e);
             throw new BuildException(e);
@@ -435,7 +488,7 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
         final Properties props = new Properties();
         final File inFile = parent.resolve(pluginId).toFile();
         if (!inFile.exists()) {
-            LOG.error("Contents file for plugin {} ({}) does not exist", pluginId, inFile.getAbsolutePath());
+            LOG.debug("Contents file for plugin {} ({}) does not exist", pluginId, inFile.getAbsolutePath());
             installedContents = Collections.emptyList();
             return;
         }
@@ -597,7 +650,7 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
      * @throws BuildException if badness is detected.
      */
     private void setupPluginId() throws BuildException {
-        final File propertyFile = distribution.resolve("bootstrap").resolve("id.property").toFile();
+        final File propertyFile = distribution.resolve("bootstrap").resolve("plugin.properties").toFile();
         if (!propertyFile.exists()) {
             LOG.error("Could not locate identity of plugin. "
                     + "Identity file 'bootstrap/id.property' not present in plugin distribution.");
@@ -606,7 +659,7 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
         try (final InputStream inStream = new BufferedInputStream(new FileInputStream(propertyFile))) {
             final Properties idProperties = new Properties();
             idProperties.load(inStream);
-            final String id = StringSupport.trimOrNull(idProperties.getProperty("pluginid"));
+            final String id = StringSupport.trimOrNull(idProperties.getProperty("plugin.id"));
             if (id == null) {
                 LOG.error("Identity property file 'bootstrap/id.property' did not contain 'pluginid' property");
                 throw new BuildException("No property in ID file");
@@ -681,6 +734,53 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
         moduleContext = new ModuleContext(idpHome);
         moduleContext.setHttpClientSecurityParameters(securityParams);
         moduleContext.setHttpClient(httpClient);
+        renamePath = idpHome.resolve("dist").resolve("plugin-rollback");
+    }
+
+    /** Generate a {@link URLClassLoader} which looks at the
+     * installed WEB-INF/lib in addition to the dist webapp and bin/lib directories.
+     * @return an appropriate loader
+     * @throws IOException if a directory traversal fails.
+     */
+    private synchronized URLClassLoader getInstalledPluginLoader() throws IOException {
+        if (installedPluginLoader != null) {
+            return installedPluginLoader;
+        }
+        final List<URL> urls = new ArrayList<>();
+        try (final DirectoryStream<Path> webAppList =
+                Files.newDirectoryStream(idpHome.resolve("dist"), "edit-webapp-*")) {
+            for (final Path webApp : webAppList) {
+                try (final DirectoryStream<Path> webInfLibs =
+                        Files.newDirectoryStream(webApp.resolve("WEB-INF").resolve("lib"))) {
+                    for (final Path jar : webInfLibs) {
+                        urls.add(jar.toUri().toURL());
+                    }
+                }
+            }
+        }
+        installedPluginLoader = new URLClassLoader(urls.toArray(URL[]::new));
+        return installedPluginLoader;        
+    }
+
+    
+    /** Generate a {@link URLClassLoader} which looks at the
+     * installing WEB-INF.
+     * @return an appropriate loader
+     * @throws IOException if a directory traversal fails.
+     */
+    private synchronized URLClassLoader getDistributionLoader() throws IOException {
+        if (installingPluginLoader!= null) {
+            return installingPluginLoader;
+        }
+        final List<URL> urls = new ArrayList<>();
+        final Path libDir = distribution.resolve("edit-webapp").resolve("WEB-INF").resolve("lib");
+        try (final DirectoryStream<Path> libDirPaths = Files.newDirectoryStream(libDir)){
+            for (final Path jar : libDirPaths) {
+                urls.add(jar.toUri().toURL());
+            }
+            installingPluginLoader  = new URLClassLoader(urls.toArray(URL[]::new));
+            return installingPluginLoader;
+        }
     }
 
     /**
@@ -689,20 +789,7 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
      */
     public List<IdPPlugin> getInstalledPlugins() {
         try {
-            final List<URL> urls = new ArrayList<>();
-
-            try (final DirectoryStream<Path> webAppList = 
-                    Files.newDirectoryStream(idpHome.resolve("dist"), "edit-webapp-*")) {
-                for (final Path webApp : webAppList) {
-                    try (final DirectoryStream<Path> webInfLibs =
-                            Files.newDirectoryStream(webApp.resolve("WEB-INF").resolve("lib"))) {
-                        for (final Path jar : webInfLibs) {
-                            urls.add(jar.toUri().toURL());
-                        }
-                    }
-                }
-            }
-            try (final URLClassLoader loader = new URLClassLoader(urls.toArray(URL[]::new))){
+            try (final URLClassLoader loader = getInstalledPluginLoader()){
                try (final Stream<Provider<IdPPlugin>> loaderStream =
                        ServiceLoader.load(IdPPlugin.class, loader).stream()) {
                    return loaderStream.map(ServiceLoader.Provider::get).collect(Collectors.toList());
@@ -728,21 +815,27 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
         return null;
     }
 
+    /** close, ignoring errors.
+     * @param what what to close
+     */
+    private void closeSilently(final AutoCloseable what) {
+        if (what == null) {
+            return;
+        }
+        try {
+            what.close();
+        } catch (final Exception e) {
+            LOG.error("Autoclose of {} failed", what, e);
+        }
+    }
+
     /** {@inheritDoc} */
     public void close() {
+        closeSilently(installedPluginLoader);
+        closeSilently(installingPluginLoader);
         PluginInstallerSupport.deleteTree(downloadDirectory);
         PluginInstallerSupport.deleteTree(unpackDirectory);
-        for (final Path p : copiedFiles) {
-            try {
-                if (Files.exists(p)) {
-                    InstallerSupport.setReadOnly(p, false);
-                    Files.delete(p);
-                }
-            } catch (final IOException e) {
-                p.toFile().deleteOnExit();
-                LOG.warn("Failed to delete {}", p, e);
-            }
-        }
+        PluginInstallerSupport.deleteTree(renamePath);
     }
     
 }
diff --git a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstallerSupport.java b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstallerSupport.java
index 2096e4c45..c9b00eb5b 100644
--- a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstallerSupport.java
+++ b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstallerSupport.java
@@ -41,7 +41,9 @@ import org.apache.tools.ant.BuildException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import net.shibboleth.idp.installer.InstallerSupport;
 import net.shibboleth.utilities.java.support.annotation.constraint.Live;
+import net.shibboleth.utilities.java.support.collection.Pair;
 
 /**
  * Support for copying files during plugin manipulation.
@@ -73,6 +75,7 @@ public final class PluginInstallerSupport {
             return;
         }
         LOG.debug("Deleting directory {}", directory);
+        InstallerSupport.setReadOnly(directory, false);
         try {
             Files.walkFileTree(directory, new DeletingVisitor());
         } catch (final IOException e) {
@@ -125,6 +128,37 @@ public final class PluginInstallerSupport {
         pathsCopied.addAll(visitor.getCopiedList());
     }
     
+    /** Rename Files into the provided tree. 
+     * @param fromBase The root directory of the from files
+     * @param toBase The root directory to rename to
+     * @param fromFiles The list of files (inside fromBase) to rename 
+     * @param renames All the work as it is done
+     * @throws IOException If any of the file operations fail
+     */
+    
+    public static void renameToTree(@Nonnull final Path fromBase,
+            @Nonnull final Path toBase,
+            @Nonnull final List<String> fromFiles,
+            @Nonnull @Live final List<Pair<Path, Path>> renames) throws IOException {
+
+        if (!Files.exists(toBase)) {
+            Files.createDirectories(toBase);
+        }
+        for (final String file : fromFiles) {
+            final Path path = Path.of(file);
+            if (!Files.exists(path)) {
+                LOG.info("File {} was not renamed away because it does not exist", file);
+                continue;
+            }
+            final Path relName = fromBase.relativize(path);
+            LOG.trace("Relative name {}", relName);
+            final Path to = toBase.resolve(relName);
+            Files.createDirectories(to.getParent());
+            Files.move(path,to);
+            renames.add(new Pair<>(path, to));
+        }
+    }
+
     /**
      * A @{link {@link FileVisitor} which detects (and logs) whether a copy would overwrite.
      */
diff --git a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/RollbackPluginInstall.java b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/RollbackPluginInstall.java
index 88fa9f144..a00acd4c8 100644
--- a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/RollbackPluginInstall.java
+++ b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/RollbackPluginInstall.java
@@ -19,7 +19,6 @@ package net.shibboleth.idp.installer.plugin.impl;
 
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
-import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.InputStream;
@@ -57,7 +56,7 @@ public class RollbackPluginInstall implements AutoCloseable {
     @Nonnull private List<IdPModule> modulesDisabled = new ArrayList<>();
 
     /** The files copied in as the {@link IdPPlugin} was installed. */
-    @Nonnull private List<String> filesCopied = new ArrayList<>();
+    @Nonnull private List<Path> filesCopied = new ArrayList<>();
    
     /** The files renamed away during the installation. */
     @Nonnull private List<Pair<Path, Path>> filesRenamedAway = new ArrayList<>();
@@ -90,7 +89,7 @@ public class RollbackPluginInstall implements AutoCloseable {
     /** What was copied?
      * @return Returns the files copied as part of the install
      */
-    @Live @Nonnull public List<String> getFilesCopied() {
+    @Live @Nonnull public List<Path> getFilesCopied() {
         return filesCopied;
     }
 
@@ -147,13 +146,13 @@ public class RollbackPluginInstall implements AutoCloseable {
             return false;
         }
         for (int i = filesCopied.size()-1; i >=0; i--) {
-            final String file = filesCopied.get(i);
+            final Path path = filesCopied.get(i);
             try {
-                log.trace("Deleting {}", file);
-                Files.delete(Path.of(file));
+                log.trace("Deleting {}", path);
+                Files.delete(path);
             } catch (final Throwable t) {
-                log.error("Could not delete {}, continuing ", file, t);
-                new File(file).deleteOnExit();
+                log.error("Could not delete {}, continuing ", path, t);
+                path.toFile().deleteOnExit();
             }
         }
         return true;
diff --git a/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/RollbackTester.java b/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/RollbackTester.java
index 7bc6bbc82..10b0d93ad 100644
--- a/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/RollbackTester.java
+++ b/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/RollbackTester.java
@@ -80,7 +80,7 @@ public class RollbackTester {
             assertFalse(disabled2.isEnabled(null));
             
             try (final RollbackPluginInstall rp = new RollbackPluginInstall(new ModuleContext(parent))) {
-                rp.getFilesCopied().add(copied.toString());
+                rp.getFilesCopied().add(copied);
                 rp.getFilesRenamedAway().add(renamed);
                 rp.getModulesDisabled().add(disabled1);
                 rp.getModulesDisabled().add(disabled2);
diff --git a/idp-installer/src/test/resources/credentials/truststore.asc b/idp-installer/src/test/resources/credentials/truststore.asc
index 0ccc32281..f65286857 100644
--- a/idp-installer/src/test/resources/credentials/truststore.asc
+++ b/idp-installer/src/test/resources/credentials/truststore.asc
@@ -187,3 +187,140 @@ p4tHovBdwCerszJryXL9jPma9jdfbGmttL+36358si1RNrQG6IhaJpa5a89/BJQH
 3aNjQSVwGC4Owz97QpUq9StZb1qhvIw=
 =1e4x
 -----END PGP PUBLIC KEY BLOCK-----
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.11 (Darwin)
+
+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
+=ShRP
+-----END PGP PUBLIC KEY BLOCK-----
+
+pub  1024D/47905D15 2004-06-10 Scott Cantor (Internet2) <cantor.2 at osu.edu>
+     Key fingerprint = 272F 5F06 84CE A946 E796  7774 A57B 56F3 4790 5D15
+     sig 3       47905D15 2004-06-10   Scott Cantor (Internet2) <cantor.2 at osu.edu>
+     sig 3       47905D15 2004-06-10   Scott Cantor (Internet2) <cantor.2 at osu.edu>
+     sig 3       EA2882BB 2006-12-08   Ian A. Young <ian at iay.org.uk>
+     sig         A260F52E 2007-04-25   Chad La Joie (Georgetown University / Internet 2) <lajoie at georgetown.edu>
+     sig          146B2514 2008-05-17  Chad La Joie <chad.lajoie at switch.ch>
+     sub  1024g/14E203EB 2004-06-10
+     sig         47905D15 2004-06-10   Scott Cantor (Internet2) <cantor.2 at osu.edu>
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.5 (GNU/Linux)
+
+mQGiBEDIr5QRBACv65rtbmSMA2zb4g+5LgcUMznK0fkU/+zL0ApdIKjJWfZ/E0GC
+tXKfn4oKIXNCCHuwbqHx9+xUmdYHJ1KZMmaQY+sm1VzrIexixwTs4M4p/w/mK8ry
+un1WYfBnAleXz0geqE9/EYvum270R1e+Ms36flQGVRdaS0cUzUHCHG3zkwCg8/9B
+SjFPZNsJrv2C0FtEm4nt4PMD/AnE7N5pwUCaGwL/zJbfWOOK7O+jA1LZ9Q/jM6kq
+oPC+hXuQn6duflr31rICmazMBxIGQUIA6GeHM9FnlhJQ3hBNuEMwLqJmYJpBLA1i
+5LUe9NpnvXPsDc+lTHRh9uecHYCRYhuO1bIJ9drRpLALaiuAlGeLDJcLKPLekj2x
+3OCVBACurQQIBDSz3r9gXiU3oWVIsHNmeEM7pOD2fv8rfnDRyCeljef3kCUrUQjW
+Zuu1bBLf5r/N98JdmcrcxS6r3kIGcn68ok+ZHx7poIlxUchXPtWvFLFpEA8KZCOg
+Iz24CnqvbYUYckQHGP8cs4yMBAgZT6jZQQLJVeH3iKgDBAnRRLQrU2NvdHQgQ2Fu
+dG9yIChJbnRlcm5ldDIpIDxjYW50b3IuMkBvc3UuZWR1PohXBBMRAgAXBQJAyK+U
+BQsHCgMEAxUDAgMWAgECF4AACgkQpXtW80eQXRWLfgCZAXU659pjCO9DQ/6CYL19
+lqEqShYAoMlk3kjCmfai14uV5kE40RwQepXZiFcEExECABcFAkDIr5QFCwcKAwQD
+FQMCAxYCAQIXgAAKCRCle1bzR5BdFYt+AKCXvVk4Lmz9JaRBkAI5EszEIJE4mgCg
+yyOCRkBi7uyn7Un5GJWglYMII0+IRgQTEQIABgUCRXn6YQAKCRDvQPwp6iiCuzQx
+AKCfIAINZmu9xCNwkk/CEEzd+xmYwQCfQcjlajsk6muOASv/SLcUZrrEt72IRgQQ
+EQIABgUCRi/L2AAKCRA/Xp6HomD1LoeLAKCNxma/y2VQdGJ6A1IfTMoU0XykIACg
++cbPJ3wf4ZLTL+hByHHiPtySCpCIRgQQEQIABgUCSC4lHQAKCRAj3qZaFGslFHrH
+AJ9U+XqePUTjjs0CXUJhYenyP2tVHwCfUa53/zRRZxQbQvXSKFojiQ6ZUs25AQ0E
+QMivlRAEAJWXpGwp79Yefu3qoZM86JYxBc9IuEzB5sCEr9H3FSMkmqp40ayTBxcN
+8lUa4ZVGuycnlPmBcWRKOvXUgJsjkSmkBHLvNvQOReVCtThpd7/Rzk+TCo4fBujL
+12DpBwG/yl3dBkotD/6RXiVtDXFejm2ok8FOWk+sylevCTeNYXGvAAQLA/9HZwfL
+HB6sfcAeRc3FjzQ0S9Fzi63i92tePL3IQvNmF+VXQgx0q4fXeKBZCzlYBt9x5kjp
+ksSWPKKUao0OggiC0wT72JHSrK2BcWDWEmfrsjPep9eEqayJm06ae//MaKnZ/CCb
+E1loar6uE1KD6uqt2MARjEby/egO2akyKRlLYohGBBgRAgAGBQJAyK+VAAoJEKV7
+VvNHkF0V7YMAoPO+egNv4FH2swYCqzPX/vD4BEKNAJ9fuG71ljRK4lCGrZu+Ei5W
+AhqwAA==
+=Hsu3
+-----END PGP PUBLIC KEY BLOCK-----
+

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list