/* * Copyright 2003-2011 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.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.components.ApplicationComponent; import com.intellij.openapi.progress.EmptyProgressIndicator; 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.util.WaitForProgressToShow; import jetbrains.mps.classloading.ClassLoaderManager; import jetbrains.mps.classloading.DeployListener; import jetbrains.mps.ide.MPSCoreComponents; import jetbrains.mps.ide.ThreadUtils; import jetbrains.mps.ide.actions.Ide_PluginInitializer; import jetbrains.mps.make.IMakeService; import jetbrains.mps.module.ReloadableModule; import jetbrains.mps.progress.ProgressMonitorAdapter; import jetbrains.mps.project.Solution; import jetbrains.mps.project.structure.modules.SolutionKind; import jetbrains.mps.smodel.Language; import jetbrains.mps.smodel.MPSModuleRepository; import jetbrains.mps.smodel.ModelAccessHelper; import jetbrains.mps.util.annotation.ToRemove; import org.apache.log4j.Logger; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.module.ModelAccess; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SRepository; import org.jetbrains.mps.openapi.util.ProgressMonitor; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import static java.util.stream.Collectors.toCollection; /** * 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> * TODO: currently it reloads only the ModulePluginContributors, need to work on AbstractPluginFactories also. Makes sense to remove these factories at all */ public class PluginLoaderRegistry implements ApplicationComponent { private static final Logger LOG = Logger.getLogger(PluginLoaderRegistry.class); private final ClassLoaderManager myClassLoaderManager; private final ModelAccess myModelAccess; private final DeployListener myClassesListener = new SchedulingUpdateListener(); private final Set<PluginContributor> myCurrentContributors = new LinkedHashSet<>(); private final Set<PluginLoader> myCurrentLoaders = new LinkedHashSet<>(); private final AtomicBoolean myDirtyFlag = new AtomicBoolean(false); public PluginLoaderRegistry(MPSCoreComponents coreComponents, @SuppressWarnings(value="UnusedParameters") Ide_PluginInitializer idePlugin) { myClassLoaderManager = coreComponents.getClassLoaderManager(); SRepository repo = coreComponents.getPlatform().findComponent(MPSModuleRepository.class); assert repo != null; myModelAccess = repo.getModelAccess(); } private static Set<PluginContributor> createPluginContributors(Collection<ReloadableModule> modules) { List<ReloadableModule> sortedModules = new PluginSorter(modules).sortByDependencies(); List<PluginContributor> contributors = new ArrayList<>(); for (ReloadableModule module : sortedModules) { PluginContributor contributor = createPluginContributor(module); if (contributor != null) { contributors.add(contributor); } } return new LinkedHashSet<>(contributors); } @Nullable private static PluginContributor createPluginContributor(@NotNull ReloadableModule module) { if (module.willLoad()) { LOG.trace("Creating plugin contributor from " + module); return new ModulePluginContributor(module); } return null; } /** * 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) { synchronized (myLoadersDeltaLock) { LOG.debug("Registering the " + loader); myLoaderDelta.load(Collections.singleton(loader)); } } /** * Unregisters the loader asynchronously in EDT. * Before the unregistration we unload all contributors which have remained loaded at that moment */ public void unregister(@NotNull final PluginLoader loader) { synchronized (myLoadersDeltaLock) { LOG.debug("Unregistering the " + loader); myLoaderDelta.unload(Collections.singleton(loader)); scheduleUpdate(); // fixme hack to schedule on project closing. appropriate classloading events will do in the next release } } /** * 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) { loader.loadPlugins(new ArrayList<>(contributors)); 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) { loader.unloadPlugins(new ArrayList<>(contributors)); monitor.advance(1); } } finally { monitor.done(); LOG.info(String.format("Unloading of %d plugins took %.3f s", contributors.size(), (System.nanoTime() - beginTime) / 1e9)); } } private void runTask(Task task) { LOG.trace("running task with new indicator"); if (isMakeActive()) { task.run(new EmptyProgressIndicator()); } else { ProgressManager.getInstance().run(task); } } private Set<PluginContributor> calcContributorsToUnload(Set<PluginContributor> currentContributors, Set<ReloadableModule> toUnload) { List<PluginContributor> toUnloadContributors = new ArrayList<>(); for (PluginContributor contributor : currentContributors) { if (contributor instanceof ModulePluginContributor) { ReloadableModule module = ((ModulePluginContributor) contributor).getModule(); if (toUnload.contains(module)) { toUnloadContributors.add(contributor); } } } Collections.reverse(toUnloadContributors); return new LinkedHashSet<>(toUnloadContributors); } @Override @NonNls @NotNull public String getComponentName() { return PluginLoaderRegistry.class.getName(); } @Override public void initComponent() { myClassLoaderManager.addListener(myClassesListener); assert myCurrentContributors.isEmpty(); } /** * Factories are registered via IdeaInitializerDescriptor generated code. * They are initialized during the application initialization. * Note that these contributors are registered once and NEVER unregistered * This is one reason we need to get rid of them * * @deprecated mechanism will be disabled */ @ToRemove(version = 3.3) @Deprecated private Set<PluginContributor> getFactoryContributors(Set<PluginContributor> currentContributors) { final List<PluginContributor> pluginFactoriesRegistryContributors = getPluginFactoriesRegistryContributors(); return pluginFactoriesRegistryContributors.stream().filter(contributor -> !currentContributors.contains(contributor)).collect( toCollection(LinkedHashSet::new)); } private static List<PluginContributor> getPluginFactoriesRegistryContributors() { List<PluginContributor> pluginContributors; pluginContributors = new ArrayList<>(); Collection<AbstractPluginFactory> pluginFactories = PluginFactoriesRegistry.flush(); for (AbstractPluginFactory factory : pluginFactories) { pluginContributors.add(PluginContributor.adapt(factory)); } return pluginContributors; } private void update() { ThreadUtils.assertEDT(); if (myTaskInProgress) { // this happens to be called inside the UpdatingTask#doUpdate LOG.debug("Rescheduling update"); reschedule(); } LOG.debug("Updating"); Delta<PluginLoader> loadersDelta; Delta<ReloadableModule> moduleDelta; synchronized (myLoadersDeltaLock) { loadersDelta = new Delta<>(myLoaderDelta); myLoaderDelta.clear(); } synchronized (myDeltaLock) { moduleDelta = new Delta<>(myDelta); myDelta.clear(); } myDirtyFlag.set(false); if (loadersDelta.isEmpty() && moduleDelta.isEmpty()) { LOG.debug("Nothing to do in update"); return; } assert !myTaskInProgress; Set<PluginContributor> toUnloadContributors = calcContributorsToUnload(myCurrentContributors, getPluginModules(moduleDelta.toUnload)); Set<PluginContributor> toLoadContributors = new ModelAccessHelper(myModelAccess).runReadAction(() -> createPluginContributors(getPluginModules(moduleDelta.toLoad))); Delta<PluginContributor> contributorDelta = new Delta<>(toLoadContributors, toUnloadContributors); assert !myTaskInProgress; UpdatingTask task = new UpdatingTask(null, loadersDelta, contributorDelta); runTask(task); } private void reschedule() { Application application = ApplicationManager.getApplication(); application.invokeLater(this::update, ModalityState.NON_MODAL, application.getDisposed()); } private boolean isMakeActive() { return IMakeService.INSTANCE.isSessionActive(); } /** * This task flushes all added/removed loaders and added/removed contributors * update happens only inside this task * * @see #update */ private class UpdatingTask extends Task.Modal { @NotNull private final Delta<PluginLoader> loadersDelta; @NotNull private final Delta<PluginContributor> contributorsDelta; UpdatingTask(Project project, @NotNull Delta<PluginLoader> loadersDelta, @NotNull Delta<PluginContributor> contributorsDelta) { super(project, "Reloading MPS Plugins", false); this.loadersDelta = loadersDelta; this.contributorsDelta = contributorsDelta; } @Override public void run(@NotNull ProgressIndicator indicator) { if (loadersDelta.isEmpty() && contributorsDelta.isEmpty()) { LOG.debug("Nothing to do in update"); return; } ProgressMonitor monitor = new ProgressMonitorAdapter(indicator); try { LOG.info("Running Update Task : loaders " + loadersDelta + "; contributors : " + contributorsDelta + "; " + Thread.currentThread()); indicator.pushState(); indicator.setIndeterminate(true); monitor.start("Reloading MPS Plugins", 5); WaitForProgressToShow.runOrInvokeAndWaitAboveProgress(() -> doUpdate(monitor), indicator.getModalityState()); } finally { monitor.done(); indicator.popState(); } } private void doUpdate(ProgressMonitor monitor) { try { ThreadUtils.assertEDT(); assert !myTaskInProgress; myTaskInProgress = true; removeLoaders(monitor); removeContributors(monitor); addLoaders(monitor); addFactories(monitor); addContributors(monitor); } finally{ myTaskInProgress = false; } } private void addContributors(ProgressMonitor monitor) { Set<PluginContributor> contributorsToAdd = new LinkedHashSet<>(contributorsDelta.toLoad); contributorsToAdd.removeAll(myCurrentContributors); LOG.debug("Loading " + contributorsToAdd.size() + " new contributors to " + myCurrentLoaders.size() + " current loaders"); loadContributors(contributorsToAdd, myCurrentLoaders, monitor.subTask(1)); myCurrentContributors.addAll(contributorsToAdd); } private void addFactories(ProgressMonitor monitor) { Set<PluginContributor> factories = getFactoryContributors(myCurrentContributors); factories.removeAll(myCurrentContributors); LOG.debug("Loading " + factories.size() + " Factories"); loadContributors(factories, myCurrentLoaders, monitor.subTask(1)); myCurrentContributors.addAll(factories); } private void addLoaders(ProgressMonitor monitor) { Set<PluginLoader> loadersToAdd = loadersDelta.toLoad; 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(ProgressMonitor monitor) { Set<PluginContributor> contributorsToRemove = contributorsDelta.toUnload; contributorsToRemove.retainAll(myCurrentContributors); // just a precaution LOG.debug("Unloading " + contributorsToRemove.size() + " contributors from " + myCurrentLoaders.size() + " current loaders"); unloadContributors(contributorsToRemove, myCurrentLoaders, monitor.subTask(1)); myCurrentContributors.removeAll(contributorsToRemove); } private void removeLoaders(ProgressMonitor monitor) { Set<PluginLoader> loadersToRemove = loadersDelta.toUnload; 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); } } /** * NOTE: * not unloading plugins on application dispose (since we are inside the dispose Application.isDisposed == true; * it is not tolerated by ActionGroup#getChildren which is called in some of the plugins #dispose method. */ private void scheduleUpdate() { if (myDirtyFlag.compareAndSet(false, true)) { Application application = ApplicationManager.getApplication(); if (ThreadUtils.isInEDT() && !application.isDisposed()) { update(); } else { reschedule(); } } } private Set<ReloadableModule> getPluginModules(Collection<ReloadableModule> modules) { return modules.stream().filter(this::isPluginModule).collect(toCollection(LinkedHashSet::new)); } private boolean isPluginModule(SModule module) { if (module instanceof ReloadableModule) { if (module instanceof Language) { return true; } if (module instanceof Solution) { SolutionKind kind = ((Solution) module).getKind(); return kind != SolutionKind.NONE; } } return false; } @Override public void disposeComponent() { myClassLoaderManager.removeListener(myClassesListener); } private class SchedulingUpdateListener implements DeployListener { @Override public void onUnloaded(Set<ReloadableModule> unloadedModules, @NotNull ProgressMonitor monitor) { synchronized (myDeltaLock) { myDelta.unload(unloadedModules); } } @Override public void onLoaded(Set<ReloadableModule> loadedModules, @NotNull ProgressMonitor monitor) { synchronized (myDeltaLock) { myDelta.load(loadedModules); scheduleUpdate(); } } } private volatile boolean myTaskInProgress = false; private final Object myDeltaLock = new Object(); private final Object myLoadersDeltaLock = new Object(); private final Delta<ReloadableModule> myDelta = new Delta<>(); private final Delta<PluginLoader> myLoaderDelta = new Delta<>(); private static 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 Set<T> loaded, @NotNull Set<T> unloaded) { toLoad = new LinkedHashSet<>(loaded); toUnload = new LinkedHashSet<>(unloaded); } public final 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 void apply(Set<T> tsToChange) { tsToChange.addAll(toLoad); tsToChange.removeAll(toUnload); } public boolean isEmpty() { return toLoad.isEmpty() && toUnload.isEmpty(); } @Override public String toString() { return "[toLoad: " + toLoad.size() + "; toUnload:" + toUnload.size() + "]"; } } }