From b92ec9ed53f69638c9726c29bc25a780a8eb7a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20L=C3=B6vdahl?= Date: Tue, 20 Feb 2024 09:16:09 +0200 Subject: [PATCH] Post `PluginState.UNLOADED` event when plugins are unloaded (#567) --- .../java/org/pf4j/AbstractPluginManager.java | 1 + pf4j/src/main/java/org/pf4j/PluginState.java | 11 ++++- .../org/pf4j/DefaultPluginManagerTest.java | 43 +++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/pf4j/src/main/java/org/pf4j/AbstractPluginManager.java b/pf4j/src/main/java/org/pf4j/AbstractPluginManager.java index f3a7231..78c261c 100644 --- a/pf4j/src/main/java/org/pf4j/AbstractPluginManager.java +++ b/pf4j/src/main/java/org/pf4j/AbstractPluginManager.java @@ -311,6 +311,7 @@ public abstract class AbstractPluginManager implements PluginManager { } // remove the plugin + pluginWrapper.setPluginState(PluginState.UNLOADED); plugins.remove(pluginId); getResolvedPlugins().remove(pluginWrapper); getUnresolvedPlugins().remove(pluginWrapper); diff --git a/pf4j/src/main/java/org/pf4j/PluginState.java b/pf4j/src/main/java/org/pf4j/PluginState.java index 989157f..6a6c1f9 100644 --- a/pf4j/src/main/java/org/pf4j/PluginState.java +++ b/pf4j/src/main/java/org/pf4j/PluginState.java @@ -20,7 +20,7 @@ package org.pf4j; *

* Lifecycle of a plugin: *

- * CREATED -> RESOLVED -> STARTED -> STOPPED
+ * CREATED -> RESOLVED -> STARTED -> STOPPED -> UNLOADED
  * CREATED -> DISABLED
  * CREATED -> FAILED
  *
@@ -57,7 +57,14 @@ public enum PluginState {
     /**
      * Plugin failed to start.
      */
-    FAILED("FAILED");
+    FAILED("FAILED"),
+
+    /**
+     * The plugin has been unloaded. After this event has been completed, the plugin's
+     * {@link ClassLoader} will be closed.
+     */
+    UNLOADED("UNLOADED"),
+    ;
 
     private final String status;
 
diff --git a/pf4j/src/test/java/org/pf4j/DefaultPluginManagerTest.java b/pf4j/src/test/java/org/pf4j/DefaultPluginManagerTest.java
index 2e78ad2..d95a2d0 100644
--- a/pf4j/src/test/java/org/pf4j/DefaultPluginManagerTest.java
+++ b/pf4j/src/test/java/org/pf4j/DefaultPluginManagerTest.java
@@ -25,6 +25,9 @@ import org.pf4j.test.PluginZip;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -40,12 +43,17 @@ public class DefaultPluginManagerTest {
     private DefaultPluginDescriptor pluginDescriptor;
     private PluginWrapper pluginWrapper;
 
+    private List receivedEvents;
+
     @TempDir
     Path pluginsPath;
 
     @BeforeEach
     public void setUp() throws IOException {
+        receivedEvents = new ArrayList<>();
+
         pluginManager = new DefaultPluginManager(pluginsPath);
+        pluginManager.addPluginStateListener(event -> receivedEvents.add(event));
 
         pluginDescriptor = new DefaultPluginDescriptor();
         pluginDescriptor.setPluginId("myPlugin");
@@ -63,6 +71,7 @@ public class DefaultPluginManagerTest {
         pluginManager = null;
         pluginDescriptor = null;
         pluginWrapper = null;
+        receivedEvents = null;
     }
 
     @Test
@@ -140,6 +149,7 @@ public class DefaultPluginManagerTest {
 
         PluginManager pluginManager = new DefaultPluginManager(pluginsPath) {
 
+            @Override
             protected PluginStatusProvider createPluginStatusProvider() {
                 return statusProvider;
             }
@@ -171,6 +181,12 @@ public class DefaultPluginManagerTest {
         assertTrue(deleted);
 
         assertFalse(pluginZip.file().exists());
+
+        Optional unloadedEvent = receivedEvents.stream()
+            .filter(event -> event.getPluginState() == PluginState.UNLOADED)
+            .findFirst();
+
+        assertTrue(unloadedEvent.isPresent());
     }
 
     @Test
@@ -188,6 +204,12 @@ public class DefaultPluginManagerTest {
         assertTrue(deleted);
 
         assertFalse(pluginJar.file().exists());
+
+        Optional unloadedEvent = receivedEvents.stream()
+            .filter(event -> event.getPluginState() == PluginState.UNLOADED)
+            .findFirst();
+
+        assertTrue(unloadedEvent.isPresent());
     }
 
     @Test
@@ -310,4 +332,25 @@ public class DefaultPluginManagerTest {
         assertThrows(DependencyResolver.CyclicDependencyException.class, () -> pluginManager.loadPlugins());
     }
 
+    @Test
+    public void deleteZipPluginForPluginThatHasNotBeenStartedPostsUnloadedEvent() throws Exception {
+        PluginZip pluginZip = new PluginZip.Builder(pluginsPath.resolve("my-plugin-1.2.3.zip"), "myPlugin")
+            .pluginVersion("1.2.3")
+            .build();
+
+        pluginManager.loadPlugin(pluginZip.path());
+
+        assertEquals(1, pluginManager.getPlugins().size());
+
+        boolean deleted = pluginManager.deletePlugin(pluginZip.pluginId());
+        assertTrue(deleted);
+
+        assertFalse(pluginZip.file().exists());
+
+        Optional unloadedEvent = receivedEvents.stream()
+            .filter(event -> event.getPluginState() == PluginState.UNLOADED)
+            .findFirst();
+
+        assertTrue(unloadedEvent.isPresent());
+    }
 }