workbench/mps-workbench/source/jetbrains/mps/plugins/PluginLoaderRegistry.java (647 lines of code) (raw):

/* * Copyright 2003-2024 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.mps.plugins; import com.intellij.concurrency.ThreadContext; import com.intellij.configurationStore.JdomSerializer; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx; import com.intellij.openapi.progress.EmptyProgressIndicator; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.util.IconLoader; import com.intellij.openapi.wm.ex.WindowManagerEx; import com.intellij.openapi.wm.impl.ProjectFrameHelper; import com.intellij.util.EventDispatcher; import com.intellij.util.concurrency.Semaphore; import com.intellij.util.messages.MessageBusConnection; import com.intellij.util.messages.Topic; import jetbrains.mps.core.platform.Platform; import jetbrains.mps.ide.MPSCoreComponents; import jetbrains.mps.ide.ThreadUtils; import jetbrains.mps.logging.Logger; import jetbrains.mps.plugins.applicationplugins.BaseApplicationPlugin; import jetbrains.mps.plugins.projectplugins.BaseProjectPlugin; import jetbrains.mps.progress.EmptyProgressMonitor; import jetbrains.mps.smodel.language.LanguageRegistry; import jetbrains.mps.smodel.runtime.ModuleDeploymentChange; import jetbrains.mps.smodel.runtime.ModuleDeploymentListener; import jetbrains.mps.smodel.runtime.ModuleRuntime; import jetbrains.mps.util.JavaNameUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.util.ProgressMonitor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Queue; import java.util.ServiceLoader; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; /** * Represents a single class loading listener to trigger the plugin reload in * {@link jetbrains.mps.plugins.applicationplugins.ApplicationPluginManager} * and {@link jetbrains.mps.plugins.projectplugins.ProjectPluginManager}. It does that via the {@link jetbrains.mps.plugins.PluginReloadingListener} interface. * <p> * It listens for class loading events, calculate the plugin contributors which need to be updated and notifies these managers. * <p> * This is an application service. There's a `PluginReloadingListener` topic with reloading events dispatched. */ public class PluginLoaderRegistry implements Disposable { private static final Logger LOG = Logger.getLogger(PluginLoaderRegistry.class); public static final int SEMAPHORE_WAIT_TIMEOUT = 100; // ms private final Semaphore myUpdateSemaphore = new Semaphore(); private final SchedulingUpdateListener myClassesListener = new SchedulingUpdateListener(); private final Set<PluginContributor> myCurrentContributors = new LinkedHashSet<>(); private final Set<PluginLoader> myCurrentLoaders = new LinkedHashSet<>(); @Topic.AppLevel public static final Topic<PluginReloadingListener> TOPIC = Topic.create("MPS Plugin Management", PluginReloadingListener.class); private final EventDispatcher<PluginReloadingListener> myReloadingListeners = EventDispatcher.create(PluginReloadingListener.class); private final AtomicBoolean myUpdateIsScheduledInEDT = new AtomicBoolean(false); private final AtomicBoolean myAppInitialized = new AtomicBoolean(); public static PluginLoaderRegistry getInstance() { return ApplicationManager.getApplication().getService(PluginLoaderRegistry.class); } public PluginLoaderRegistry() { // XXX Now, PluginLoaderRegistry is app service, although indeed doesn't make too much // sense unless we convert ApplicationPluginManager and ProjectPluginManager as well. // Switching to extension points for these managers might be an option // although I don't clearly see how to approach per-project PPM and app-wide // APM in a single extpoint (two would be too much, imo). // Note, MPSApplicationInitializedListener fits service story quite well - it would be the // listener to activate PLR service by issuing an update (see uses of #signalAppInitialized). MPSCoreComponents coreComponents = MPSCoreComponents.getInstance(); Platform mpsPlatform = coreComponents.getPlatform(); mpsPlatform.findComponent(LanguageRegistry.class).addRegistryListener(myClassesListener); final MessageBusConnection mbc = ApplicationManager.getApplication().getMessageBus().connect(this); mbc.subscribe(TOPIC, myReloadingListeners.getMulticaster()); } void signalAppInitialized() { myAppInitialized.set(true); // XXX invoked from ApplicationInitializedListener which "doesn't guarantee EDT", but OTOH doesn't // guarantee NOT EDT, while run() here asserts !EDT if (!myUpdateSemaphore.waitFor(SEMAPHORE_WAIT_TIMEOUT)) { // reschedule the task LOG.debug("congestion on semaphore", new Throwable("Semaphore wait timeout exceeded")); ApplicationManager.getApplication().invokeLater(() -> update()); return; } myUpdateSemaphore.down(); try { new UpdatingTask(null).run(new EmptyProgressIndicator()); } finally { myUpdateSemaphore.up(); } } private Set<PluginContributor> createPluginContributors(Collection<ModuleRuntime> modules) { if (modules.isEmpty()) { return Collections.emptySet(); } // FIXME!!! List<ReloadableModule> sortedModules = PluginSorter.sortByDependencies(modules); Collection<ModuleRuntime> sortedModules = modules; List<PluginContributor> contributors = new ArrayList<>(); for (ModuleRuntime module : sortedModules) { contributors.add(new ModulePluginContributor2(module)); } return new LinkedHashSet<>(contributors); } private void fireAfterPluginsLoaded(List<PluginContributor> contributorsToLoad) { if (contributorsToLoad.isEmpty()) { return; } if (LOG.isDebugLevel()) { // there's no way to tell the number of actual topic listeners, therefore we // assume there's at least one to perform dispatch, and optionally those registered directly final String m = "Dispatch %d contributors loaded to %d or more listeners"; LOG.debug(String.format(m, contributorsToLoad.size(), 1 + myReloadingListeners.getListeners().size())); } getPublisher().afterPluginsLoaded(contributorsToLoad); } private void fireBeforePluginsUnloaded(List<PluginContributor> contributorsToUnload) { if (contributorsToUnload.isEmpty()) { return; } if (LOG.isDebugLevel()) { final String m = "Dispatch %d contributors unloaded to %d or more listeners"; LOG.debug(String.format(m, contributorsToUnload.size(), 1 + myReloadingListeners.getListeners().size())); } getPublisher().beforePluginsUnloaded(contributorsToUnload); } public void addReloadingListener(@NotNull PluginReloadingListener listener) { myReloadingListeners.addListener(listener); } public void removeReloadingListener(PluginReloadingListener listener) { myReloadingListeners.removeListener(listener); } private static PluginReloadingListener getPublisher() { return ApplicationManager.getApplication().getMessageBus().syncPublisher(TOPIC); } /** * Registers new loader asynchronously in EDT. * Before the registration we load all contributors which have been loaded up to that moment */ public void register(@NotNull final PluginLoader loader) { LOG.debug("Registering the " + loader); myAccumulation.loaderChange(loader, null); update(); } /** * Unregisters the loader asynchronously in EDT. * Before the un-registration we unload all contributors which have remained loaded at that moment */ public void unregister(@NotNull final PluginLoader loader) { LOG.debug("Unregistering the " + loader); myAccumulation.loaderChange(null, loader); forceUpdate(); } /** * Loads the given plugin contributors one by one. Asynchronously via the platform edt queue. */ private void loadContributors(Set<PluginContributor> contributors, Set<PluginLoader> pluginLoaders, ProgressMonitor monitor) { if (pluginLoaders.isEmpty() || contributors.isEmpty()) { return; } final long beginTime = System.nanoTime(); try { monitor.start("Loading", pluginLoaders.size()); for (final PluginLoader loader : pluginLoaders) { List<PluginContributor> contribList = List.copyOf(contributors); loader.loadPlugins(contribList); monitor.advance(1); } } finally { monitor.done(); LOG.info(String.format("Loading of %d plugins took %.3f s", contributors.size(), (System.nanoTime() - beginTime) / 1e9)); } } /** * Unloads the given plugin contributors one by one. Asynchronously via the platform edt queue. */ private void unloadContributors(final Set<PluginContributor> contributors, Set<PluginLoader> pluginLoaders, ProgressMonitor monitor) { if (pluginLoaders.isEmpty() || contributors.isEmpty()) { return; } monitor.start("Unloading", pluginLoaders.size()); long beginTime = System.nanoTime(); try { for (final PluginLoader loader : pluginLoaders) { List<PluginContributor> contribList = List.copyOf(contributors); loader.unloadPlugins(contribList); monitor.advance(1); } } finally { monitor.done(); LOG.info(String.format("Unloading of %d plugins took %.3f s", contributors.size(), (System.nanoTime() - beginTime) / 1e9)); } } private Set<PluginContributor> calcContributorsToUnload(Set<PluginContributor> currentContributors, Collection<ModuleRuntime> toUnload) { if (toUnload.isEmpty()) { return Collections.emptySet(); } List<PluginContributor> toUnloadContributors = new ArrayList<>(); for (PluginContributor contributor : currentContributors) { if (contributor instanceof ModulePluginContributor2) { ModuleRuntime module = ((ModulePluginContributor2) contributor).getModule(); if (toUnload.contains(module)) { toUnloadContributors.add(contributor); } } } Collections.reverse(toUnloadContributors); return new LinkedHashSet<>(toUnloadContributors); } private Set<PluginContributor> getContributorsFromExtPoint() { class ExtPointContributor extends PluginContributor { private final ComponentContributorExtension myExtension; private ExtPointContributor(ComponentContributorExtension extension) { myExtension = extension; } @Override public BaseProjectPlugin createProjectPlugin() { if (myExtension.myProjectPartContributor != null) { return instantiateSafe(myExtension.myProjectPartContributor); } return null; } @Override public BaseApplicationPlugin createApplicationPlugin() { if (myExtension.myApplicationPartContributor != null) { return instantiateSafe(myExtension.myApplicationPartContributor); } return null; } private <T> T instantiateSafe(String contributorClassName) { try { Class<T> cls = myExtension.findClass(contributorClassName); return cls.getDeclaredConstructor().newInstance(); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) { String msg = String.format("Failed to load class %s from plugin %s", contributorClassName, getContributingPluginId()); PluginLoaderRegistry.LOG.error(msg, ex); return null; } } @Override public int hashCode() { String contributingPlugin = getContributingPluginId(); return Objects.hash(contributingPlugin, myExtension.myApplicationPartContributor, myExtension.myProjectPartContributor); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (false == obj instanceof ExtPointContributor) { return false; } ExtPointContributor other = (ExtPointContributor) obj; return other.getContributingPluginId().equals(getContributingPluginId()) && Objects.equals(myExtension.myApplicationPartContributor, other.myExtension.myApplicationPartContributor) && Objects.equals(myExtension.myProjectPartContributor, other.myExtension.myProjectPartContributor); } @Override public String toString() { String app = JavaNameUtil.shortName(myExtension.myApplicationPartContributor); String proj = JavaNameUtil.shortName(myExtension.myProjectPartContributor); return String.format("ext-point contributor (%s, %s) from %s", app, proj, getContributingPluginId()); } private String getContributingPluginId() { return myExtension.getPluginDescriptor().getPluginId().getIdString(); } } HashSet<PluginContributor> rv = new HashSet<>(); for (ComponentContributorExtension ext : ComponentContributorExtension.POINT.getExtensions()) { rv.add(new ExtPointContributor(ext)); } return rv; } /** * here we come from the CLM notifications #onLoaded, #onUnloaded */ private void update() { if (myUpdateIsScheduledInEDT.compareAndSet(false, true)) { // logging more details to catch IJPL-205891 LOG.info("update. Current ThreadContext: " + ThreadContext.currentThreadContext()); // doesn't make sense to ask PI from pooled thread, the only chance to get not null, imo, is here. final ProgressIndicator globalProgressIndicator = ProgressManager.getGlobalProgressIndicator(); // no idea which executor/thread pool to use, e.g. seen uses of AppExecutorUtil.getAppExecutorService() ApplicationManager.getApplication().executeOnPooledThread(() -> { if (!myUpdateSemaphore.waitFor(SEMAPHORE_WAIT_TIMEOUT)) { // reschedule the task // undo flag set myUpdateIsScheduledInEDT.set(false); LOG.debug("congestion on semaphore", new Throwable("Semaphore wait timeout exceeded")); ApplicationManager.getApplication().invokeLater(() -> update()); return; }; myUpdateSemaphore.down(); try { // we need to get out of application read since it is impossible to #invokeAndWait from read, lets postpone LOG.debug("running the task later"); // trying to pass the current indicator, for example this helps us to reload plugins within the global project open indicator runTask(globalProgressIndicator); } finally { myUpdateSemaphore.up(); } }); } } /** * here we come from loaders registering/unregistering * * we would like on app/project dispose to unload all extensions in time, so we ignore if another update is already scheduled */ private void forceUpdate() { LOG.debug("force update"); if (!ThreadUtils.isInEDT()) { update(); } else { ThreadUtils.assertEDT(); if (isAppLoaded()) { if (!myUpdateSemaphore.waitFor(SEMAPHORE_WAIT_TIMEOUT)) { // reschedule the task LOG.debug("force update failed: congestion on semaphore", new Throwable("Semaphore wait timeout exceeded")); ApplicationManager.getApplication().invokeLater(() -> update()); return; } myUpdateSemaphore.down(); try { UpdatingTask task = new UpdatingTask(null); // here we are also when #disposeComponent is called in AppPlManager // async will not do, the app will be disposed then, so sync update with a good old UI freeze myUpdateIsScheduledInEDT.set(true); task.update(new EmptyProgressMonitor()); } finally { myUpdateSemaphore.up(); } } else { // here we are on any not first project open update(); } } } // now we are not in read private void runTask(@Nullable ProgressIndicator oldIndicator) { if (ApplicationManager.getApplication() == null || ApplicationManager.getApplication().isDisposed()) { return; } assert (!ApplicationManager.getApplication().isReadAccessAllowed()); assert !ThreadUtils.isInEDT(); // we are in EDT iff we have 'write' LOG.trace("running task with old/new indicator"); UpdatingTask task = new UpdatingTask(null); if (!isAppLoaded()) { LOG.trace("rescheduling task"); return; } assert isAppLoaded(); if (oldIndicator == null) { // trying to find any indicator oldIndicator = ProgressManager.getGlobalProgressIndicator(); } if (oldIndicator != null && oldIndicator.isShowing()) { // let's run under the old indicator LOG.trace("running with the old PI"); task.run(oldIndicator); } else { // usually this section is called after rebuild/make // we have no indicator -- lets create one LOG.trace("queued with the new PI"); // logging more details to catch IJPL-205891 LOG.info("run task @" + System.identityHashCode(task) + " Current ThreadContext: " + ThreadContext.currentThreadContext() + " Application.isTopmostReadAccessAllowed=" + ApplicationManager.getApplication().isTopmostReadAccessAllowed() + " Application.isReadAccessAllowed=" + ApplicationManager.getApplication().isReadAccessAllowed()); task.queue(); } } // we don't want to do anything until components are all #initialized // this is just to guarantee that we will have only one plugin reload before we open a project // consider the period when app is not loaded a dead zone for this class, the plugin update are disabled during that moment private boolean isAppLoaded() { return myAppInitialized.get(); } /** * This task flushes all added/removed loaders and added/removed contributors * It's executed from a pooled thread (regular Task behavior), with progress window (isModal() == true). * For MPS needs, it fires update() in EDT thread and pooled thread waits for EDT activity to complete. * @see #update */ private class UpdatingTask extends Task.Modal { UpdatingTask(@Nullable Project project) { super(project, "Reloading MPS Plugins", false); } @Override public void run(@NotNull ProgressIndicator indicator) { // here we just try to update the caption and freeze edt to do our reload of extensions there assert !ThreadUtils.isInEDT(); // MPS-generated ApplicationPlugins register actions (createGroups->BAP.addAction), and it happens in EDT // (invokeAndWait, below), but ActionManager service doesn't want to be initialized in EDT, therefore we make // sure it's initialized while we're not in EDT. See MPS-33757 // FIXME this is a workaround, need a proper fix. I don't understand the limitation of ActionManager/EDT, // and what's the regular initialization sequence for the ActionManager class (i.e. how does the rest of // IDEA code make sure it doesn't invoke it accidentally) // Perhaps, the issue is due to improper timing caused by MyApplicationInitializedListener (need to figure // out alternative mechanism to trigger plugin reload). Another approach would be not to use EDT for update // although I expect assumptions about EDT in App/ProjectPlugin initialization code. ActionManager.getInstance(); // explicit ANY modality state, despite the fact invokeAndWait documentation promises modality state of the current task: // "When invoked in the thread of some modal progress, returns modality state corresponding to that progress' dialog". // Indeed, occasionally I see modality tailored for this model task, however, I've also seen NON_MODAL value, which // prevents/delays update. As long as we need EDT just to ensure MPS plugins can init/dispose their UI elements, and not for user // interaction, I hope "ANY" modality is good (== we just need to run this code in EDT ASAP). final ModalityState modalityState = ApplicationManager.getApplication().getAnyModalityState(); boolean showing = indicator.isShowing(); if (!showing) { // we cannot do anything, lets just freeze without any progress ApplicationManager.getApplication().invokeAndWait(() -> update(new EmptyProgressMonitor()), modalityState); // I've never seen this part of code working, can't be certain defaultModalityState() won't work here, but // with the argument, above, seems fair to run with ANY, and, second, helps code consistency. } else { try { indicator.pushState(); indicator.setText("Reloading MPS Plugins"); // we hope that the user will see the text but that is unlikely // this also does not work // try { // TimeUnit.MILLISECONDS.sleep(200); // } catch (InterruptedException e) { // e.printStackTrace(); // } // logging more details to catch IJPL-205891 LOG.info("calling invokeAndWait @" + System.identityHashCode(this) + " Current ThreadContext: " + ThreadContext.currentThreadContext()); ApplicationManager.getApplication().invokeAndWait(() -> update(new EmptyProgressMonitor()), modalityState); } finally { indicator.popState(); } } } public void update(@NotNull final ProgressMonitor monitor) { try { ThreadUtils.assertEDT(); if (ApplicationManager.getApplication().isDisposed()) return; myUpdateIsScheduledInEDT.compareAndSet(true, false); monitor.start("Reloading MPS Plugins", 6); ProgressManager.checkCanceled(); // FWIW, here used to be quite an interesting log explaining why we needed model read and CLM transactions, and // why it's not true anymore. // // NOTE: when we call #reset we are bound to process those changes, otherwise we lose them // for instance, that is the reason we cannot call #checkCancelled here // FIXME although with our own code we can have sort of 'staged' reset here, and 'revert' reset() in case // operation was cancelled. Snapshot snapshot = myAccumulation.reset(); try { // this is in clm transaction, so we do not get any updates on the delta with modules Delta<ModuleRuntime> moduleDelta = snapshot.getModuleDelta(); Delta<PluginLoader> loadersDelta = snapshot.getLoaderDelta(); if (loadersDelta.isEmpty() && moduleDelta.isEmpty()) { LOG.debug("Nothing to do in update"); return; } monitor.advance(1); Set<PluginContributor> toUnloadContributors = calcContributorsToUnload(myCurrentContributors, moduleDelta.toUnload); Set<PluginContributor> toLoadContributors = createPluginContributors(moduleDelta.toLoad); List<PluginContributor> notifyToUnload; List<PluginContributor> notifyToLoad; if (loadersDelta.isEmpty()) { notifyToLoad = new ArrayList<>(toLoadContributors); notifyToUnload = new ArrayList<>(toUnloadContributors); } else { // if there's new or gone loader (e.g. opened/closed project and ProjectPluginManager) // we are going to consult all current contributors (see addLoaders()/removeLoaders()), hence // need to include myCurrentContributors into notification set to avoid scenarios like MPSSPRT-413 // where a project open event comes after all contributors are there, toLoadContributors.isEmpty and // fireAfterPluginsLoaded() sends notification which is ignored as the list of contributors is empty. // Indeed, it's unlikely there ever would be a listener to process actual PluginContributor instances, but // it doesn't hurt to be responsible here. notifyToLoad = new ArrayList<>(myCurrentContributors); notifyToLoad.addAll(toLoadContributors); notifyToUnload = new ArrayList<>(myCurrentContributors); } if (LOG.isInfoLevel()) { final String m = "Running Update Task : loaders %s; contributors : [+%d / -%d]; %s"; LOG.info(String.format(m, loadersDelta, toLoadContributors.size(), toUnloadContributors.size(), Thread.currentThread())); } fireBeforePluginsUnloaded(notifyToUnload); monitor.step("Unloading..."); clearIDEMenusFromOurActionRefs(); clearIDEASerializationCaches(); clearIDEAIconsGlobalCache(snapshot.getCLsToBeDisposed()); // for ALL myCurrentContributors AND loadersDelta.toUnload do PC.unloadPlugins; myCurrentLoaders\= toUnload removeLoaders(loadersDelta.toUnload, monitor); // for contributorsDelta.toUnload AND myCurrentLoaders do PC.unloadPlugins; myCurrentContributors \= toUnload // XXX if toUnload == myCurrentContributors, a pair of removeLoaders/removeContributors effectively unloads all plugins removeContributors(toUnloadContributors, monitor); monitor.step("Loading..."); addLoaders(loadersDelta.toLoad, monitor); addIdeaExtPointPluginContributors(monitor); addContributors(toLoadContributors, monitor); fireAfterPluginsLoaded(notifyToLoad); } catch (Throwable t) { LOG.error("caught some error during #update", t); } finally { snapshot.invokePostRunnables(); } } catch (VirtualMachineError e) { throw e; } catch (ProcessCanceledException ignored) { } catch (Throwable t) { LOG.error("Problem while reloading mps-plugins in EDT", t); } finally { monitor.done(); } } private void addContributors(Set<PluginContributor> contributorsToAdd, ProgressMonitor monitor) { contributorsToAdd.removeAll(myCurrentContributors); LOG.debug("Loading " + contributorsToAdd.size() + " new contributors to " + myCurrentLoaders.size() + " current loaders"); loadContributors(contributorsToAdd, new HashSet<>(myCurrentLoaders), monitor.subTask(1)); myCurrentContributors.addAll(contributorsToAdd); } private void addIdeaExtPointPluginContributors(@NotNull ProgressMonitor monitor) { Set<PluginContributor> factories = new LinkedHashSet<>(getContributorsFromExtPoint()); factories.removeAll(myCurrentContributors); LOG.debug("Loading " + factories.size() + " Factories"); loadContributors(factories, new HashSet<>(myCurrentLoaders), monitor.subTask(1)); myCurrentContributors.addAll(factories); } private void addLoaders(Set<PluginLoader> loadersToAdd, ProgressMonitor monitor) { loadersToAdd.removeAll(myCurrentLoaders); LOG.debug("Loading " + myCurrentContributors.size() + " current contributors to new " + loadersToAdd.size() + " loaders"); loadContributors(myCurrentContributors, loadersToAdd, monitor.subTask(1)); myCurrentLoaders.addAll(loadersToAdd); } private void removeContributors(Set<PluginContributor> contributorsToRemove, ProgressMonitor monitor) { contributorsToRemove.retainAll(myCurrentContributors); // just a precaution LOG.debug("Unloading " + contributorsToRemove.size() + " contributors from " + myCurrentLoaders.size() + " current loaders"); unloadContributors(contributorsToRemove, new HashSet<>(myCurrentLoaders), monitor.subTask(1)); myCurrentContributors.removeAll(contributorsToRemove); } private void removeLoaders(Set<PluginLoader> loadersToRemove, ProgressMonitor monitor) { loadersToRemove.retainAll(myCurrentLoaders); // just a precaution LOG.debug("Unloading " + myCurrentContributors.size() + " current contributors from " + loadersToRemove.size() + " loaders"); unloadContributors(myCurrentContributors, loadersToRemove, monitor.subTask(1)); myCurrentLoaders.removeAll(loadersToRemove); } private void clearIDEAIconsGlobalCache(@NotNull Collection<ClassLoader> classLoadersToBeDisposed) { for (ClassLoader cl : classLoadersToBeDisposed) { // until IDEA-345462 is fixed, patch the platform when preparing for use in MPS and expose the method IconLoader.detachClassLoader(cl); } final ProjectManager pm = ProjectManager.getInstanceIfCreated(); if (pm == null) { return; } for (Project project : pm.getOpenProjects()) { FileEditorManagerEx.getInstanceEx(project).refreshIcons(); } } @SuppressWarnings("UnstableApiUsage") private void clearIDEASerializationCaches() { try { Optional<JdomSerializer> jdomSerializer = ServiceLoader.load(JdomSerializer.class, JdomSerializer.class.getClassLoader()).findFirst(); jdomSerializer.ifPresent(JdomSerializer::clearSerializationCaches); } catch (Throwable t) { LOG.error("Caught exception while clearing IDEA serialization caches", t); } } @SuppressWarnings("UnstableApiUsage") private void clearIDEMenusFromOurActionRefs() { try { WindowManagerEx windowManager = WindowManagerEx.getInstanceEx(); for (Project project : ProjectManager.getInstance().getOpenProjects()) { ProjectFrameHelper frame = windowManager.getFrameHelper(project); if (frame != null) { frame.updateView(); } } ProjectFrameHelper frame = windowManager.getFrameHelper(null); if (frame != null) { frame.updateView(); } } catch (Throwable t) { LOG.error("Caught exception while clearing IDE menus", t); } } } @Override public void dispose() { MPSCoreComponents coreComponents = MPSCoreComponents.getInstance(); Platform mpsPlatform = coreComponents.getPlatform(); mpsPlatform.findComponent(LanguageRegistry.class).removeRegistryListener(myClassesListener); } private class SchedulingUpdateListener implements ModuleDeploymentListener { @Override public void deploymentStateChanged(@NotNull ModuleDeploymentChange change) { final Runnable releaseCLs = change.acquireRemovedTrackingLock(); AtomicInteger added = new AtomicInteger(0); AtomicInteger addedWE = new AtomicInteger(0); AtomicInteger removed = new AtomicInteger(0); AtomicInteger removedWE = new AtomicInteger(0); AtomicInteger reloaded = new AtomicInteger(0); AtomicInteger reloadedWE = new AtomicInteger(0); // XXX no real use for hash set here, for pc2load, as we just fill with new instances, // however, it's just too much pain now to change Delta api to accommodate Collection final LinkedHashSet<ModuleRuntime> mr2load = new LinkedHashSet<>(); final LinkedHashSet<ModuleRuntime> mr2Unload = new LinkedHashSet<>(); // I don't filter classLoaders2Dispose to match plugin modules as there's no way to 'release' subset only. // Besides, these CLs are likely dependant between each other anyway, why bother. // Moreover, update() uses these to refresh various caches, so it's highly likely we need to clear all these. Set<ClassLoader> classLoaders2Dispose = new HashSet<>(); change.forEachAdded(t -> { added.incrementAndGet(); if (t.withExtensions()) { addedWE.incrementAndGet(); mr2load.add(t); } }); change.forEachRemoved(t -> { removed.incrementAndGet(); classLoaders2Dispose.add(t.getModuleClassLoader()); if (t.withExtensions()) { removedWE.incrementAndGet(); mr2Unload.add(t); } }); change.forEachReloaded(t -> { reloaded.incrementAndGet(); if (t.withExtensions()) { reloadedWE.incrementAndGet(); } }); String traceMsg = "deployment state change (total/with extensions): added: %d/%d; removed: %d/%d; reloaded: %d/%d. Thread: %s"; LOG.debug(String.format(traceMsg, added.get(), addedWE.get(), removed.get(), removedWE.get(), reloaded.get(), reloadedWE.get(), Thread.currentThread().getName())); if (!mr2load.isEmpty()) { myAccumulation.onLoad(mr2load); } if (!mr2Unload.isEmpty()) { myAccumulation.onUnload(mr2Unload, classLoaders2Dispose, releaseCLs); } else { releaseCLs.run(); } if (!mr2load.isEmpty() || !mr2Unload.isEmpty()) { update(); } } } private final EventAccumulation myAccumulation = new EventAccumulation(); // access to all data members is synchronized through access methods static class EventAccumulation { private final Delta<ModuleRuntime> myDelta = new Delta<>(); private final List<ClassLoader> myCLsToBeDisposed = new ArrayList<>(); // to be disposed in the next invocation of UpdatingTask#update private final Queue<Runnable> myPostRunnableQueue = new LinkedList<>(); private final Delta<PluginLoader> myLoaderDelta = new Delta<>(); synchronized void onUnload(Set<ModuleRuntime> unloadedModules, @NotNull Set<ClassLoader> disposingCLs, @Nullable Runnable postRunnable) { myDelta.unload(unloadedModules); myCLsToBeDisposed.addAll(disposingCLs); if (postRunnable != null) { myPostRunnableQueue.add(postRunnable); } } synchronized void onLoad(Set<ModuleRuntime> loadedModules) { myDelta.load(loadedModules); } synchronized void loaderChange(@Nullable PluginLoader added, @Nullable PluginLoader removed) { if (added != null) { myLoaderDelta.load(Collections.singleton(added)); } if (removed != null) { myLoaderDelta.unload(Collections.singleton(removed)); } } @NotNull public synchronized Snapshot reset() { return new Snapshot(myDelta.reset(), myLoaderDelta.reset(), snapshotCLs(), snapshotPostRunnables()); } @NotNull private List<ClassLoader> snapshotCLs() { List<ClassLoader> cls2dispose = new ArrayList<>(myCLsToBeDisposed); myCLsToBeDisposed.clear(); return cls2dispose; } @NotNull private List<Runnable> snapshotPostRunnables() { List<Runnable> postRunnablesToRun = new ArrayList<>(myPostRunnableQueue); myPostRunnableQueue.clear(); return postRunnablesToRun; } } final static class Snapshot { private final Delta<ModuleRuntime> myModuleDelta; private final Delta<PluginLoader> myLoaderDelta; private final List<ClassLoader> myClassLoaders; private final List<Runnable> myPostRun; Snapshot(Delta<ModuleRuntime> md, Delta<PluginLoader> pld, List<ClassLoader> cbd, List<Runnable> postRun) { myModuleDelta = md; myLoaderDelta = pld; myClassLoaders = cbd; myPostRun = postRun; } @NotNull Delta<ModuleRuntime> getModuleDelta() { return myModuleDelta; } @NotNull Delta<PluginLoader> getLoaderDelta() { return myLoaderDelta; } @NotNull List<ClassLoader> getCLsToBeDisposed() { return myClassLoaders; } void invokePostRunnables() { myPostRun.forEach(Runnable::run); } } private static final class Delta<T> { private final Set<T> toUnload; private final Set<T> toLoad; public Delta() { toUnload = new LinkedHashSet<>(); toLoad = new LinkedHashSet<>(); } public Delta(@NotNull Delta<T> delta) { this(delta.toLoad, delta.toUnload); } public Delta(@NotNull Collection<T> load, @NotNull Collection<T> unload) { this.toLoad = new LinkedHashSet<>(load); this.toUnload = new LinkedHashSet<>(unload); } public void clear() { toUnload.clear(); toLoad.clear(); } public void load(Set<T> ts) { toLoad.addAll(ts); } public void unload(Set<T> ts) { toUnload.addAll(ts); toLoad.removeAll(ts); } public boolean isEmpty() { return toLoad.isEmpty() && toUnload.isEmpty(); } @Override public String toString() { return String.format("∆[load:%d; unload:%d]", toLoad.size(), toUnload.size()); } @NotNull public Delta<T> reset() { Delta<T> tDelta = new Delta<>(this); clear(); return tDelta; } } }