From 7c6aaa7e88dfbe1aec601961989e2993023cafce Mon Sep 17 00:00:00 2001 From: Dmitry Jemerov Date: Sun, 24 Nov 2019 22:55:10 +0100 Subject: [PATCH] Support message bus listeners for dynamic plugins GitOrigin-RevId: f2f7a011eb15782f81144f324defde85e2ce2d59 --- .../impl/MessageBusConnectionImpl.java | 3 +- .../util/messages/impl/MessageBusImpl.java | 34 +++++++++---- .../intellij/ide/plugins/DynamicPlugins.kt | 12 +++-- .../ide/plugins/DynamicPluginsTest.kt | 49 +++++++++++++++++++ .../ide/plugins/PluginDescriptorTest.kt | 32 ++++++------ .../PlatformComponentManagerImpl.kt | 1 - .../intellij.platform.testExtensions.iml | 2 +- 7 files changed, 102 insertions(+), 31 deletions(-) create mode 100644 platform/platform-tests/testSrc/com/intellij/ide/plugins/DynamicPluginsTest.kt diff --git a/platform/core-api/src/com/intellij/util/messages/impl/MessageBusConnectionImpl.java b/platform/core-api/src/com/intellij/util/messages/impl/MessageBusConnectionImpl.java index e6954e5d545a..be429c884de7 100644 --- a/platform/core-api/src/com/intellij/util/messages/impl/MessageBusConnectionImpl.java +++ b/platform/core-api/src/com/intellij/util/messages/impl/MessageBusConnectionImpl.java @@ -12,6 +12,7 @@ import org.jetbrains.annotations.NotNull; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Queue; @@ -56,7 +57,7 @@ final class MessageBusConnectionImpl implements MessageBusConnection { } // avoid notifyOnSubscription and map modification for each handler - void subscribe(@NotNull Topic topic, @NotNull List handlers) { + void subscribe(@NotNull Topic topic, @NotNull Collection handlers) { boolean notifyBusAboutTopic = false; synchronized (myPendingMessages) { Object currentHandler = mySubscriptions.get(topic); diff --git a/platform/core-api/src/com/intellij/util/messages/impl/MessageBusImpl.java b/platform/core-api/src/com/intellij/util/messages/impl/MessageBusImpl.java index 4ac455b18da7..06bd50c51514 100644 --- a/platform/core-api/src/com/intellij/util/messages/impl/MessageBusImpl.java +++ b/platform/core-api/src/com/intellij/util/messages/impl/MessageBusImpl.java @@ -4,12 +4,15 @@ package com.intellij.util.messages.impl; import com.intellij.openapi.Disposable; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.extensions.ExtensionNotApplicableException; +import com.intellij.openapi.extensions.PluginDescriptor; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.util.Disposer; import com.intellij.util.ArrayUtil; import com.intellij.util.ConcurrencyUtil; import com.intellij.util.EventDispatcher; import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.FactoryMap; +import com.intellij.util.containers.MultiMap; import com.intellij.util.lang.CompoundRuntimeException; import com.intellij.util.messages.ListenerDescriptor; import com.intellij.util.messages.MessageBus; @@ -65,7 +68,7 @@ public class MessageBusImpl implements MessageBus { private final Disposable myConnectionDisposable; private MessageDeliveryListener myMessageDeliveryListener; - private final MessageBusConnectionImpl myLazyConnection; + private final Map myLazyConnections; public MessageBusImpl(@NotNull MessageBusOwner owner, @NotNull MessageBusImpl parentBus) { myOwner = owner; @@ -79,7 +82,7 @@ public class MessageBusImpl implements MessageBus { LOG.assertTrue(parentBus.myChildBuses.contains(this)); myRootBus.clearSubscriberCache(); // only for project - myLazyConnection = parentBus.myParentBus == null ? connect() : null; + myLazyConnections = parentBus.myParentBus == null ? FactoryMap.create((key) -> connect()) : null; } // root message bus constructor @@ -88,7 +91,7 @@ public class MessageBusImpl implements MessageBus { myConnectionDisposable = Disposer.newDisposable(myOwner.toString()); myOrder = ArrayUtil.EMPTY_INT_ARRAY; myRootBus = (RootBus)this; - myLazyConnection = connect(); + myLazyConnections = FactoryMap.create((key) -> connect()); } /** @@ -97,9 +100,12 @@ public class MessageBusImpl implements MessageBus { @ApiStatus.Internal public void setLazyListeners(@NotNull ConcurrentMap> map) { if (myTopicClassToListenerClass != Collections.>emptyMap()) { - throw new IllegalStateException("Already set: "+myTopicClassToListenerClass); + myTopicClassToListenerClass.putAll(map); + myPublishers.clear(); + } + else { + myTopicClassToListenerClass = map; } - myTopicClassToListenerClass = map; } @Override @@ -204,10 +210,10 @@ public class MessageBusImpl implements MessageBus { List listenerDescriptors = myTopicClassToListenerClass.remove(listenerClass.getName()); if (listenerDescriptors != null) { - List listeners = new ArrayList<>(listenerDescriptors.size()); + MultiMap listenerMap = new MultiMap<>(); for (ListenerDescriptor listenerDescriptor : listenerDescriptors) { try { - listeners.add(myOwner.createListener(listenerDescriptor)); + listenerMap.putValue(listenerDescriptor.pluginDescriptor, myOwner.createListener(listenerDescriptor)); } catch (ExtensionNotApplicableException ignore) { } @@ -219,8 +225,10 @@ public class MessageBusImpl implements MessageBus { } } - if (!listeners.isEmpty()) { - myLazyConnection.subscribe(topic, listeners); + if (!listenerMap.isEmpty()) { + for (Map.Entry> entry : listenerMap.entrySet()) { + myLazyConnections.get(entry.getKey()).subscribe(topic, entry.getValue()); + } } } @@ -230,6 +238,14 @@ public class MessageBusImpl implements MessageBus { return publisher; } + @ApiStatus.Internal + public void unsubscribePluginListeners(PluginDescriptor pluginDescriptor) { + MessageBusConnectionImpl connection = myLazyConnections.remove(pluginDescriptor); + if (connection != null) { + Disposer.dispose(connection); + } + } + @NotNull private InvocationHandler createTopicHandler(@NotNull Topic topic) { return (proxy, method, args) -> { diff --git a/platform/platform-impl/src/com/intellij/ide/plugins/DynamicPlugins.kt b/platform/platform-impl/src/com/intellij/ide/plugins/DynamicPlugins.kt index 76d551932ca8..44a7681a838e 100644 --- a/platform/platform-impl/src/com/intellij/ide/plugins/DynamicPlugins.kt +++ b/platform/platform-impl/src/com/intellij/ide/plugins/DynamicPlugins.kt @@ -28,6 +28,7 @@ import com.intellij.util.ArrayUtil import com.intellij.util.MemoryDumpHelper import com.intellij.util.SystemProperties import com.intellij.util.messages.Topic +import com.intellij.util.messages.impl.MessageBusImpl import com.intellij.util.ui.UIUtil import com.intellij.util.xmlb.BeanBinding import java.text.SimpleDateFormat @@ -174,6 +175,7 @@ object DynamicPlugins { for (appServiceInstance in appServiceInstances) { application.stateStore.unloadComponent(appServiceInstance) } + (application.messageBus as MessageBusImpl).unsubscribePluginListeners(loadedPluginDescriptor) for (project in openProjects) { val projectServiceInstances = (project as ProjectImpl).unloadServices(pluginDescriptor.project) @@ -187,6 +189,8 @@ object DynamicPlugins { module.stateStore.unloadComponent(moduleServiceInstance) } } + + (project.messageBus as MessageBusImpl).unsubscribePluginListeners(loadedPluginDescriptor) } } @@ -217,8 +221,10 @@ object DynamicPlugins { } @JvmStatic - fun loadPlugin(pluginDescriptor: IdeaPluginDescriptorImpl, enable: Boolean) { - PluginManagerCore.initClassLoader(pluginDescriptor) + fun loadPlugin(pluginDescriptor: IdeaPluginDescriptorImpl, wasDisabled: Boolean) { + if (!ApplicationManager.getApplication().isUnitTestMode) { + PluginManagerCore.initClassLoader(pluginDescriptor) + } val application = ApplicationManager.getApplication() as ApplicationImpl application.runWriteAction { @@ -232,7 +238,7 @@ object DynamicPlugins { (ActionManager.getInstance() as ActionManagerImpl).registerPluginActions(pluginDescriptor) } - if (enable) { + if (wasDisabled) { // Update list of disabled plugins PluginManagerCore.setPlugins(PluginManagerCore.getPlugins()) } diff --git a/platform/platform-tests/testSrc/com/intellij/ide/plugins/DynamicPluginsTest.kt b/platform/platform-tests/testSrc/com/intellij/ide/plugins/DynamicPluginsTest.kt new file mode 100644 index 000000000000..b39ce1b67a50 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/ide/plugins/DynamicPluginsTest.kt @@ -0,0 +1,49 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.ide.plugins + +import com.github.marschall.memoryfilesystem.MemoryFileSystemBuilder +import com.intellij.ide.ui.UISettings +import com.intellij.ide.ui.UISettingsListener +import com.intellij.openapi.application.ApplicationManager +import com.intellij.testFramework.LightPlatformTestCase +import com.intellij.util.io.write +import org.junit.Test + +class DynamicPluginsTest : LightPlatformTestCase() { + companion object { + val receivedNotifications = mutableListOf() + } + + @Test + fun testLoadListeners() { + receivedNotifications.clear() + + MemoryFileSystemBuilder.newEmpty().build(DynamicPluginsTest::class.java.simpleName).use { fs -> + ApplicationManager.getApplication().messageBus.syncPublisher(UISettingsListener.TOPIC).uiSettingsChanged(UISettings()) + + val pluginFile = fs.getPath("/plugin/META-INF/plugin.xml") + pluginFile.write(""" + + testLoadListeners + + + + """) + val descriptor = loadDescriptorInTest(pluginFile.parent.parent) + descriptor.setLoader(DynamicPlugins::class.java.classLoader) + DynamicPlugins.loadPlugin(descriptor, false) + ApplicationManager.getApplication().messageBus.syncPublisher(UISettingsListener.TOPIC).uiSettingsChanged(UISettings()) + assertEquals(1, receivedNotifications.size) + + DynamicPlugins.unloadPlugin(descriptor, false) + ApplicationManager.getApplication().messageBus.syncPublisher(UISettingsListener.TOPIC).uiSettingsChanged(UISettings()) + assertEquals(1, receivedNotifications.size) + } + } + + private class MyUISettingsListener : UISettingsListener { + override fun uiSettingsChanged(uiSettings: UISettings) { + receivedNotifications.add(uiSettings) + } + } +} \ No newline at end of file diff --git a/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginDescriptorTest.kt b/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginDescriptorTest.kt index 76dd01e99cef..0bcc25ec9a2f 100644 --- a/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginDescriptorTest.kt +++ b/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginDescriptorTest.kt @@ -36,7 +36,7 @@ class PluginDescriptorTest { @Test fun testDescriptorLoading() { - val descriptor = loadDescriptor("asp.jar") + val descriptor = loadDescriptorInTest("asp.jar") assertThat(descriptor).isNotNull() assertThat(descriptor.pluginId.idString).isEqualTo("com.jetbrains.plugins.asp") assertThat(descriptor.name).isEqualTo("ASP") @@ -44,14 +44,14 @@ class PluginDescriptorTest { @Test fun testOptionalDescriptors() { - val descriptor = loadDescriptor("family") + val descriptor = loadDescriptorInTest("family") assertThat(descriptor).isNotNull() assertThat(descriptor.optionalConfigs.size).isEqualTo(1) } @Test fun testMultipleOptionalDescriptors() { - val descriptor = loadDescriptor("multipleOptionalDescriptors") + val descriptor = loadDescriptorInTest("multipleOptionalDescriptors") assertThat(descriptor).isNotNull() val ids = descriptor.optionalConfigs.keys assertThat(ids).hasSize(2) @@ -61,20 +61,20 @@ class PluginDescriptorTest { @Test fun testMalformedDescriptor() { @Suppress("GrazieInspection") - assertThatThrownBy { loadDescriptor("malformed") } + assertThatThrownBy { loadDescriptorInTest("malformed") } .hasMessageContaining("Content is not allowed in prolog.") } @Test fun testAnonymousDescriptor() { - val descriptor = loadDescriptor("anonymous") + val descriptor = loadDescriptorInTest("anonymous") assertThat(descriptor.pluginId).isNull() assertThat(descriptor.name).isNull() } @Test fun testCyclicOptionalDeps() { - assertThatThrownBy { loadDescriptor("cyclicOptionalDeps") } + assertThatThrownBy { loadDescriptorInTest("cyclicOptionalDeps") } .hasMessage("Plugin someId optional descriptors form a cycle: a.xml, b.xml") } @@ -119,7 +119,7 @@ class PluginDescriptorTest { @Test fun testDuplicateDependency() { - val descriptor = loadDescriptor("duplicateDependency") + val descriptor = loadDescriptorInTest("duplicateDependency") assertThat(descriptor).isNotNull() assertThat( descriptor.optionalDependentPluginIds).isEmpty() @@ -129,7 +129,7 @@ class PluginDescriptorTest { @Test fun testPluginNameAsId() { - val descriptor = loadDescriptor("noId") + val descriptor = loadDescriptorInTest("noId") assertThat(descriptor).isNotNull() assertThat(descriptor.pluginId.idString).isEqualTo(descriptor.name) } @@ -143,7 +143,7 @@ class PluginDescriptorTest { JetBrains """) - val descriptor = loadDescriptor(pluginFile.parent.parent) + val descriptor = loadDescriptorInTest(pluginFile.parent.parent) assertThat(descriptor).isNotNull() assertThat(descriptor.vendor).isEqualTo("JetBrains") assertThat(SimpleDateFormat("yyyyMMdd", Locale.US).format(descriptor.releaseDate)).isEqualTo("20190811") @@ -302,14 +302,14 @@ class PluginDescriptorTest { fun componentConfig() { val pluginFile = inMemoryFs.fs.getPath("/plugin/META-INF/plugin.xml") pluginFile.write("\n bar\n \n \n com.intellij.ide.favoritesTreeView.FavoritesManager\n \n\n \n \n") - val descriptor = loadDescriptor(pluginFile.parent.parent) + val descriptor = loadDescriptorInTest(pluginFile.parent.parent) assertThat(descriptor).isNotNull assertThat(descriptor.projectContainerDescriptor.components!![0].options).isEqualTo(Collections.singletonMap("workspace", "true")) } @Test fun testPluginIdAsName() { - val descriptor = loadDescriptor("noName") + val descriptor = loadDescriptorInTest("noName") assertThat(descriptor).isNotNull() assertThat(descriptor.name).isEqualTo(descriptor.pluginId.idString) } @@ -357,13 +357,13 @@ class PluginDescriptorTest { ID A """) - val impl1 = loadDescriptor(fs.getPath("/")) + val impl1 = loadDescriptorInTest(fs.getPath("/")) tempFile.write(""" ID B """) - val impl2 = loadDescriptor(fs.getPath("/")) + val impl2 = loadDescriptorInTest(fs.getPath("/")) TestCase.assertEquals(impl1, impl2) TestCase.assertEquals(impl1.hashCode(), impl2.hashCode()) TestCase.assertNotSame(impl1.name, impl2.name) @@ -377,11 +377,11 @@ private fun writeDescriptor(id: String, pluginDir: Path, @Language("xml") data: private val testDataPath: String get() = PlatformTestUtil.getPlatformTestDataPath() + "plugins/pluginDescriptor" -private fun loadDescriptor(dirName: String): IdeaPluginDescriptorImpl { - return loadDescriptor(Paths.get(testDataPath, dirName)) +private fun loadDescriptorInTest(dirName: String): IdeaPluginDescriptorImpl { + return loadDescriptorInTest(Paths.get(testDataPath, dirName)) } -private fun loadDescriptor(dir: Path): IdeaPluginDescriptorImpl { +internal fun loadDescriptorInTest(dir: Path): IdeaPluginDescriptorImpl { assertThat(dir).exists() PluginManagerCore.ourPluginError = null val parentContext = DescriptorListLoadingContext.createSingleDescriptorContext(emptySet()) diff --git a/platform/service-container/src/com/intellij/serviceContainer/PlatformComponentManagerImpl.kt b/platform/service-container/src/com/intellij/serviceContainer/PlatformComponentManagerImpl.kt index 95af7a4c8673..bdc2ba7f2e21 100644 --- a/platform/service-container/src/com/intellij/serviceContainer/PlatformComponentManagerImpl.kt +++ b/platform/service-container/src/com/intellij/serviceContainer/PlatformComponentManagerImpl.kt @@ -155,7 +155,6 @@ abstract class PlatformComponentManagerImpl @JvmOverloads constructor(internal v StartUpMeasurer.setCurrentState(LoadingState.COMPONENTS_REGISTERED) } - // todo support lazy listeners for dynamically loaded plugins // ensure that messageBus is created, regardless of lazy listeners map state val messageBus = messageBus as MessageBusImpl if (map != null) { diff --git a/platform/testFramework/extensions/intellij.platform.testExtensions.iml b/platform/testFramework/extensions/intellij.platform.testExtensions.iml index 3b0b46763fda..10a9444da074 100644 --- a/platform/testFramework/extensions/intellij.platform.testExtensions.iml +++ b/platform/testFramework/extensions/intellij.platform.testExtensions.iml @@ -12,7 +12,7 @@ - + \ No newline at end of file