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;
}
}
}