/*
* 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() + "]";
}
}
}