Support message bus listeners for dynamic plugins

GitOrigin-RevId: f2f7a011eb15782f81144f324defde85e2ce2d59
This commit is contained in:
Dmitry Jemerov
2019-11-24 23:02:07 +00:00
committed by intellij-monorepo-bot
parent c45fbce546
commit 7c6aaa7e88
7 changed files with 102 additions and 31 deletions
@@ -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
<L> void subscribe(@NotNull Topic<L> topic, @NotNull List<Object> handlers) {
<L> void subscribe(@NotNull Topic<L> topic, @NotNull Collection<Object> handlers) {
boolean notifyBusAboutTopic = false;
synchronized (myPendingMessages) {
Object currentHandler = mySubscriptions.get(topic);
@@ -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<PluginDescriptor, MessageBusConnectionImpl> 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<String, List<ListenerDescriptor>> map) {
if (myTopicClassToListenerClass != Collections.<String, List<ListenerDescriptor>>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<ListenerDescriptor> listenerDescriptors = myTopicClassToListenerClass.remove(listenerClass.getName());
if (listenerDescriptors != null) {
List<Object> listeners = new ArrayList<>(listenerDescriptors.size());
MultiMap<PluginDescriptor, Object> 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<PluginDescriptor, Collection<Object>> 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 <L> InvocationHandler createTopicHandler(@NotNull Topic<L> topic) {
return (proxy, method, args) -> {
@@ -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())
}
@@ -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<UISettings>()
}
@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("""
<idea-plugin>
<name>testLoadListeners</name>
<applicationListeners>
<listener class="${MyUISettingsListener::class.java.name}" topic="com.intellij.ide.ui.UISettingsListener"/>
</applicationListeners>
</idea-plugin>""")
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)
}
}
}
@@ -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 {
<vendor>JetBrains</vendor>
<product-descriptor code="IJ" release-date="20190811" release-version="42"/>
</idea-plugin>""")
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("<idea-plugin>\n <id>bar</id>\n <project-components>\n <component>\n <implementation-class>com.intellij.ide.favoritesTreeView.FavoritesManager</implementation-class>\n <option name=\"workspace\" value=\"true\"/>\n </component>\n\n \n </project-components>\n</idea-plugin>")
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>ID</id>
<name>A</name>
</idea-plugin>""")
val impl1 = loadDescriptor(fs.getPath("/"))
val impl1 = loadDescriptorInTest(fs.getPath("/"))
tempFile.write("""
<idea-plugin>
<id>ID</id>
<name>B</name>
</idea-plugin>""")
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())
@@ -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) {
@@ -12,7 +12,7 @@
<orderEntry type="module" module-name="intellij.platform.testFramework" />
<orderEntry type="library" name="JDOM" level="project" />
<orderEntry type="library" exported="" name="assertJ" level="project" />
<orderEntry type="library" name="memoryfilesystem" level="project" />
<orderEntry type="library" exported="" name="memoryfilesystem" level="project" />
<orderEntry type="library" name="snakeyaml" level="project" />
</component>
</module>