Tuned "incompatible-with" diagnostic message
[idea/community.git] / platform / core-impl / src / com / intellij / ide / plugins / PluginManagerCore.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.ide.plugins;
3
4 import com.intellij.diagnostic.Activity;
5 import com.intellij.diagnostic.LoadingState;
6 import com.intellij.diagnostic.PluginException;
7 import com.intellij.diagnostic.StartUpMeasurer;
8 import com.intellij.ide.plugins.cl.PluginClassLoader;
9 import com.intellij.openapi.application.Application;
10 import com.intellij.openapi.application.ApplicationManager;
11 import com.intellij.openapi.application.JetBrainsProtocolHandler;
12 import com.intellij.openapi.application.PathManager;
13 import com.intellij.openapi.application.impl.ApplicationInfoImpl;
14 import com.intellij.openapi.diagnostic.Logger;
15 import com.intellij.openapi.extensions.*;
16 import com.intellij.openapi.extensions.impl.ExtensionsAreaImpl;
17 import com.intellij.openapi.project.Project;
18 import com.intellij.openapi.util.BuildNumber;
19 import com.intellij.openapi.util.InvalidDataException;
20 import com.intellij.openapi.util.JDOMUtil;
21 import com.intellij.openapi.util.SafeJdomFactory;
22 import com.intellij.openapi.util.io.FileUtilRt;
23 import com.intellij.openapi.util.text.StringUtil;
24 import com.intellij.openapi.util.text.StringUtilRt;
25 import com.intellij.reference.SoftReference;
26 import com.intellij.serialization.SerializationException;
27 import com.intellij.util.*;
28 import com.intellij.util.concurrency.AppExecutorUtil;
29 import com.intellij.util.containers.ContainerUtil;
30 import com.intellij.util.containers.ContainerUtilRt;
31 import com.intellij.util.execution.ParametersListUtil;
32 import com.intellij.util.graph.DFSTBuilder;
33 import com.intellij.util.graph.GraphGenerator;
34 import com.intellij.util.io.URLUtil;
35 import com.intellij.util.lang.UrlClassLoader;
36 import org.jdom.Element;
37 import org.jdom.JDOMException;
38 import org.jetbrains.annotations.ApiStatus;
39 import org.jetbrains.annotations.NotNull;
40 import org.jetbrains.annotations.Nullable;
41 import org.jetbrains.annotations.TestOnly;
42
43 import java.io.*;
44 import java.lang.invoke.MethodHandle;
45 import java.lang.invoke.MethodHandles;
46 import java.lang.invoke.MethodType;
47 import java.lang.ref.Reference;
48 import java.net.MalformedURLException;
49 import java.net.URI;
50 import java.net.URISyntaxException;
51 import java.net.URL;
52 import java.nio.charset.StandardCharsets;
53 import java.nio.file.*;
54 import java.util.*;
55 import java.util.concurrent.*;
56 import java.util.function.Function;
57 import java.util.stream.Collectors;
58 import java.util.stream.Stream;
59
60 import static com.intellij.ide.plugins.DescriptorListLoadingContext.IGNORE_MISSING_INCLUDE;
61 import static com.intellij.ide.plugins.DescriptorListLoadingContext.IS_PARALLEL;
62
63 // Prefer to use only JDK classes. Any post start-up functionality should be placed in PluginManager class.
64 public final class PluginManagerCore {
65   public static final String META_INF = "META-INF/";
66   public static final String IDEA_IS_INTERNAL_PROPERTY = "idea.is.internal";
67
68   public static final String DISABLED_PLUGINS_FILENAME = "disabled_plugins.txt";
69
70   public static final PluginId CORE_ID = PluginId.getId("com.intellij");
71   public static final String CORE_PLUGIN_ID = "com.intellij";
72
73   public static final PluginId JAVA_PLUGIN_ID = PluginId.getId("com.intellij.java");
74   private static final PluginId JAVA_MODULE_ID = PluginId.getId("com.intellij.modules.java");
75
76   public static final String PLUGIN_XML = "plugin.xml";
77   public static final String PLUGIN_XML_PATH = META_INF + PLUGIN_XML;
78   private static final PluginId ALL_MODULES_MARKER = PluginId.getId("com.intellij.modules.all");
79
80   public static final String VENDOR_JETBRAINS = "JetBrains";
81
82   private static final String MODULE_DEPENDENCY_PREFIX = "com.intellij.module";
83
84   private static final PluginId SPECIAL_IDEA_PLUGIN_ID = PluginId.getId("IDEA CORE");
85
86   private static final String PROPERTY_PLUGIN_PATH = "plugin.path";
87
88   public static final String DISABLE = "disable";
89   public static final String ENABLE = "enable";
90   public static final String EDIT = "edit";
91
92   private static volatile Set<PluginId> ourDisabledPlugins;
93   private static Reference<Map<PluginId, Set<String>>> ourBrokenPluginVersions;
94   private static volatile IdeaPluginDescriptorImpl[] ourPlugins;
95   static volatile List<IdeaPluginDescriptorImpl> ourLoadedPlugins;
96
97   @SuppressWarnings("StaticNonFinalField")
98   public static volatile boolean isUnitTestMode = Boolean.getBoolean("idea.is.unit.test");
99
100   @SuppressWarnings("StaticNonFinalField") @ApiStatus.Internal
101   public static String ourPluginError;
102
103   @SuppressWarnings("StaticNonFinalField")
104   @ApiStatus.Internal
105   public static Set<PluginId> ourPluginsToDisable;
106   @SuppressWarnings("StaticNonFinalField")
107   @ApiStatus.Internal
108   public static Set<PluginId> ourPluginsToEnable;
109   /**
110    * Bundled plugins that were updated.
111    * When we update bundled plugin it becomes not bundled, so it is more difficult for analytics to use that data.
112    */
113   private static Set<PluginId> ourShadowedBundledPlugins;
114
115   private static Boolean isRunningFromSources;
116   private static volatile CompletableFuture<DescriptorListLoadingContext> descriptorListFuture;
117
118   private static BuildNumber ourBuildNumber;
119
120   @Nullable
121   @ApiStatus.Internal
122   public static String getPluginsCompatibleBuild() {
123     return System.getProperty("idea.plugins.compatible.build");
124   }
125
126   @Nullable
127   private static Runnable disabledPluginListener;
128
129   @ApiStatus.Internal
130   public static void setDisabledPluginListener(@NotNull Runnable value) {
131     disabledPluginListener = value;
132   }
133
134   /**
135    * Returns list of all available plugin descriptors (bundled and custom, include disabled ones). Use {@link #getLoadedPlugins()}
136    * if you need to get loaded plugins only.
137    *
138    * <p>
139    * Do not call this method during bootstrap, should be called in a copy of PluginManager, loaded by PluginClassLoader.
140    */
141   public static @NotNull IdeaPluginDescriptor @NotNull[] getPlugins() {
142     IdeaPluginDescriptor[] result = ourPlugins;
143     if (result == null) {
144       loadAndInitializePlugins(null, null);
145       return ourPlugins;
146     }
147     return result;
148   }
149
150   /**
151    * Returns descriptors of plugins which are successfully loaded into IDE. The result is sorted in a way that if each plugin comes after
152    * the plugins it depends on.
153    */
154   @NotNull
155   public static List<? extends IdeaPluginDescriptor> getLoadedPlugins() {
156     return getLoadedPlugins(null);
157   }
158
159   @NotNull
160   @ApiStatus.Internal
161   public static List<IdeaPluginDescriptorImpl> getLoadedPlugins(@Nullable ClassLoader coreClassLoader) {
162     List<IdeaPluginDescriptorImpl> result = ourLoadedPlugins;
163     if (result == null) {
164       loadAndInitializePlugins(null, coreClassLoader);
165       return ourLoadedPlugins;
166     }
167     return result;
168   }
169
170   @ApiStatus.Internal
171   public static boolean arePluginsInitialized() {
172     return ourPlugins != null;
173   }
174
175   @ApiStatus.Internal
176   static synchronized void doSetPlugins(@NotNull IdeaPluginDescriptorImpl @NotNull [] value) {
177     ourPlugins = value;
178     //noinspection NonPrivateFieldAccessedInSynchronizedContext
179     ourLoadedPlugins = Collections.unmodifiableList(getOnlyEnabledPlugins(value));
180   }
181
182   @ApiStatus.Internal
183   public static void loadDisabledPlugins(@NotNull String configPath, @NotNull Collection<PluginId> disabledPlugins) {
184     Path file = Paths.get(configPath, DISABLED_PLUGINS_FILENAME);
185     if (!Files.isRegularFile(file)) {
186       return;
187     }
188
189     List<String> requiredPlugins = StringUtil.split(System.getProperty(JetBrainsProtocolHandler.REQUIRED_PLUGINS_KEY, ""), ",");
190     try {
191       boolean updateDisablePluginsList = false;
192       try (BufferedReader reader = Files.newBufferedReader(file)) {
193         String id;
194         while ((id = reader.readLine()) != null) {
195           id = id.trim();
196           if (!requiredPlugins.contains(id) && !ApplicationInfoImpl.getShadowInstance().isEssentialPlugin(id)) {
197             disabledPlugins.add(PluginId.getId(id));
198           }
199           else {
200             updateDisablePluginsList = true;
201           }
202         }
203       }
204       finally {
205         if (updateDisablePluginsList) {
206           savePluginsList(disabledPlugins, file, false);
207           fireEditDisablePlugins();
208         }
209       }
210     }
211     catch (IOException e) {
212       getLogger().info("Unable to load disabled plugins list from " + file, e);
213     }
214   }
215
216   // For use in headless environment only
217   public static void dontLoadDisabledPlugins() {
218     ourDisabledPlugins = Collections.emptySet();
219   }
220
221   /**
222    * @deprecated Bad API, sorry. Please use {@link #isDisabled(PluginId)} to check plugin's state,
223    * {@link #enablePlugin(PluginId)}/{@link #disablePlugin(PluginId)} for state management,
224    * {@link #disabledPlugins()} to get an unmodifiable collection of all disabled plugins (rarely needed).
225    */
226   @Deprecated
227   @ApiStatus.ScheduledForRemoval(inVersion = "2020.2")
228   @NotNull
229   public static List<String> getDisabledPlugins() {
230     Set<PluginId> list = getDisabledIds();
231     return new AbstractList<String>() {
232       //<editor-fold desc="Just a ist-like immutable wrapper over a set; move along.">
233       @Override
234       public boolean contains(Object o) {
235         return list.contains(o);
236       }
237
238       @Override
239       public int size() {
240         return list.size();
241       }
242
243       @Override
244       public String get(int index) {
245         if (index < 0 || index >= list.size()) {
246           throw new IndexOutOfBoundsException("index=" + index + " size=" + list.size());
247         }
248         Iterator<PluginId> iterator = list.iterator();
249         for (int i = 0; i < index; i++) {
250           iterator.next();
251         }
252         return iterator.next().getIdString();
253       }
254       //</editor-fold>
255     };
256   }
257
258   @NotNull
259   static Set<PluginId> getDisabledIds() {
260     Set<PluginId> result = ourDisabledPlugins;
261     if (result != null) {
262       return result;
263     }
264
265     // to preserve the order of additions and removals
266     if (System.getProperty("idea.ignore.disabled.plugins") != null) {
267       return Collections.emptySet();
268     }
269
270     //noinspection SynchronizeOnThis
271     synchronized (PluginManagerCore.class) {
272       result = ourDisabledPlugins;
273       if (result != null) {
274         return result;
275       }
276
277       result = new LinkedHashSet<>();
278       loadDisabledPlugins(PathManager.getConfigPath(), result);
279       ourDisabledPlugins = result;
280     }
281     return result;
282   }
283
284   @NotNull
285   public static Set<PluginId> disabledPlugins() {
286     return Collections.unmodifiableSet(getDisabledIds());
287   }
288
289   public static boolean isDisabled(@NotNull PluginId pluginId) {
290     return getDisabledIds().contains(pluginId);
291   }
292
293   /**
294    * @deprecated Use {@link #isDisabled(PluginId)}
295    */
296   @Deprecated
297   public static boolean isDisabled(@NotNull String pluginId) {
298     return getDisabledIds().contains(PluginId.getId(pluginId));
299   }
300
301   public static boolean isBrokenPlugin(@NotNull IdeaPluginDescriptor descriptor) {
302     PluginId pluginId = descriptor.getPluginId();
303     if (pluginId == null) {
304       return true;
305     }
306
307     Set<String> set = getBrokenPluginVersions().get(pluginId);
308     return set != null && set.contains(descriptor.getVersion());
309   }
310
311   @NotNull
312   private static Map<PluginId, Set<String>> getBrokenPluginVersions() {
313     Map<PluginId, Set<String>> result = SoftReference.dereference(ourBrokenPluginVersions);
314     if (result != null) {
315       return result;
316     }
317
318     if (System.getProperty("idea.ignore.disabled.plugins") != null) {
319       result = Collections.emptyMap();
320       ourBrokenPluginVersions = new java.lang.ref.SoftReference<>(result);
321       return result;
322     }
323
324     result = new HashMap<>();
325     try (InputStream resource = PluginManagerCore.class.getResourceAsStream("/brokenPlugins.txt");
326          BufferedReader br = new BufferedReader(new InputStreamReader(resource, StandardCharsets.UTF_8))) {
327       String s;
328       while ((s = br.readLine()) != null) {
329         s = s.trim();
330         if (s.startsWith("//")) {
331           continue;
332         }
333
334         List<String> tokens = ParametersListUtil.parse(s);
335         if (tokens.isEmpty()) {
336           continue;
337         }
338
339         if (tokens.size() == 1) {
340           throw new RuntimeException("brokenPlugins.txt is broken. The line contains plugin name, but does not contains version: " + s);
341         }
342
343         PluginId pluginId = PluginId.getId(tokens.get(0));
344         List<String> versions = tokens.subList(1, tokens.size());
345
346         Set<String> set = result.get(pluginId);
347         if (set == null) {
348           set = new HashSet<>();
349           result.put(pluginId, set);
350         }
351         set.addAll(versions);
352       }
353     }
354     catch (IOException e) {
355       throw new RuntimeException("Failed to read /brokenPlugins.txt", e);
356     }
357
358     ourBrokenPluginVersions = new java.lang.ref.SoftReference<>(result);
359     return result;
360   }
361
362   private static void fireEditDisablePlugins() {
363     if (disabledPluginListener != null) {
364       disabledPluginListener.run();
365     }
366   }
367
368   public static void savePluginsList(@NotNull Collection<PluginId> ids, @NotNull Path file, boolean append) throws IOException {
369     Files.createDirectories(file.getParent());
370     try (BufferedWriter writer = (append ? Files.newBufferedWriter(file, StandardOpenOption.APPEND, StandardOpenOption.CREATE) : Files.newBufferedWriter(file))) {
371       writePluginsList(ids, writer);
372     }
373   }
374
375   public static void writePluginsList(@NotNull Collection<PluginId> ids, @NotNull Writer writer) throws IOException {
376     List<PluginId> sortedIds = new ArrayList<>(ids);
377     sortedIds.sort(null);
378     String separator = LineSeparator.getSystemLineSeparator().getSeparatorString();
379     for (PluginId id : sortedIds) {
380       writer.write(id.getIdString());
381       writer.write(separator);
382     }
383   }
384
385   /**
386    * @deprecated Use {@link #disablePlugin(PluginId)}
387    */
388   @Deprecated
389   public static boolean disablePlugin(@NotNull String id) {
390     return disablePlugin(PluginId.getId(id));
391   }
392
393   public static boolean disablePlugin(@NotNull PluginId id) {
394     Set<PluginId> disabledPlugins = getDisabledIds();
395     return disabledPlugins.add(id) && trySaveDisabledPlugins(disabledPlugins);
396   }
397
398   public static boolean enablePlugin(@NotNull PluginId id) {
399     Set<PluginId> disabledPlugins = getDisabledIds();
400     return disabledPlugins.remove(id) && trySaveDisabledPlugins(disabledPlugins);
401   }
402
403   /**
404    * @deprecated Use {@link #enablePlugin(PluginId)}
405    */
406   @Deprecated
407   public static boolean enablePlugin(@NotNull String id) {
408     return enablePlugin(PluginId.getId(id));
409   }
410
411   static boolean trySaveDisabledPlugins(@NotNull Collection<PluginId> disabledPlugins) {
412     try {
413       saveDisabledPlugins(disabledPlugins, false);
414       return true;
415     }
416     catch (IOException e) {
417       getLogger().warn("Unable to save disabled plugins list", e);
418       return false;
419     }
420   }
421
422   public static void saveDisabledPlugins(@NotNull Collection<PluginId> ids, boolean append) throws IOException {
423     saveDisabledPlugins(PathManager.getConfigPath(), ids, append);
424   }
425
426   public static void saveDisabledPlugins(@NotNull String configPath, @NotNull Collection<PluginId> ids, boolean append) throws IOException {
427     Path plugins = Paths.get(configPath, DISABLED_PLUGINS_FILENAME);
428     savePluginsList(ids, plugins, append);
429     ourDisabledPlugins = null;
430     fireEditDisablePlugins();
431   }
432
433   public static boolean isModuleDependency(@NotNull PluginId dependentPluginId) {
434     return dependentPluginId.getIdString().startsWith(MODULE_DEPENDENCY_PREFIX);
435   }
436
437   /**
438    * This is an internal method, use {@link PluginException#createByClass(String, Throwable, Class)} instead.
439    */
440   @ApiStatus.Internal
441   @NotNull
442   public static PluginException createPluginException(@NotNull String errorMessage, @Nullable Throwable cause,
443                                                       @NotNull Class<?> pluginClass) {
444     ClassLoader classLoader = pluginClass.getClassLoader();
445     PluginId pluginId = classLoader instanceof PluginClassLoader ? ((PluginClassLoader)classLoader).getPluginId()
446                                                                  : getPluginByClassName(pluginClass.getName());
447     return new PluginException(errorMessage, cause, pluginId);
448   }
449
450   @Nullable
451   public static PluginId getPluginByClassName(@NotNull String className) {
452     PluginId id = getPluginOrPlatformByClassName(className);
453     return (id == null || CORE_ID == id) ? null : id;
454   }
455
456   @Nullable
457   public static PluginId getPluginOrPlatformByClassName(@NotNull String className) {
458     PluginDescriptor result = getPluginDescriptorOrPlatformByClassName(className);
459     return result == null ? null : result.getPluginId();
460   }
461
462   @Nullable
463   @ApiStatus.Internal
464   public static PluginDescriptor getPluginDescriptorOrPlatformByClassName(@NotNull String className) {
465     List<IdeaPluginDescriptorImpl> loadedPlugins = ourLoadedPlugins;
466     if (loadedPlugins == null ||
467         className.startsWith("java.") ||
468         className.startsWith("javax.") ||
469         className.startsWith("kotlin.") ||
470         className.startsWith("groovy.")) {
471       return null;
472     }
473
474     IdeaPluginDescriptor result = null;
475     for (IdeaPluginDescriptorImpl o : loadedPlugins) {
476       ClassLoader classLoader = o.getPluginClassLoader();
477       if (classLoader == null || !hasLoadedClass(className, classLoader)) {
478         continue;
479       }
480
481       result = o;
482       break;
483     }
484
485     if (result == null) {
486       return null;
487     }
488
489     // return if the found plugin is not "core" or the package is obviously "core"
490     if (result.getPluginId() != CORE_ID ||
491         className.startsWith("com.jetbrains.") || className.startsWith("org.jetbrains.") ||
492         className.startsWith("com.intellij.") || className.startsWith("org.intellij.") ||
493         className.startsWith("com.android.") ||
494         className.startsWith("git4idea.") || className.startsWith("org.angularjs.")) {
495       return result;
496     }
497
498     // otherwise we need to check plugins with use-idea-classloader="true"
499     String root = PathManager.getResourceRoot(result.getPluginClassLoader(), "/" + className.replace('.', '/') + ".class");
500     if (root == null) {
501       return null;
502     }
503
504     for (IdeaPluginDescriptorImpl o : loadedPlugins) {
505       if (!o.getUseIdeaClassLoader()) {
506         continue;
507       }
508
509       Path path = o.getPluginPath();
510       if (!root.startsWith(FileUtilRt.toSystemIndependentName(path.toString()))) {
511         continue;
512       }
513
514       result = o;
515       break;
516     }
517     return result;
518   }
519
520   private static boolean hasLoadedClass(@NotNull String className, @NotNull ClassLoader loader) {
521     if (loader instanceof UrlClassLoader) {
522       return ((UrlClassLoader)loader).hasLoadedClass(className);
523     }
524
525     // it can be an UrlClassLoader loaded by another class loader, so instanceof doesn't work
526     Class<? extends ClassLoader> aClass = loader.getClass();
527     if (isInstanceofUrlClassLoader(aClass)) {
528       try {
529         return (Boolean)aClass.getMethod("hasLoadedClass", String.class).invoke(loader, className);
530       }
531       catch (Exception ignored) {
532       }
533     }
534     return false;
535   }
536
537   private static boolean isInstanceofUrlClassLoader(Class<?> aClass) {
538     String urlClassLoaderName = UrlClassLoader.class.getName();
539     while (aClass != null) {
540       if (aClass.getName().equals(urlClassLoaderName)) return true;
541       aClass = aClass.getSuperclass();
542     }
543     return false;
544   }
545
546   /**
547    * In 191.* and earlier builds Java plugin was part of the platform, so any plugin installed in IntelliJ IDEA might be able to use its
548    * classes without declaring explicit dependency on the Java module. This method is intended to add implicit dependency on the Java plugin
549    * for such plugins to avoid breaking compatibility with them.
550    */
551   @Nullable
552   private static IdeaPluginDescriptorImpl getImplicitDependency(@NotNull IdeaPluginDescriptor descriptor,
553                                                                 @Nullable IdeaPluginDescriptorImpl javaDep,
554                                                                 boolean hasAllModules) {
555     // skip our plugins as expected to be up-to-date whether bundled or not
556     if (descriptor.getPluginId() == CORE_ID || descriptor.getPluginId() == JAVA_PLUGIN_ID ||
557         VENDOR_JETBRAINS.equals(descriptor.getVendor()) ||
558         !hasAllModules ||
559         javaDep == null) {
560       return null;
561     }
562
563     // If a plugin does not include any module dependency tags in its plugin.xml, it's assumed to be a legacy plugin
564     // and is loaded only in IntelliJ IDEA, so it may use classes from Java plugin.
565     return hasModuleDependencies(descriptor) ? null : javaDep;
566   }
567
568   static boolean hasModuleDependencies(@NotNull IdeaPluginDescriptor descriptor) {
569     for (PluginId depId : descriptor.getDependentPluginIds()) {
570       if (depId == JAVA_PLUGIN_ID || depId == JAVA_MODULE_ID || isModuleDependency(depId)) {
571         return true;
572       }
573     }
574     return false;
575   }
576
577   private static boolean shouldLoadPlugins() {
578     try {
579       // no plugins during bootstrap
580       Class.forName("com.intellij.openapi.extensions.Extensions");
581     }
582     catch (ClassNotFoundException e) {
583       return false;
584     }
585     String loadPlugins = System.getProperty("idea.load.plugins");
586     return loadPlugins == null || Boolean.TRUE.toString().equals(loadPlugins);
587   }
588
589   @NotNull
590   private static ClassLoader createPluginClassLoader(ClassLoader @NotNull [] parentLoaders, @NotNull IdeaPluginDescriptorImpl descriptor, @NotNull UrlClassLoader.Builder urlLoaderBuilder) {
591     List<Path> classPath = descriptor.jarFiles;
592     if (classPath == null) {
593       classPath = descriptor.collectClassPath();
594     }
595     else {
596       descriptor.jarFiles = null;
597     }
598
599     if (descriptor.getUseIdeaClassLoader()) {
600       getLogger().warn(descriptor.getPluginId() + " uses deprecated `use-idea-classloader` attribute");
601       ClassLoader loader = PluginManagerCore.class.getClassLoader();
602       try {
603         // `UrlClassLoader#addURL` can't be invoked directly, because the core classloader is created at bootstrap in a "lost" branch
604         MethodHandle addURL = MethodHandles.lookup().findVirtual(loader.getClass(), "addURL", MethodType.methodType(void.class, URL.class));
605         for (Path pathElement : classPath) {
606           addURL.invoke(loader, localFileToUrl(pathElement, descriptor));
607         }
608         return loader;
609       }
610       catch (Throwable t) {
611         throw new IllegalStateException("An unexpected core classloader: " + loader.getClass(), t);
612       }
613     }
614     else {
615       List<URL> urls = new ArrayList<>(classPath.size());
616       for (Path pathElement : classPath) {
617         urls.add(localFileToUrl(pathElement, descriptor));
618       }
619       return new PluginClassLoader(urlLoaderBuilder.urls(urls), parentLoaders, descriptor.getPluginId(), descriptor, descriptor.getVersion(), descriptor.getPluginPath());
620     }
621   }
622
623   private static @NotNull URL localFileToUrl(@NotNull Path file, @NotNull IdeaPluginDescriptor descriptor) {
624     try {
625       // it is important not to have traversal elements in classpath
626       return new URL("file", "", file.normalize().toUri().getRawPath());
627     }
628     catch (MalformedURLException e) {
629       throw new PluginException("Corrupted path element: `" + file + '`', e, descriptor.getPluginId());
630     }
631   }
632
633   public static synchronized void invalidatePlugins() {
634     ourPlugins = null;
635     //noinspection NonPrivateFieldAccessedInSynchronizedContext
636     ourLoadedPlugins = null;
637     ourDisabledPlugins = null;
638     ourShadowedBundledPlugins = null;
639   }
640
641   private static void logPlugins(@NotNull IdeaPluginDescriptorImpl @NotNull[] plugins) {
642     StringBuilder bundled = new StringBuilder();
643     StringBuilder disabled = new StringBuilder();
644     StringBuilder custom = new StringBuilder();
645     for (IdeaPluginDescriptor descriptor : plugins) {
646       StringBuilder target;
647       if (!descriptor.isEnabled()) {
648         target = disabled;
649       }
650       else if (descriptor.isBundled() || descriptor.getPluginId() == SPECIAL_IDEA_PLUGIN_ID) {
651         target = bundled;
652       }
653       else {
654         target = custom;
655       }
656
657       if (target.length() > 0) {
658         target.append(", ");
659       }
660
661       target.append(descriptor.getName());
662       String version = descriptor.getVersion();
663       if (version != null) {
664         target.append(" (").append(version).append(')');
665       }
666     }
667
668     Logger logger = getLogger();
669     logger.info("Loaded bundled plugins: " + bundled);
670     if (custom.length() > 0) {
671       logger.info("Loaded custom plugins: " + custom);
672     }
673     if (disabled.length() > 0) {
674       logger.info("Disabled plugins: " + disabled);
675     }
676   }
677
678   public static boolean isRunningFromSources() {
679     Boolean result = isRunningFromSources;
680     if (result == null) {
681       result = Files.isDirectory(Paths.get(PathManager.getHomePath(), Project.DIRECTORY_STORE_FOLDER));
682       isRunningFromSources = result;
683     }
684     return result;
685   }
686
687   private static void prepareLoadingPluginsErrorMessage(@NotNull List<String> errors) {
688     prepareLoadingPluginsErrorMessage(errors, Collections.emptyList());
689   }
690
691   private static void prepareLoadingPluginsErrorMessage(@NotNull List<String> errors, @NotNull List<String> actions) {
692     if (errors.isEmpty()) {
693       return;
694     }
695
696     String message = "Problems found loading plugins:\n" + String.join("\n", errors);
697     Application app = ApplicationManager.getApplication();
698     if (app == null || !app.isHeadlessEnvironment() || isUnitTestMode) {
699       String errorMessage = Stream.concat(errors.stream().map(o -> o + "."), actions.stream()).collect(Collectors.joining("<p/>"));
700       if (ourPluginError == null) {
701         ourPluginError = errorMessage;
702       }
703       else {
704         ourPluginError += "<p/>\n" + errorMessage;
705       }
706
707       // as warn in tests
708       getLogger().warn(message);
709     }
710     else {
711       getLogger().error(message);
712     }
713   }
714
715   @NotNull
716   private static CachingSemiGraph<IdeaPluginDescriptorImpl> createPluginIdGraph(@NotNull List<IdeaPluginDescriptorImpl> descriptors,
717                                                                                 @NotNull Map<PluginId, IdeaPluginDescriptorImpl> idToDescriptorMap,
718                                                                                 boolean withOptional) {
719     IdeaPluginDescriptorImpl javaDep = idToDescriptorMap.get(JAVA_MODULE_ID);
720     boolean hasAllModules = idToDescriptorMap.containsKey(ALL_MODULES_MARKER);
721     Set<IdeaPluginDescriptorImpl> uniqueCheck = new HashSet<>();
722     return new CachingSemiGraph<>(descriptors, rootDescriptor -> {
723       List<PluginId> dependentPluginIds = ContainerUtil.newArrayList(rootDescriptor.getDependentPluginIds());
724       List<PluginId> incompatibleModuleIds = rootDescriptor.getIncompatibleModuleIds();
725       IdeaPluginDescriptorImpl implicitDep = getImplicitDependency(rootDescriptor, javaDep, hasAllModules);
726       PluginId[] optionalDependentPluginIds = withOptional ? rootDescriptor.getOptionalDependentPluginIds() : PluginId.EMPTY_ARRAY;
727       int capacity = dependentPluginIds.size() - (withOptional ? 0 : optionalDependentPluginIds.length) + incompatibleModuleIds.size();
728       if (capacity == 0) {
729         return implicitDep == null ? Collections.emptyList() : Collections.singletonList(implicitDep);
730       }
731
732       uniqueCheck.clear();
733
734       List<IdeaPluginDescriptorImpl> plugins = new ArrayList<>(capacity + (implicitDep == null ? 0 : 1));
735       if (implicitDep != null) {
736         if (rootDescriptor == implicitDep) {
737           getLogger().error("Plugin " + rootDescriptor + " depends on self");
738         }
739         else {
740           uniqueCheck.add(implicitDep);
741           plugins.add(implicitDep);
742         }
743       }
744
745       boolean excludeOptional = !withOptional && optionalDependentPluginIds.length > 0 && optionalDependentPluginIds.length != dependentPluginIds.size();
746
747       dependentPluginIds.addAll(incompatibleModuleIds);
748
749       loop:
750       for (PluginId dependentPluginId : dependentPluginIds) {
751         if (excludeOptional) {
752           for (PluginId id : optionalDependentPluginIds) {
753             if (id == dependentPluginId) {
754               continue loop;
755             }
756           }
757         }
758
759         // check for missing optional dependency
760         IdeaPluginDescriptorImpl dep = idToDescriptorMap.get(dependentPluginId);
761         // if 'dep' refers to a module we need to check the real plugin containing this module only if it's still enabled,
762         // otherwise the graph will be inconsistent
763         if (dep == null) {
764           continue;
765         }
766
767         // ultimate plugin it is combined plugin, where some included XML can define dependency on ultimate explicitly and for now not clear,
768         // can be such requirements removed or not
769         if (rootDescriptor == dep) {
770           if (rootDescriptor.getPluginId() != CORE_ID) {
771             getLogger().error("Plugin " + rootDescriptor + " depends on self");
772           }
773         }
774         else if (uniqueCheck.add(dep)) {
775           plugins.add(dep);
776         }
777       }
778       return plugins;
779     });
780   }
781
782   private static void checkPluginCycles(@NotNull List<IdeaPluginDescriptorImpl> descriptors,
783                                         @NotNull Map<PluginId, IdeaPluginDescriptorImpl> idToDescriptorMap,
784                                         @NotNull List<? super String> errors) {
785     CachingSemiGraph<IdeaPluginDescriptorImpl> graph = createPluginIdGraph(descriptors, idToDescriptorMap, true);
786     DFSTBuilder<IdeaPluginDescriptorImpl> builder = new DFSTBuilder<>(GraphGenerator.generate(graph));
787     if (builder.isAcyclic()) {
788       return;
789     }
790
791     StringBuilder cyclePresentation = new StringBuilder();
792     for (Collection<IdeaPluginDescriptorImpl> component : builder.getComponents()) {
793       if (component.size() < 2) {
794         continue;
795       }
796
797       if (cyclePresentation.length() > 0) {
798         cyclePresentation.append(", ");
799       }
800
801       String separator = " <-> ";
802       for (IdeaPluginDescriptor descriptor : component) {
803         descriptor.setEnabled(false);
804
805         cyclePresentation.append(descriptor.getPluginId());
806         cyclePresentation.append(separator);
807       }
808
809       cyclePresentation.setLength(cyclePresentation.length() - separator.length());
810     }
811
812     if (cyclePresentation.length() > 0) {
813       errors.add("Plugins should not have cyclic dependencies: " + cyclePresentation);
814     }
815   }
816
817   private static @Nullable IdeaPluginDescriptorImpl loadDescriptorFromDir(@NotNull Path file,
818                                                                           @NotNull String descriptorRelativePath,
819                                                                           @Nullable Path pluginPath,
820                                                                           @NotNull DescriptorLoadingContext context) {
821     Path descriptorFile = file.resolve(descriptorRelativePath);
822     try {
823       IdeaPluginDescriptorImpl descriptor = new IdeaPluginDescriptorImpl(pluginPath == null ? file : pluginPath, context.isBundled);
824       Element element = JDOMUtil.load(descriptorFile, context.parentContext.getXmlFactory());
825       descriptor.readExternal(element, descriptorFile.getParent(), context.pathResolver, context, descriptor);
826       return descriptor;
827     }
828     catch (NoSuchFileException e) {
829       return null;
830     }
831     catch (SerializationException | JDOMException | IOException e) {
832       if (context.isEssential) {
833         ExceptionUtil.rethrow(e);
834       }
835       context.parentContext.getLogger().warn("Cannot load " + descriptorFile, e);
836       prepareLoadingPluginsErrorMessage(Collections.singletonList("File '" + file.getFileName() + "' contains invalid plugin descriptor"));
837     }
838     catch (Throwable e) {
839       if (context.isEssential) {
840         ExceptionUtil.rethrow(e);
841       }
842       context.parentContext.getLogger().warn("Cannot load " + descriptorFile, e);
843     }
844     return null;
845   }
846
847   @Nullable
848   private static IdeaPluginDescriptorImpl loadDescriptorFromJar(@NotNull Path file,
849                                                                 @NotNull String fileName,
850                                                                 @NotNull PathBasedJdomXIncluder.PathResolver<?> pathResolver,
851                                                                 @NotNull DescriptorLoadingContext context,
852                                                                 @Nullable Path pluginPath) {
853     SafeJdomFactory factory = context.parentContext.getXmlFactory();
854     try {
855       Path metaInf = context.open(file).getPath("/META-INF");
856       Element element;
857       try {
858         element = JDOMUtil.load(metaInf.resolve(fileName), factory);
859       }
860       catch (NoSuchFileException ignore) {
861         return null;
862       }
863
864       IdeaPluginDescriptorImpl descriptor = new IdeaPluginDescriptorImpl(pluginPath == null ? file : pluginPath, context.isBundled);
865       if (descriptor.readExternal(element, metaInf, pathResolver, context, descriptor)) {
866         descriptor.jarFiles = Collections.singletonList(descriptor.getPluginPath());
867       }
868       return descriptor;
869     }
870     catch (SerializationException | InvalidDataException e) {
871       if (context.isEssential) {
872         ExceptionUtil.rethrow(e);
873       }
874       context.parentContext.getLogger().info("Cannot load " + file + "!/META-INF/" + fileName, e);
875       prepareLoadingPluginsErrorMessage(Collections.singletonList("File '" + file.getFileName() + "' contains invalid plugin descriptor"));
876     }
877     catch (Throwable e) {
878       if (context.isEssential) {
879         ExceptionUtil.rethrow(e);
880       }
881       context.parentContext.getLogger().info("Cannot load " + file + "!/META-INF/" + fileName, e);
882     }
883
884     return null;
885   }
886
887   /**
888    * @deprecated Use {@link PluginManager#loadDescriptor(Path, String)}
889    */
890   @Nullable
891   @Deprecated
892   public static IdeaPluginDescriptorImpl loadDescriptor(@NotNull File file, @NotNull String fileName) {
893     return PluginManager.loadDescriptor(file.toPath(), fileName, disabledPlugins(), false);
894   }
895
896   @ApiStatus.Internal
897   public static @Nullable IdeaPluginDescriptorImpl loadDescriptor(@NotNull Path file,
898                                                                    boolean isBundled,
899                                                                    @NotNull DescriptorListLoadingContext parentContext) {
900     try (DescriptorLoadingContext context = new DescriptorLoadingContext(parentContext, isBundled, /* isEssential = */ false,
901                                                                          PathBasedJdomXIncluder.DEFAULT_PATH_RESOLVER)) {
902       return loadDescriptorFromFileOrDir(file, PLUGIN_XML, context, Files.isDirectory(file));
903     }
904   }
905
906   @Nullable
907   static IdeaPluginDescriptorImpl loadDescriptorFromFileOrDir(@NotNull Path file, @NotNull String pathName, @NotNull DescriptorLoadingContext context, boolean isDirectory) {
908     if (isDirectory) {
909       return loadDescriptorFromDirAndNormalize(file, pathName, context);
910     }
911     else if (StringUtilRt.endsWithIgnoreCase(file.getFileName().toString(), ".jar")) {
912       return loadDescriptorFromJar(file, pathName, context.pathResolver, context, null);
913     }
914     else {
915       return null;
916     }
917   }
918
919   @Nullable
920   private static IdeaPluginDescriptorImpl loadDescriptorFromDirAndNormalize(@NotNull Path file,
921                                                                             @NotNull String pathName,
922                                                                             @NotNull DescriptorLoadingContext context) {
923     String descriptorRelativePath = META_INF + pathName;
924     IdeaPluginDescriptorImpl descriptor = loadDescriptorFromDir(file, descriptorRelativePath, null, context);
925     if (descriptor != null) {
926       return descriptor;
927     }
928
929     List<Path> files;
930     try (DirectoryStream<Path> s = Files.newDirectoryStream(file.resolve("lib"))) {
931       files = ContainerUtil.collect(s.iterator());
932     }
933     catch (IOException e) {
934       return null;
935     }
936
937     if (files.isEmpty()) {
938       return null;
939     }
940
941     putMoreLikelyPluginJarsFirst(file, files);
942
943     List<Path> pluginJarFiles = null;
944     List<Path> dirs = null;
945     for (Path childFile : files) {
946       if (Files.isDirectory(childFile)) {
947         if (dirs == null) {
948           dirs = new ArrayList<>();
949         }
950         dirs.add(childFile);
951       }
952       else {
953         String path = childFile.toString();
954         if (StringUtilRt.endsWithIgnoreCase(path, ".jar") || StringUtilRt.endsWithIgnoreCase(path, ".zip")) {
955           if (files.size() == 1) {
956             pluginJarFiles = Collections.singletonList(childFile);
957             break;
958           }
959           else {
960             if (pluginJarFiles == null) {
961               pluginJarFiles = new ArrayList<>();
962             }
963             pluginJarFiles.add(childFile);
964           }
965         }
966       }
967     }
968
969     if (pluginJarFiles != null) {
970       PluginXmlPathResolver pathResolver = new PluginXmlPathResolver(pluginJarFiles, context);
971       for (Path jarFile : pluginJarFiles) {
972         descriptor = loadDescriptorFromJar(jarFile, pathName, pathResolver, context, file);
973         if (descriptor != null) {
974           descriptor.jarFiles = pluginJarFiles;
975           return descriptor;
976         }
977       }
978     }
979
980     if (dirs == null) {
981       return null;
982     }
983
984     for (Path dir : dirs) {
985       IdeaPluginDescriptorImpl otherDescriptor = loadDescriptorFromDir(dir, descriptorRelativePath, file, context);
986       if (otherDescriptor != null) {
987         if (descriptor != null) {
988           context.parentContext.getLogger().info("Cannot load " + file + " because two or more plugin.xml's detected");
989           return null;
990         }
991         descriptor = otherDescriptor;
992       }
993     }
994
995     return descriptor;
996   }
997
998   /*
999    * Sort the files heuristically to load the plugin jar containing plugin descriptors without extra ZipFile accesses
1000    * File name preference:
1001    * a) last order for files with resources in name, like resources_en.jar
1002    * b) last order for files that have -digit suffix is the name e.g. completion-ranking.jar is before gson-2.8.0.jar or junit-m5.jar
1003    * c) jar with name close to plugin's directory name, e.g. kotlin-XXX.jar is before all-open-XXX.jar
1004    * d) shorter name, e.g. android.jar is before android-base-common.jar
1005    */
1006   private static void putMoreLikelyPluginJarsFirst(@NotNull Path pluginDir, @NotNull List<Path> filesInLibUnderPluginDir) {
1007     String pluginDirName = pluginDir.getFileName().toString();
1008
1009     filesInLibUnderPluginDir.sort((o1, o2) -> {
1010       String o2Name = o2.getFileName().toString();
1011       String o1Name = o1.getFileName().toString();
1012
1013       boolean o2StartsWithResources = o2Name.startsWith("resources");
1014       boolean o1StartsWithResources = o1Name.startsWith("resources");
1015       if (o2StartsWithResources != o1StartsWithResources) {
1016         return o2StartsWithResources ? -1 : 1;
1017       }
1018
1019       boolean o2IsVersioned = fileNameIsLikeVersionedLibraryName(o2Name);
1020       boolean o1IsVersioned = fileNameIsLikeVersionedLibraryName(o1Name);
1021       if (o2IsVersioned != o1IsVersioned) {
1022         return o2IsVersioned ? -1 : 1;
1023       }
1024
1025       boolean o2StartsWithNeededName = StringUtil.startsWithIgnoreCase(o2Name, pluginDirName);
1026       boolean o1StartsWithNeededName = StringUtil.startsWithIgnoreCase(o1Name, pluginDirName);
1027       if (o2StartsWithNeededName != o1StartsWithNeededName) {
1028         return o2StartsWithNeededName ? 1 : -1;
1029       }
1030
1031       return o1Name.length() - o2Name.length();
1032     });
1033   }
1034
1035   private static boolean fileNameIsLikeVersionedLibraryName(@NotNull String name) {
1036     int i = name.lastIndexOf('-');
1037     if (i == -1) return false;
1038     if (i + 1 < name.length()) {
1039       char c = name.charAt(i + 1);
1040       if (Character.isDigit(c)) return true;
1041       return (c == 'm' || c == 'M') && i + 2 < name.length() && Character.isDigit(name.charAt(i + 2));
1042     }
1043     return false;
1044   }
1045
1046   private static void loadDescriptorsFromDir(@NotNull Path dir,
1047                                              boolean isBundled,
1048                                              @NotNull DescriptorListLoadingContext context) throws ExecutionException, InterruptedException {
1049     List<Future<IdeaPluginDescriptorImpl>> tasks = new ArrayList<>();
1050     ExecutorService executorService = context.getExecutorService();
1051     try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dir)) {
1052       for (Path file : dirStream) {
1053         tasks.add(executorService.submit(() -> loadDescriptor(file, isBundled, context)));
1054       }
1055     }
1056     catch (IOException ignore) {
1057       return;
1058     }
1059
1060     for (Future<IdeaPluginDescriptorImpl> task : tasks) {
1061       IdeaPluginDescriptorImpl descriptor = task.get();
1062       if (descriptor != null) {
1063         context.result.add(descriptor, context, /* overrideUseIfCompatible = */ false);
1064       }
1065     }
1066   }
1067
1068   private static void prepareLoadingPluginsErrorMessage(@NotNull Map<PluginId, String> disabledIds,
1069                                                         @NotNull Set<PluginId> disabledRequiredIds,
1070                                                         @NotNull Map<PluginId, ? extends IdeaPluginDescriptor> idMap,
1071                                                         @NotNull List<String> errors) {
1072     List<String> actions = new ArrayList<>();
1073     if (!disabledIds.isEmpty()) {
1074       String text = "<br><a href=\"" + DISABLE + "\">Disable ";
1075       if (disabledIds.size() == 1) {
1076         PluginId id = disabledIds.keySet().iterator().next();
1077         text += idMap.containsKey(id) ? toPresentableName(idMap.get(id)) : toPresentableName(id.getIdString());
1078       }
1079       else {
1080         text += "not loaded plugins";
1081       }
1082       actions.add(text + "</a>");
1083       if (!disabledRequiredIds.isEmpty()) {
1084         String name = disabledRequiredIds.size() == 1
1085                       ? toPresentableName(idMap.get(disabledRequiredIds.iterator().next()))
1086                       : "all necessary plugins";
1087         actions.add("<a href=\"" + ENABLE + "\">Enable " + name + "</a>");
1088       }
1089       actions.add("<a href=\"" + EDIT + "\">Open plugin manager</a>");
1090     }
1091     prepareLoadingPluginsErrorMessage(errors, actions);
1092   }
1093
1094   @TestOnly
1095   public static @NotNull List<? extends IdeaPluginDescriptor> testLoadDescriptorsFromClassPath(@NotNull ClassLoader loader)
1096     throws ExecutionException, InterruptedException {
1097     Map<URL, String> urlsFromClassPath = new LinkedHashMap<>();
1098     collectPluginFilesInClassPath(loader, urlsFromClassPath);
1099     DescriptorListLoadingContext context = new DescriptorListLoadingContext(0, Collections.emptySet(), new PluginLoadingResult(Collections.emptyMap(), BuildNumber.currentVersion(), false));
1100     try (DescriptorLoadingContext loadingContext = new DescriptorLoadingContext(context, true, true, new ClassPathXmlPathResolver(loader))) {
1101       loadDescriptorsFromClassPath(urlsFromClassPath, loadingContext, null);
1102     }
1103
1104     context.result.finishLoading();
1105     return context.result.getEnabledPlugins();
1106   }
1107
1108   private static void loadDescriptorsFromClassPath(@NotNull Map<URL, String> urls,
1109                                                    @NotNull DescriptorLoadingContext context,
1110                                                    @Nullable URL platformPluginURL) throws ExecutionException, InterruptedException {
1111     if (urls.isEmpty()) {
1112       return;
1113     }
1114
1115     List<Future<IdeaPluginDescriptorImpl>> tasks = new ArrayList<>(urls.size());
1116     ExecutorService executorService = context.parentContext.getExecutorService();
1117     for (Map.Entry<URL, String> entry : urls.entrySet()) {
1118       URL url = entry.getKey();
1119       tasks.add(executorService.submit(() -> loadDescriptorFromResource(url, entry.getValue(), context.copy(url.equals(platformPluginURL)))));
1120     }
1121
1122     PluginLoadingResult result = context.parentContext.result;
1123     for (Future<IdeaPluginDescriptorImpl> task : tasks) {
1124       IdeaPluginDescriptorImpl descriptor = task.get();
1125       if (descriptor != null) {
1126         descriptor.setUseCoreClassLoader();
1127         result.add(descriptor, context.parentContext, /* overrideUseIfCompatible = */ false);
1128       }
1129     }
1130   }
1131
1132   @Nullable
1133   private static URL computePlatformPluginUrlAndCollectPluginUrls(@NotNull ClassLoader loader, @NotNull Map<URL, String> urls) {
1134     String platformPrefix = System.getProperty(PlatformUtils.PLATFORM_PREFIX_KEY);
1135     URL result = null;
1136     if (platformPrefix != null) {
1137       // should be the only plugin in lib (only for Ultimate and WebStorm for now)
1138       if ((platformPrefix.equals(PlatformUtils.IDEA_PREFIX) || platformPrefix.equals(PlatformUtils.WEB_PREFIX)) && !isRunningFromSources()) {
1139         urls.put(loader.getResource(PLUGIN_XML_PATH), PLUGIN_XML);
1140         return null;
1141       }
1142
1143       String fileName = platformPrefix + "Plugin.xml";
1144       URL resource = loader.getResource(META_INF + fileName);
1145       if (resource != null) {
1146         urls.put(resource, fileName);
1147         result = resource;
1148       }
1149     }
1150     collectPluginFilesInClassPath(loader, urls);
1151     return result;
1152   }
1153
1154   private static void collectPluginFilesInClassPath(@NotNull ClassLoader loader, @NotNull Map<URL, String> urls) {
1155     try {
1156       Enumeration<URL> enumeration = loader.getResources(PLUGIN_XML_PATH);
1157       while (enumeration.hasMoreElements()) {
1158         urls.put(enumeration.nextElement(), PLUGIN_XML);
1159       }
1160     }
1161     catch (IOException e) {
1162       getLogger().info(e);
1163     }
1164   }
1165
1166   @Nullable
1167   private static IdeaPluginDescriptorImpl loadDescriptorFromResource(@NotNull URL resource, @NotNull String pathName, @NotNull DescriptorLoadingContext loadingContext) {
1168     try {
1169       Path file;
1170       if (URLUtil.FILE_PROTOCOL.equals(resource.getProtocol())) {
1171         file = Paths.get(StringUtil.trimEnd(FileUtilRt.toSystemIndependentName(urlToFile(resource).toString()), pathName)).getParent();
1172         return loadDescriptorFromFileOrDir(file, pathName, loadingContext, Files.isDirectory(file));
1173       }
1174       else if (URLUtil.JAR_PROTOCOL.equals(resource.getProtocol())) {
1175         String path = resource.getFile();
1176         file = urlToFile(path.substring(0, path.indexOf(URLUtil.JAR_SEPARATOR)));
1177         return loadDescriptorFromJar(file, pathName, loadingContext.pathResolver, loadingContext, null);
1178       }
1179       else {
1180         return null;
1181       }
1182     }
1183     catch (Throwable e) {
1184       if (loadingContext.isEssential) {
1185         ExceptionUtil.rethrow(e);
1186       }
1187       loadingContext.parentContext.getLogger().info("Cannot load " + resource, e);
1188       return null;
1189     }
1190     finally {
1191       loadingContext.close();
1192     }
1193   }
1194
1195   // work around corrupted URLs produced by File.getURL()
1196   @NotNull
1197   private static Path urlToFile(@NotNull String url) throws URISyntaxException {
1198     try {
1199       return Paths.get(new URI(url));
1200     }
1201     catch (URISyntaxException e) {
1202       if (url.indexOf(' ') > 0) {
1203         return Paths.get(new URI(StringUtil.replace(url, " ", "%20")));
1204       }
1205       throw e;
1206     }
1207   }
1208
1209   // work around corrupted URLs produced by File.getURL()
1210   @NotNull
1211   private static Path urlToFile(URL url) throws URISyntaxException, MalformedURLException {
1212     try {
1213       return Paths.get(url.toURI());
1214     }
1215     catch (URISyntaxException e) {
1216       String str = url.toString();
1217       if (str.indexOf(' ') > 0) {
1218         return Paths.get(new URL(StringUtil.replace(str, " ", "%20")).toURI());
1219       }
1220       throw e;
1221     }
1222   }
1223
1224   private static void loadDescriptorsFromProperty(@NotNull PluginLoadingResult result,
1225                                                   @NotNull DescriptorListLoadingContext context) {
1226     String pathProperty = System.getProperty(PROPERTY_PLUGIN_PATH);
1227     if (pathProperty == null) {
1228       return;
1229     }
1230
1231     for (StringTokenizer t = new StringTokenizer(pathProperty, File.pathSeparator + ","); t.hasMoreTokens(); ) {
1232       String s = t.nextToken();
1233       IdeaPluginDescriptorImpl descriptor = loadDescriptor(Paths.get(s), false, context);
1234       if (descriptor != null) {
1235         // plugins added via property shouldn't be overridden to avoid plugin root detection issues when running external plugin tests
1236         result.add(descriptor, context, /* overrideUseIfCompatible = */ true);
1237       }
1238     }
1239   }
1240
1241   /**
1242    * Think twice before use and get approve from core team.
1243    *
1244    * Returns enabled plugins only.
1245    */
1246   @ApiStatus.Internal
1247   public static @NotNull List<? extends IdeaPluginDescriptor> loadUncachedDescriptors() {
1248     return loadDescriptors().result.getEnabledPlugins();
1249   }
1250
1251   public static synchronized void scheduleDescriptorLoading() {
1252     getOrScheduleLoading();
1253   }
1254
1255   private static synchronized @NotNull CompletableFuture<DescriptorListLoadingContext> getOrScheduleLoading() {
1256     CompletableFuture<DescriptorListLoadingContext> future = descriptorListFuture;
1257     if (future != null) {
1258       return future;
1259     }
1260
1261     future = CompletableFuture.supplyAsync(() -> {
1262       Activity activity = StartUpMeasurer.startActivity("plugin descriptor loading");
1263       DescriptorListLoadingContext context = loadDescriptors();
1264       activity.end();
1265       return context;
1266     }, AppExecutorUtil.getAppExecutorService());
1267     descriptorListFuture = future;
1268     return future;
1269   }
1270
1271   /**
1272    * Think twice before use and get approve from core team. Returns enabled plugins only.
1273    */
1274   @ApiStatus.Internal
1275   public static @NotNull List<IdeaPluginDescriptorImpl> getEnabledPluginRawList() {
1276     return getOrScheduleLoading().join().result.getEnabledPlugins();
1277   }
1278
1279   @ApiStatus.Internal
1280   public static @NotNull CompletionStage<List<IdeaPluginDescriptorImpl>> initPlugins(@NotNull ClassLoader coreClassLoader) {
1281     CompletableFuture<DescriptorListLoadingContext> future = descriptorListFuture;
1282     if (future == null) {
1283       future = CompletableFuture.completedFuture(null);
1284     }
1285     return future.thenApply(context -> {
1286       loadAndInitializePlugins(context, coreClassLoader);
1287       return ourLoadedPlugins;
1288     });
1289   }
1290
1291   @ApiStatus.Internal
1292   private static @NotNull DescriptorListLoadingContext loadDescriptors() {
1293     PluginLoadingResult result = new PluginLoadingResult(getBrokenPluginVersions(), getBuildNumber());
1294     Map<URL, String> urlsFromClassPath = new LinkedHashMap<>();
1295     ClassLoader classLoader = PluginManagerCore.class.getClassLoader();
1296     URL platformPluginURL = computePlatformPluginUrlAndCollectPluginUrls(classLoader, urlsFromClassPath);
1297     int flags = IS_PARALLEL;
1298     boolean isUnitTestMode = PluginManagerCore.isUnitTestMode;
1299     if (isUnitTestMode) {
1300       flags |= IGNORE_MISSING_INCLUDE;
1301     }
1302
1303     DescriptorListLoadingContext context = new DescriptorListLoadingContext(flags, disabledPlugins(), result);
1304     try {
1305       try (DescriptorLoadingContext loadingContext = new DescriptorLoadingContext(context, /* isBundled = */ true, /* isEssential, doesn't matter = */ true, new ClassPathXmlPathResolver(classLoader))) {
1306         loadDescriptorsFromClassPath(urlsFromClassPath, loadingContext, platformPluginURL);
1307       }
1308
1309       loadDescriptorsFromDir(Paths.get(PathManager.getPluginsPath()), /* isBundled = */ false, context);
1310
1311       if (!isUnitTestMode) {
1312         loadDescriptorsFromDir(Paths.get(PathManager.getPreInstalledPluginsPath()), /* isBundled = */ true, context);
1313       }
1314
1315       loadDescriptorsFromProperty(result, context);
1316
1317       if (isUnitTestMode && result.enabledPluginCount() <= 1) {
1318         // we're running in unit test mode, but the classpath doesn't contain any plugins; try to load bundled plugins anyway
1319         context.usePluginClassLoader = true;
1320         loadDescriptorsFromDir(Paths.get(PathManager.getPreInstalledPluginsPath()), /* isBundled = */ true, context);
1321       }
1322     }
1323     catch (InterruptedException | ExecutionException e) {
1324       throw new RuntimeException(e);
1325     }
1326     finally {
1327       context.close();
1328     }
1329
1330     context.result.finishLoading();
1331     return context;
1332   }
1333
1334   private static void mergeOptionalConfigs(@NotNull List<IdeaPluginDescriptorImpl> enabledPlugins,
1335                                            @NotNull Map<PluginId, IdeaPluginDescriptorImpl> idMap) {
1336     if (isRunningFromSources()) {
1337       // fix optional configs
1338       for (IdeaPluginDescriptorImpl descriptor : enabledPlugins) {
1339         if (!descriptor.isUseCoreClassLoader() || descriptor.optionalConfigs == null) {
1340           continue;
1341         }
1342
1343         descriptor.optionalConfigs.forEach((id, entries) -> {
1344           IdeaPluginDescriptorImpl dependent = idMap.get(id);
1345           if (dependent != null && !dependent.isUseCoreClassLoader()) {
1346             entries.clear();
1347           }
1348         });
1349       }
1350     }
1351
1352     for (IdeaPluginDescriptorImpl rootDescriptor : enabledPlugins) {
1353       mergeOptionalDescriptors(rootDescriptor, rootDescriptor, idMap);
1354     }
1355
1356     for (IdeaPluginDescriptorImpl descriptor : enabledPlugins) {
1357       descriptor.optionalConfigs = null;
1358     }
1359   }
1360
1361   private static void mergeOptionalDescriptors(@NotNull IdeaPluginDescriptorImpl mergedDescriptor,
1362                                                @NotNull IdeaPluginDescriptorImpl rootDescriptor,
1363                                                @NotNull Map<PluginId, IdeaPluginDescriptorImpl> idMap) {
1364     Map<PluginId, List<IdeaPluginDescriptorImpl>> optionalDescriptors = rootDescriptor.optionalConfigs;
1365     if (optionalDescriptors == null) {
1366       return;
1367     }
1368
1369     optionalDescriptors.forEach((dependencyId, entries) -> {
1370       IdeaPluginDescriptorImpl dependencyDescriptor = idMap.get(dependencyId);
1371       if (dependencyDescriptor == null || !dependencyDescriptor.isEnabled()) {
1372         return;
1373       }
1374
1375       loop:
1376       for (IdeaPluginDescriptorImpl descriptor : entries) {
1377         // check that plugin doesn't depend on unavailable plugin
1378         for (PluginId id : descriptor.getDependentPluginIds()) {
1379           IdeaPluginDescriptorImpl dependentDescriptor = idMap.get(id);
1380           // ignore if optional
1381           if ((dependentDescriptor == null || !dependentDescriptor.isEnabled()) && !isOptional(descriptor, id)) {
1382             continue loop;
1383           }
1384         }
1385
1386         mergedDescriptor.mergeOptionalConfig(descriptor);
1387         mergeOptionalDescriptors(mergedDescriptor, descriptor, idMap);
1388       }
1389     });
1390   }
1391
1392   private static boolean isOptional(@NotNull IdeaPluginDescriptorImpl descriptor, @NotNull PluginId id) {
1393     PluginId[] optional = descriptor.getOptionalDependentPluginIds();
1394     if (optional.length == 0) {
1395       // all are required, so, this one also required
1396       return false;
1397     }
1398     if (optional.length == descriptor.getDependentPluginIds().length) {
1399       // all are optional, so, this one also optional
1400       return true;
1401     }
1402
1403     for (PluginId otherId : optional) {
1404       if (id == otherId) {
1405         return true;
1406       }
1407     }
1408     return false;
1409   }
1410
1411   @ApiStatus.Internal
1412   public static void initClassLoader(@NotNull IdeaPluginDescriptorImpl rootDescriptor) {
1413     Map<PluginId, IdeaPluginDescriptorImpl> idMap = buildPluginIdMap(ContainerUtil.concat(getLoadedPlugins(null), Collections.singletonList(rootDescriptor)));
1414
1415     Set<ClassLoader> loaders = new LinkedHashSet<>();
1416     processAllDependencies(rootDescriptor, true, idMap, descriptor -> {
1417       ClassLoader loader = descriptor.getPluginClassLoader();
1418       if (loader == null) {
1419         getLogger().error(rootDescriptor.formatErrorMessage("requires missing class loader for " + toPresentableName(descriptor)));
1420       }
1421       else {
1422         loaders.add(loader);
1423       }
1424       // see configureClassLoaders about why we don't need to process recursively
1425       return FileVisitResult.SKIP_SUBTREE;
1426     });
1427
1428     IdeaPluginDescriptorImpl javaDep = idMap.get(JAVA_MODULE_ID);
1429     boolean hasAllModules = idMap.containsKey(ALL_MODULES_MARKER);
1430     IdeaPluginDescriptorImpl implicitDependency = getImplicitDependency(rootDescriptor, javaDep, hasAllModules);
1431     if (implicitDependency != null && implicitDependency.getPluginClassLoader() != null) {
1432       loaders.add(implicitDependency.getPluginClassLoader());
1433     }
1434
1435     ClassLoader[] array = loaders.isEmpty() ? new ClassLoader[]{PluginManagerCore.class.getClassLoader()} : loaders.toArray(new ClassLoader[0]);
1436     rootDescriptor.setLoader(createPluginClassLoader(array, rootDescriptor, createUrlClassLoaderBuilder()));
1437   }
1438
1439   @NotNull
1440   private static UrlClassLoader.Builder createUrlClassLoaderBuilder() {
1441     return UrlClassLoader.build().allowLock().useCache().urlsInterned();
1442   }
1443
1444   @NotNull
1445   static BuildNumber getBuildNumber() {
1446     BuildNumber result = ourBuildNumber;
1447     if (result == null) {
1448       result = BuildNumber.fromString(getPluginsCompatibleBuild());
1449       if (result == null) {
1450         if (isUnitTestMode) {
1451           result = BuildNumber.currentVersion();
1452         }
1453         else {
1454           try {
1455             result = ApplicationInfoImpl.getShadowInstance().getApiVersionAsNumber();
1456           }
1457           catch (RuntimeException ignore) {
1458             // no need to log error - ApplicationInfo is required in production in any case, so, will be logged if really needed
1459             result = BuildNumber.currentVersion();
1460           }
1461         }
1462       }
1463       ourBuildNumber = result;
1464     }
1465     return result;
1466   }
1467
1468   private static void disableIncompatiblePlugins(@NotNull List<IdeaPluginDescriptorImpl> descriptors,
1469                                                  @NotNull Map<PluginId, IdeaPluginDescriptorImpl> idMap,
1470                                                  @NotNull List<String> errors) {
1471     String selectedIds = System.getProperty("idea.load.plugins.id");
1472     String selectedCategory = System.getProperty("idea.load.plugins.category");
1473
1474     IdeaPluginDescriptorImpl coreDescriptor = idMap.get(CORE_ID);
1475     Set<IdeaPluginDescriptor> explicitlyEnabled = null;
1476     if (selectedIds != null) {
1477       Set<PluginId> set = new HashSet<>();
1478       List<String> strings = StringUtil.split(selectedIds, ",");
1479       for (String it : strings) {
1480         set.add(PluginId.getId(it));
1481       }
1482       set.addAll(((ApplicationInfoImpl)ApplicationInfoImpl.getShadowInstance()).getEssentialPluginsIds());
1483
1484       explicitlyEnabled = new LinkedHashSet<>(set.size());
1485       for (PluginId id : set) {
1486         IdeaPluginDescriptorImpl descriptor = idMap.get(id);
1487         if (descriptor != null) {
1488           explicitlyEnabled.add(descriptor);
1489         }
1490       }
1491     }
1492     else if (selectedCategory != null) {
1493       explicitlyEnabled = new LinkedHashSet<>();
1494       for (IdeaPluginDescriptor descriptor : descriptors) {
1495         if (selectedCategory.equals(descriptor.getCategory())) {
1496           explicitlyEnabled.add(descriptor);
1497         }
1498       }
1499     }
1500
1501     if (explicitlyEnabled != null) {
1502       // add all required dependencies
1503       Set<IdeaPluginDescriptor> finalExplicitlyEnabled = explicitlyEnabled;
1504       Set<IdeaPluginDescriptor> depProcessed = new HashSet<>();
1505       for (IdeaPluginDescriptor descriptor : new ArrayList<>(explicitlyEnabled)) {
1506         processAllDependencies(descriptor, false, idMap, depProcessed, dependency -> {
1507           finalExplicitlyEnabled.add(dependency);
1508           return FileVisitResult.CONTINUE;
1509         });
1510       }
1511     }
1512
1513     boolean shouldLoadPlugins = shouldLoadPlugins();
1514
1515     for (IdeaPluginDescriptorImpl descriptor : descriptors) {
1516       String errorSuffix;
1517       if (descriptor == coreDescriptor) {
1518         errorSuffix = null;
1519       }
1520       else if (explicitlyEnabled != null) {
1521         if (explicitlyEnabled.contains(descriptor)) {
1522           errorSuffix = null;
1523         }
1524         else {
1525           errorSuffix = "";
1526           getLogger().info("Plugin " + toPresentableName(descriptor) + " " +
1527                            (selectedIds != null
1528                             ? "is not in 'idea.load.plugins.id' system property"
1529                             : "category doesn't match 'idea.load.plugins.category' system property"));
1530         }
1531       }
1532       else if (!shouldLoadPlugins) {
1533         errorSuffix = "is skipped (plugins loading disabled)";
1534       }
1535       else {
1536         errorSuffix = null;
1537       }
1538
1539       if (errorSuffix != null) {
1540         descriptor.setEnabled(false);
1541         if (!errorSuffix.isEmpty()) {
1542           errors.add("Plugin " + toPresentableName(descriptor) + " " + errorSuffix);
1543         }
1544       }
1545     }
1546   }
1547
1548   public static boolean isCompatible(@NotNull IdeaPluginDescriptor descriptor) {
1549     return !isIncompatible(descriptor);
1550   }
1551
1552   public static boolean isCompatible(@NotNull IdeaPluginDescriptor descriptor, @Nullable BuildNumber buildNumber) {
1553     return !isIncompatible(descriptor, buildNumber);
1554   }
1555
1556   public static boolean isIncompatible(@NotNull IdeaPluginDescriptor descriptor) {
1557     return isIncompatible(descriptor, getBuildNumber());
1558   }
1559
1560   public static boolean isIncompatible(@NotNull IdeaPluginDescriptor descriptor, @Nullable BuildNumber buildNumber) {
1561     if (buildNumber == null) {
1562       buildNumber = getBuildNumber();
1563     }
1564     return isIncompatible(buildNumber, descriptor.getSinceBuild(), descriptor.getUntilBuild()) != null;
1565   }
1566
1567   static @Nullable String isIncompatible(@NotNull BuildNumber buildNumber, @Nullable String sinceBuild, @Nullable String untilBuild) {
1568     try {
1569       String message = null;
1570       BuildNumber sinceBuildNumber = sinceBuild == null ? null : BuildNumber.fromString(sinceBuild, null, null);
1571       if (sinceBuildNumber != null && sinceBuildNumber.compareTo(buildNumber) > 0) {
1572         message = "since build " + sinceBuildNumber + " > " + buildNumber;
1573       }
1574
1575       BuildNumber untilBuildNumber = untilBuild == null ? null : BuildNumber.fromString(untilBuild, null, null);
1576       if (untilBuildNumber != null && untilBuildNumber.compareTo(buildNumber) < 0) {
1577         if (message == null) {
1578           message = "";
1579         }
1580         else {
1581           message += ", ";
1582         }
1583         message += "until build " + untilBuildNumber + " < " + buildNumber;
1584       }
1585       return message;
1586     }
1587     catch (Exception e) {
1588       getLogger().error(e);
1589       return "version check failed";
1590     }
1591   }
1592
1593   private static void checkEssentialPluginsAreAvailable(@NotNull Map<PluginId, IdeaPluginDescriptorImpl> idMap) {
1594     List<PluginId> required = ((ApplicationInfoImpl)ApplicationInfoImpl.getShadowInstance()).getEssentialPluginsIds();
1595     List<String> missing = null;
1596     for (PluginId id : required) {
1597       IdeaPluginDescriptorImpl descriptor = idMap.get(id);
1598       if (descriptor == null || !descriptor.isEnabled()) {
1599         if (missing == null) {
1600           missing = new ArrayList<>();
1601         }
1602         missing.add(id.getIdString());
1603       }
1604     }
1605
1606     if (missing != null) {
1607       throw new EssentialPluginMissingException(missing);
1608     }
1609   }
1610
1611   static @NotNull PluginManagerState initializePlugins(@NotNull DescriptorListLoadingContext context, @NotNull ClassLoader coreLoader, boolean checkEssentialPlugins) {
1612     PluginLoadingResult loadingResult = context.result;
1613     List<String> errors = new ArrayList<>(loadingResult.getErrors());
1614
1615     if (loadingResult.duplicateModuleMap != null) {
1616       loadingResult.duplicateModuleMap.forEach((id, values) -> {
1617         errors.add("Module " + id + " is declared by plugins:\n  " + StringUtil.join(values, "\n  "));
1618       });
1619     }
1620
1621     Map<PluginId, IdeaPluginDescriptorImpl> idMap = loadingResult.idMap;
1622     IdeaPluginDescriptorImpl coreDescriptor = idMap.get(CORE_ID);
1623     if (checkEssentialPlugins && coreDescriptor == null) {
1624       throw new EssentialPluginMissingException(Collections.singletonList(CORE_ID + " (platform prefix: " + System.getProperty(PlatformUtils.PLATFORM_PREFIX_KEY) + ")"));
1625     }
1626
1627     List<IdeaPluginDescriptorImpl> descriptors = loadingResult.getEnabledPlugins();
1628     disableIncompatiblePlugins(descriptors, idMap, errors);
1629     checkPluginCycles(descriptors, idMap, errors);
1630
1631     // topological sort based on required dependencies only
1632     IdeaPluginDescriptorImpl[] sortedRequired = getTopologicallySorted(createPluginIdGraph(descriptors, idMap, false));
1633
1634     Set<PluginId> enabledPluginIds = new LinkedHashSet<>();
1635     Set<PluginId> enabledModuleIds = new LinkedHashSet<>();
1636     Map<PluginId, String> disabledIds = new LinkedHashMap<>();
1637     Set<PluginId> disabledRequiredIds = new LinkedHashSet<>();
1638
1639     for (IdeaPluginDescriptorImpl descriptor : sortedRequired) {
1640       boolean wasEnabled = descriptor.isEnabled();
1641       if (wasEnabled && computePluginEnabled(descriptor, enabledPluginIds, enabledModuleIds, idMap, disabledRequiredIds, context.disabledPlugins, errors)) {
1642         enabledPluginIds.add(descriptor.getPluginId());
1643         enabledModuleIds.addAll(descriptor.getModules());
1644       }
1645       else {
1646         descriptor.setEnabled(false);
1647         if (wasEnabled) {
1648           disabledIds.put(descriptor.getPluginId(), descriptor.getName());
1649         }
1650       }
1651     }
1652
1653     prepareLoadingPluginsErrorMessage(disabledIds, disabledRequiredIds, idMap, errors);
1654
1655     // topological sort based on all (required and optional) dependencies
1656     CachingSemiGraph<IdeaPluginDescriptorImpl> graph = createPluginIdGraph(Arrays.asList(sortedRequired), idMap, true);
1657     IdeaPluginDescriptorImpl[] sortedAll = getTopologicallySorted(graph);
1658
1659     List<IdeaPluginDescriptorImpl> enabledPlugins = getOnlyEnabledPlugins(sortedAll);
1660
1661     mergeOptionalConfigs(enabledPlugins, idMap);
1662     configureClassLoaders(coreLoader, graph, coreDescriptor, enabledPlugins, context.usePluginClassLoader);
1663
1664     if (checkEssentialPlugins) {
1665       checkEssentialPluginsAreAvailable(idMap);
1666     }
1667
1668     Set<PluginId> effectiveDisabledIds = disabledIds.isEmpty() ? Collections.emptySet() : new HashSet<>(disabledIds.keySet());
1669     return new PluginManagerState(sortedAll, enabledPlugins, disabledRequiredIds, effectiveDisabledIds, idMap);
1670   }
1671
1672   private static void configureClassLoaders(@NotNull ClassLoader coreLoader,
1673                                             @NotNull CachingSemiGraph<IdeaPluginDescriptorImpl> graph,
1674                                             @Nullable IdeaPluginDescriptor coreDescriptor,
1675                                             @NotNull List<IdeaPluginDescriptorImpl> enabledPlugins,
1676                                             boolean usePluginClassLoader) {
1677     ArrayList<ClassLoader> loaders = new ArrayList<>();
1678     ClassLoader[] emptyClassLoaderArray = new ClassLoader[0];
1679     UrlClassLoader.Builder urlClassLoaderBuilder = createUrlClassLoaderBuilder();
1680     for (IdeaPluginDescriptorImpl rootDescriptor : enabledPlugins) {
1681       if (rootDescriptor == coreDescriptor || rootDescriptor.isUseCoreClassLoader()) {
1682         rootDescriptor.setLoader(coreLoader);
1683         continue;
1684       }
1685
1686       if (!usePluginClassLoader) {
1687         rootDescriptor.setLoader(null);
1688         continue;
1689       }
1690
1691       loaders.clear();
1692
1693       // no need to process dependencies recursively because dependency will use own classloader
1694       // (that in turn will delegate class searching to parent class loader if needed)
1695       List<IdeaPluginDescriptorImpl> dependencies = graph.getInList(rootDescriptor);
1696       if (!dependencies.isEmpty()) {
1697         loaders.ensureCapacity(dependencies.size());
1698
1699         // do not add core loader - will be added to some dependency
1700         for (IdeaPluginDescriptorImpl descriptor : dependencies) {
1701           ClassLoader loader = descriptor.getPluginClassLoader();
1702           if (loader == null) {
1703             getLogger().error(rootDescriptor.formatErrorMessage("requires missing class loader for " + toPresentableName(descriptor)));
1704           }
1705           else {
1706             loaders.add(loader);
1707           }
1708         }
1709       }
1710
1711       ClassLoader[] parentLoaders = loaders.isEmpty() ? new ClassLoader[]{coreLoader} : loaders.toArray(emptyClassLoaderArray);
1712       rootDescriptor.setLoader(createPluginClassLoader(parentLoaders, rootDescriptor, urlClassLoaderBuilder));
1713     }
1714   }
1715
1716   private static @NotNull IdeaPluginDescriptorImpl @NotNull [] getTopologicallySorted(@NotNull CachingSemiGraph<IdeaPluginDescriptorImpl> graph) {
1717     DFSTBuilder<IdeaPluginDescriptorImpl> requiredOnlyGraph = new DFSTBuilder<>(GraphGenerator.generate(graph));
1718     IdeaPluginDescriptorImpl[] sortedRequired = graph.getNodes().toArray(IdeaPluginDescriptorImpl.EMPTY_ARRAY);
1719     Comparator<IdeaPluginDescriptorImpl> comparator = requiredOnlyGraph.comparator();
1720     // there is circular reference between core and implementation-detail plugin, as not all such plugins extracted from core,
1721     // so, ensure that core plugin is always first (otherwise not possible to register actions - parent group not defined)
1722     Arrays.sort(sortedRequired, (o1, o2) -> {
1723       if (o1.getPluginId() == CORE_ID) {
1724         return -1;
1725       }
1726       else if (o2.getPluginId() == CORE_ID) {
1727         return 1;
1728       }
1729       else {
1730         return comparator.compare(o1, o2);
1731       }
1732     });
1733     return sortedRequired;
1734   }
1735
1736   @NotNull
1737   @ApiStatus.Internal
1738   public static Map<PluginId, IdeaPluginDescriptorImpl> buildPluginIdMap(@NotNull List<IdeaPluginDescriptorImpl> descriptors) {
1739     Map<PluginId, IdeaPluginDescriptorImpl> idMap = new LinkedHashMap<>(descriptors.size());
1740     Map<PluginId, List<IdeaPluginDescriptorImpl>> duplicateMap = null;
1741     for (IdeaPluginDescriptorImpl descriptor : descriptors) {
1742       Map<PluginId, List<IdeaPluginDescriptorImpl>> newDuplicateMap = checkAndPut(descriptor, descriptor.getPluginId(), idMap, duplicateMap);
1743       if (newDuplicateMap != null) {
1744         duplicateMap = newDuplicateMap;
1745         continue;
1746       }
1747
1748       for (PluginId module : descriptor.getModules()) {
1749         newDuplicateMap = checkAndPut(descriptor, module, idMap, duplicateMap);
1750         if (newDuplicateMap != null) {
1751           duplicateMap = newDuplicateMap;
1752         }
1753       }
1754     }
1755     return idMap;
1756   }
1757
1758   @SuppressWarnings("DuplicatedCode")
1759   @Nullable
1760   private static Map<PluginId, List<IdeaPluginDescriptorImpl>> checkAndPut(@NotNull IdeaPluginDescriptorImpl descriptor,
1761                                                                            @NotNull PluginId id,
1762                                                                            @NotNull Map<PluginId, IdeaPluginDescriptorImpl> idMap,
1763                                                                            @Nullable Map<PluginId, List<IdeaPluginDescriptorImpl>> duplicateMap) {
1764     if (duplicateMap != null && duplicateMap.containsKey(id)) {
1765       ContainerUtilRt.putValue(id, descriptor, duplicateMap);
1766       return duplicateMap;
1767     }
1768
1769     IdeaPluginDescriptorImpl existingDescriptor = idMap.put(id, descriptor);
1770     if (existingDescriptor == null) {
1771       return null;
1772     }
1773
1774     // if duplicated, both are removed
1775     idMap.remove(id);
1776     if (duplicateMap == null) {
1777       duplicateMap = new LinkedHashMap<>();
1778     }
1779
1780     List<IdeaPluginDescriptorImpl> list = new ArrayList<>();
1781     list.add(existingDescriptor);
1782     list.add(descriptor);
1783     duplicateMap.put(id, list);
1784     return duplicateMap;
1785   }
1786
1787   private static boolean computePluginEnabled(@NotNull IdeaPluginDescriptorImpl descriptor,
1788                                               @NotNull Set<PluginId> loadedPluginIds,
1789                                               @NotNull Set<PluginId> loadedModuleIds,
1790                                               @NotNull Map<PluginId, IdeaPluginDescriptorImpl> idMap,
1791                                               @NotNull Set<? super PluginId> disabledRequiredIds,
1792                                               @NotNull Set<PluginId> disabledPlugins,
1793                                               @NotNull List<? super String> errors) {
1794     if (descriptor.getPluginId() == CORE_ID || descriptor.isImplementationDetail()) {
1795       return true;
1796     }
1797
1798     boolean result = true;
1799
1800     for (PluginId incompatibleId : descriptor.getIncompatibleModuleIds()) {
1801       if (!loadedModuleIds.contains(incompatibleId) || disabledPlugins.contains(incompatibleId)) continue;
1802
1803       result = false;
1804       String presentableName = toPresentableName(incompatibleId.getIdString());
1805       errors.add(descriptor.formatErrorMessage("is incompatible with the IDE containing module " + presentableName));
1806     }
1807
1808     // no deps at all or all are optional
1809     if (result && descriptor.getDependentPluginIds().length == descriptor.getOptionalDependentPluginIds().length) {
1810       return true;
1811     }
1812
1813     for (PluginId depId : descriptor.getDependentPluginIds()) {
1814       if (loadedPluginIds.contains(depId) || loadedModuleIds.contains(depId) || isOptional(descriptor, depId)) {
1815         continue;
1816       }
1817
1818       result = false;
1819       IdeaPluginDescriptor dep = idMap.get(depId);
1820       if (dep != null && disabledPlugins.contains(depId)) {
1821         // broken/incompatible plugins can be updated, add them anyway
1822         disabledRequiredIds.add(dep.getPluginId());
1823       }
1824
1825       String depName = dep == null ? null : dep.getName();
1826       if (depName == null) {
1827         errors.add(descriptor.formatErrorMessage("requires " + toPresentableName(depId.getIdString()) + " plugin to be installed"));
1828       }
1829       else {
1830         errors.add(descriptor.formatErrorMessage("requires " + toPresentableName(depName) + " plugin to be enabled"));
1831       }
1832     }
1833     return result;
1834   }
1835
1836   private static String toPresentableName(@Nullable IdeaPluginDescriptor descriptor) {
1837     return toPresentableName(descriptor == null ? null : descriptor.getName());
1838   }
1839
1840   @NotNull
1841   private static String toPresentableName(@Nullable String s) {
1842     return "\"" + (s == null ? "" : s) + "\"";
1843   }
1844
1845   /**
1846    * Load extensions points and extensions from a configuration file in plugin.xml format
1847    * <p>
1848    * Use it only for CoreApplicationEnvironment. Do not use otherwise. For IntelliJ Platform application and tests plugins are loaded in parallel
1849    * (including other optimizations).
1850    *
1851    * @param pluginRoot jar file or directory which contains the configuration file
1852    * @param fileName   name of the configuration file located in 'META-INF' directory under {@code pluginRoot}
1853    * @param area       area which extension points and extensions should be registered (e.g. {@link Extensions#getRootArea()} for application-level extensions)
1854    */
1855   public static void registerExtensionPointAndExtensions(@NotNull Path pluginRoot, @NotNull String fileName, @NotNull ExtensionsArea area) {
1856     IdeaPluginDescriptorImpl descriptor;
1857     DescriptorListLoadingContext parentContext = DescriptorListLoadingContext.createSingleDescriptorContext(disabledPlugins());
1858     try (DescriptorLoadingContext context = new DescriptorLoadingContext(parentContext, true, true, PathBasedJdomXIncluder.DEFAULT_PATH_RESOLVER)) {
1859       if (Files.isDirectory(pluginRoot)) {
1860         descriptor = loadDescriptorFromDir(pluginRoot, META_INF + fileName, null, context);
1861       }
1862       else {
1863         descriptor = loadDescriptorFromJar(pluginRoot, fileName, PathBasedJdomXIncluder.DEFAULT_PATH_RESOLVER, context, null);
1864       }
1865     }
1866
1867     if (descriptor != null) {
1868       descriptor.registerExtensionPoints((ExtensionsAreaImpl)area, ApplicationManager.getApplication());
1869       descriptor.registerExtensions((ExtensionsAreaImpl)area, ApplicationManager.getApplication(), false);
1870     }
1871     else {
1872       getLogger().error("Cannot load " + fileName + " from " + pluginRoot);
1873     }
1874   }
1875
1876   @SuppressWarnings("NonPrivateFieldAccessedInSynchronizedContext")
1877   private static synchronized void loadAndInitializePlugins(@Nullable DescriptorListLoadingContext context, @Nullable ClassLoader coreLoader) {
1878     if (coreLoader == null) {
1879       Class<?> callerClass = ReflectionUtil.findCallerClass(1);
1880       assert callerClass != null;
1881       coreLoader = callerClass.getClassLoader();
1882     }
1883
1884     try {
1885       if (context == null) {
1886         context = loadDescriptors();
1887       }
1888       Activity loadPluginsActivity = StartUpMeasurer.startActivity("plugin initialization");
1889       PluginManagerState initResult = initializePlugins(context, coreLoader, !isUnitTestMode);
1890
1891       ourPlugins = initResult.sortedPlugins;
1892       PluginLoadingResult result = context.result;
1893       if (!result.incompletePlugins.isEmpty()) {
1894         int oldSize = initResult.sortedPlugins.length;
1895         IdeaPluginDescriptorImpl[] all = new IdeaPluginDescriptorImpl[oldSize + result.incompletePlugins.size()];
1896         System.arraycopy(initResult.sortedPlugins, 0, all, 0, oldSize);
1897         ArrayUtil.copy(result.incompletePlugins.values(), all, oldSize);
1898         ourPlugins = all;
1899       }
1900
1901       ourPluginsToDisable = initResult.effectiveDisabledIds;
1902       ourPluginsToEnable = initResult.disabledRequiredIds;
1903       ourLoadedPlugins = initResult.sortedEnabledPlugins;
1904       ourShadowedBundledPlugins = result.getShadowedBundledIds();
1905
1906       loadPluginsActivity.end();
1907       loadPluginsActivity.setDescription("plugin count: " + ourLoadedPlugins.size());
1908       logPlugins(initResult.sortedPlugins);
1909     }
1910     catch (ExtensionInstantiationException e) {
1911       throw new PluginException(e, e.getExtensionOwnerId());
1912     }
1913     catch (RuntimeException e) {
1914       getLogger().error(e);
1915       throw e;
1916     }
1917   }
1918
1919   public static @NotNull Logger getLogger() {
1920     // do not use class reference here
1921     //noinspection SSBasedInspection
1922     return Logger.getInstance("#com.intellij.ide.plugins.PluginManager");
1923   }
1924
1925   public static final class EssentialPluginMissingException extends RuntimeException {
1926     public final List<String> pluginIds;
1927
1928     EssentialPluginMissingException(@NotNull List<String> ids) {
1929       super("Missing essential plugins: " + StringUtil.join(ids, ", "));
1930
1931       pluginIds = ids;
1932     }
1933   }
1934
1935   @Nullable
1936   public static IdeaPluginDescriptor getPlugin(@Nullable PluginId id) {
1937     if (id != null) {
1938       for (IdeaPluginDescriptor plugin : getPlugins()) {
1939         if (id == plugin.getPluginId()) {
1940           return plugin;
1941         }
1942       }
1943     }
1944     return null;
1945   }
1946
1947   @Nullable
1948   public static IdeaPluginDescriptor findPluginByModuleDependency(@NotNull PluginId id) {
1949     for (IdeaPluginDescriptor descriptor : getPlugins()) {
1950       if (descriptor instanceof IdeaPluginDescriptorImpl) {
1951         if (((IdeaPluginDescriptorImpl)descriptor).getModules().contains(id)) {
1952           return descriptor;
1953         }
1954       }
1955     }
1956     return null;
1957   }
1958
1959   public static boolean isPluginInstalled(PluginId id) {
1960     return getPlugin(id) != null;
1961   }
1962
1963   @NotNull
1964   @ApiStatus.Internal
1965   public static Map<PluginId, IdeaPluginDescriptorImpl> buildPluginIdMap() {
1966     LoadingState.COMPONENTS_REGISTERED.checkOccurred();
1967     return buildPluginIdMap(Arrays.asList(ourPlugins));
1968   }
1969
1970   /**
1971    * You must not use this method in cycle, in this case use {@link #processAllDependencies(IdeaPluginDescriptor, boolean, Map, Function)} instead
1972    * (to reuse result of {@link #buildPluginIdMap()}).
1973    *
1974    * {@link FileVisitResult#SKIP_SIBLINGS} is not supported.
1975    *
1976    * Returns {@code false} if processing was terminated because of {@link FileVisitResult#TERMINATE}, and {@code true} otherwise.
1977    */
1978   @SuppressWarnings("UnusedReturnValue")
1979   @ApiStatus.Internal
1980   public static boolean processAllDependencies(@NotNull IdeaPluginDescriptor rootDescriptor,
1981                                                boolean withOptionalDeps,
1982                                                @NotNull Function<IdeaPluginDescriptor, FileVisitResult> consumer) {
1983     return processAllDependencies(rootDescriptor, withOptionalDeps, buildPluginIdMap(), consumer);
1984   }
1985
1986   @ApiStatus.Internal
1987   public static boolean processAllDependencies(@NotNull IdeaPluginDescriptor rootDescriptor,
1988                                                boolean withOptionalDeps,
1989                                                @NotNull Map<PluginId, IdeaPluginDescriptorImpl> idToMap,
1990                                                @NotNull Function<IdeaPluginDescriptor, FileVisitResult> consumer) {
1991     return processAllDependencies(rootDescriptor, withOptionalDeps, idToMap, new HashSet<>(), consumer);
1992   }
1993
1994   @ApiStatus.Internal
1995   private static boolean processAllDependencies(@NotNull IdeaPluginDescriptor rootDescriptor,
1996                                                boolean withOptionalDeps,
1997                                                @NotNull Map<PluginId, IdeaPluginDescriptorImpl> idToMap,
1998                                                @NotNull Set<IdeaPluginDescriptor> depProcessed,
1999                                                @NotNull Function<IdeaPluginDescriptor, FileVisitResult> consumer) {
2000     loop:
2001     for (PluginId id : rootDescriptor.getDependentPluginIds()) {
2002       IdeaPluginDescriptorImpl descriptor = idToMap.get(id);
2003       if (descriptor == null) {
2004         continue;
2005       }
2006
2007       if (!withOptionalDeps) {
2008         for (PluginId otherId : rootDescriptor.getOptionalDependentPluginIds()) {
2009           if (otherId == id) {
2010             continue loop;
2011           }
2012         }
2013       }
2014
2015       switch (consumer.apply(descriptor)) {
2016         case TERMINATE:
2017           return false;
2018         case CONTINUE:
2019           if (depProcessed.add(descriptor)) {
2020             processAllDependencies(descriptor, withOptionalDeps, idToMap, depProcessed, consumer);
2021           }
2022           break;
2023         case SKIP_SUBTREE:
2024           break;
2025         case SKIP_SIBLINGS:
2026           throw new UnsupportedOperationException("FileVisitResult.SKIP_SIBLINGS is not supported");
2027       }
2028     }
2029
2030     return true;
2031   }
2032
2033   @NotNull
2034    private static List<IdeaPluginDescriptorImpl> getOnlyEnabledPlugins(IdeaPluginDescriptorImpl[] sortedAll) {
2035      List<IdeaPluginDescriptorImpl> enabledPlugins = new ArrayList<>(sortedAll.length);
2036      for (IdeaPluginDescriptorImpl descriptor : sortedAll) {
2037        if (descriptor.isEnabled()) {
2038          enabledPlugins.add(descriptor);
2039        }
2040      }
2041      return enabledPlugins;
2042    }
2043
2044   /**
2045    * @deprecated Use {@link PluginManager#addDisablePluginListener}
2046    */
2047   @Deprecated
2048   public static void addDisablePluginListener(@NotNull Runnable listener) {
2049     PluginManager.getInstance().addDisablePluginListener(listener);
2050   }
2051
2052   /**
2053    * @deprecated Use {@link PluginManager#addDisablePluginListener}
2054    */
2055   @Deprecated
2056   public static void removeDisablePluginListener(@NotNull Runnable listener) {
2057     PluginManager.getInstance().removeDisablePluginListener(listener);
2058   }
2059
2060   public static synchronized boolean isUpdatedBundledPlugin(@NotNull PluginDescriptor plugin) {
2061     return ourShadowedBundledPlugins != null && ourShadowedBundledPlugins.contains(plugin.getPluginId());
2062   }
2063 }