Option to auto-reload changed plugins without IDE restart (for plugin developers)

GitOrigin-RevId: f15ccc3dc194fb76d966706abeef3fc0202a7e6d
This commit is contained in:
Dmitry Jemerov
2020-01-30 18:31:30 +00:00
committed by intellij-monorepo-bot
parent b37bf2f554
commit ec8f71f90e
3 changed files with 94 additions and 2 deletions
@@ -0,0 +1,85 @@
// Copyright 2000-2020 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.intellij.ide.FrameStateListener
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.extensions.PluginDescriptor
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.AsyncFileListener
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
/**
* @author yole
*/
private const val AUTO_RELOAD_PLUGINS_SYSTEM_PROPERTY = "idea.auto.reload.plugins"
class DynamicPluginVfsListener : AsyncFileListener {
init {
if (System.getProperty(AUTO_RELOAD_PLUGINS_SYSTEM_PROPERTY) != null) {
val pluginsPath = PathManager.getPluginsPath()
LocalFileSystem.getInstance().addRootToWatch(pluginsPath, true)
val pluginsRoot = LocalFileSystem.getInstance().findFileByPath(pluginsPath)
if (pluginsRoot != null) {
// ensure all plugins are in VFS
VfsUtilCore.processFilesRecursively(pluginsRoot) { true }
}
}
}
override fun prepareChange(events: MutableList<out VFileEvent>): AsyncFileListener.ChangeApplier? {
if (System.getProperty(AUTO_RELOAD_PLUGINS_SYSTEM_PROPERTY) == null) return null
val pluginsToReload = hashSetOf<PluginDescriptor>()
for (event in events) {
if (!event.isFromRefresh) continue
if (event is VFileContentChangeEvent) {
findPluginByPath(event.path)?.let { pluginsToReload.add(it) }
}
}
val descriptorsToReload = pluginsToReload
.filter { it.isEnabled }
.map { PluginEnabler.loadFullDescriptor(it) }
.filter { DynamicPlugins.allowLoadUnloadWithoutRestart(it) }
if (descriptorsToReload.isEmpty()) return null
return object : AsyncFileListener.ChangeApplier {
override fun afterVfsChange() {
ApplicationManager.getApplication().invokeLater {
val reloaded = mutableListOf<String>()
for (pluginDescriptor in descriptorsToReload) {
if (!DynamicPlugins.unloadPlugin(pluginDescriptor, isUpdate = true)) {
continue
}
reloaded.add(pluginDescriptor.name)
DynamicPlugins.loadPlugin(pluginDescriptor, false)
}
if (reloaded.isNotEmpty()) {
DynamicPlugins.notify("${reloaded.joinToString()} reloaded successfully", NotificationType.INFORMATION)
}
}
}
}
}
private fun findPluginByPath(path: String): PluginDescriptor? {
if (!FileUtil.isAncestor(PathManager.getPluginsPath(), path, false)) return null
return PluginManager.getPlugins().firstOrNull {
FileUtil.isAncestor(it.path.absolutePath, path, false)
}
}
}
class DynamicPluginsFrameStateListener : FrameStateListener {
override fun onFrameActivated() {
if (System.getProperty(AUTO_RELOAD_PLUGINS_SYSTEM_PROPERTY) == null) return
val pluginsRoot = LocalFileSystem.getInstance().findFileByPath(PathManager.getPluginsPath())
pluginsRoot?.refresh(true, true)
}
}
@@ -357,8 +357,7 @@ object DynamicPlugins {
val snapshotDate = SimpleDateFormat("dd.MM.yyyy_HH.mm.ss").format(Date())
val snapshotPath = "$snapshotFolder/unload-${pluginDescriptor.pluginId}-$snapshotDate.hprof"
MemoryDumpHelper.captureMemoryDump(snapshotPath)
GROUP.createNotification("Captured memory snapshot on plugin unload fail: $snapshotPath",
NotificationType.WARNING).notify(null)
notify("Captured memory snapshot on plugin unload fail: $snapshotPath", NotificationType.WARNING)
}
LOG.info("Plugin ${pluginDescriptor.pluginId} is not unload-safe because class loader cannot be unloaded")
}
@@ -367,6 +366,10 @@ object DynamicPlugins {
}
}
internal fun notify(text: String, notificationType: NotificationType) {
GROUP.createNotification(text, notificationType).notify(null)
}
private fun unloadPluginDescriptor(pluginDescriptor: IdeaPluginDescriptorImpl,
loadedPluginDescriptor: IdeaPluginDescriptorImpl) {
val application = ApplicationManager.getApplication() as ApplicationImpl
@@ -901,5 +901,9 @@
<titleInfoProvider implementation="com.intellij.openapi.wm.impl.simpleTitleParts.ProductVersionTitleInfoProvider"/>
<titleInfoProvider implementation="com.intellij.openapi.wm.impl.simpleTitleParts.SuperUserSuffixTitleInfoProvider"/>
<vfs.asyncListener implementation="com.intellij.ide.plugins.DynamicPluginVfsListener"/>
</extensions>
<applicationListeners>
<listener class="com.intellij.ide.plugins.DynamicPluginsFrameStateListener" topic="com.intellij.ide.FrameStateListener"/>
</applicationListeners>
</idea-plugin>