Service management partially broken in recent IDE releases

I don’t know if you’re facing the same issues with IJ 2025.1.4.1, WebStorm 2025.1.4.1, etc.

I’m getting a service this way: `ApplicationManager.getApplication().getService(SettingsService.class)`.

Unfortunately, more and more of my customers are facing various critical issues saying (I can’t reproduce, for now):

  • Class initialization must not depend on services. Consider using instance of the service on-demand instead. Problem: I’m absolutely not on class init. I’m in IconPathPatcher:patchPath. intellij-community/platform/service-container/src/com/intellij/serviceContainer/ComponentManagerImpl.kt at a8c6da86ad21d6664738639795e636d855ce2e6b · JetBrains/intellij-community · GitHub (the checkOutsideClassInitializer method) seems buggy. I don’t know what do do
  • java.lang.ClassCastException: class lermitage.intellij.extraidetweaks.settings.SettingsIDEService cannot be cast to class lermitage.intellij.extraidetweaks.settings.SettingsIDEService (lermitage.intellij.extraidetweaks.settings.SettingsIDEService is in unnamed module of loader com.intellij.ide.plugins.cl.PluginClassLoader @3cd7f00f; lermitage.intellij.extraidetweaks.settings.SettingsIDEService is in unnamed module of loader com.intellij.ide.plugins.cl.PluginClassLoader @bba969). This issue makes me crazy. This happens when getting my service in a ModuleRootListener, or an AppLifecycleListener. I don’t know why.

This worked perfectly for years (at least 3 years), but the recent IDE releases broke this.

I opened an issue just in case (Youtrack issue). I’m wondering if you have the same problem.

This is severe problem in code you usually need to address. Could you share the stack trace?

java.lang.Throwable: krasa.grepconsole.tail.TailRunExecutor <clinit> 
requests lermitage.intellij.extratci.SettingsService instance. 
Class initialization must not depend on services. Consider using instance of the service on-demand instead.

Here the issue seems clear, right? You have a static final field probably in TailRunExecutor that want to access the service

@yuriy.artamonov No. I have no static field.

TailRunExecutor comes fro another plugin (not mine). My plugin registers an IconPathPatcher, which changes many icons, including an icon from the other plugin.

I get my service in the IconPathPatcher:patchPath method (previously, I did this in my constructor, but got the same error), but it raises the given stacktrace. I don’t know why it says I’m in an “init” block.

I explain this in https://youtrack.jetbrains.com/issue/IDEA-376370/AppLifecycleListeners-constructor-cant-get-service.

intellij-community/platform/service-container/src/com/intellij/serviceContainer/ComponentManagerImpl.kt at a8c6da86ad21d6664738639795e636d855ce2e6b · JetBrains/intellij-community · GitHub seems wrong. But it explains only some errors, not all of them.

You can get the stacktraces here:

It looks lioke you may not use services in this case. Icons can be used when application have not started yet from static contexts, for instance from AllIcons class; in these cases an exception during those classes init will ruin the class init and JVM will not try to init those again anymore.

Please avoid using services from stacks that originate in class loading activities. This is a real and not imaginary problem, that is why the assertion is present

Example:

java.lang.Throwable: icons.AwsIcons$Resources <clinit> requests lermitage.intellij.extratci.SettingsService instance. Class initialization must not depend on services. Consider using instance of the service on-demand instead.
	at com.intellij.openapi.diagnostic.Logger.error(Logger.java:375)
	at com.intellij.serviceContainer.ComponentManagerImplKt.checkOutsideClassInitializer(ComponentManagerImpl.kt:1585)
	at com.intellij.serviceContainer.ComponentManagerImplKt.getOrCreateInstanceBlocking(ComponentManagerImpl.kt:1554)
	at com.intellij.serviceContainer.ComponentManagerImpl.doGetService(ComponentManagerImpl.kt:752)
	at com.intellij.serviceContainer.ComponentManagerImpl.getService(ComponentManagerImpl.kt:696)
	at lermitage.intellij.extratci.SettingsService.getInstance(SourceFile:47)
	at lermitage.intellij.extratci.d.a(SourceFile:34)
	at lermitage.intellij.extratci.d.a(SourceFile:45)
	at lermitage.intellij.extratci.SettingsService.getAllIcons(SourceFile:51)
	at lermitage.intellij.extratci.d.a(SourceFile:161)
	at lermitage.intellij.extratci.d.patchPath(SourceFile:89)
	at com.intellij.ui.icons.IconTransform.applyPatchers(IconTransform.kt:91)
	at com.intellij.ui.icons.IconTransform.patchPath(IconTransform.kt:77)
	at com.intellij.ui.icons.CachedImageIconKt.patchIconPath(CachedImageIcon.kt:53)
	at com.intellij.openapi.util.IconLoaderKt.findIconUsingDeprecatedImplementation(IconLoader.kt:354)
	at com.intellij.openapi.util.IconLoaderKt.findIconUsingDeprecatedImplementation$default(IconLoader.kt:346)
	at com.intellij.openapi.util.IconLoader.getIcon(IconLoader.kt:113)
	at icons.AwsIcons.load(AwsIcons.kt:190)
	at icons.AwsIcons.access$load(AwsIcons.kt:14)
	at icons.AwsIcons$Resources.<clinit>(AwsIcons.kt:65)

Here your service initialized from class loading activity of AwsIcons class

I can only suggest some other SPI, your own or ServiceLoader of JVM, but yes accessing services from static context is unsafe and LOG.error there is important

No, again, I’m not accessing services from static context.

Per example:

package lermitage.intellij.extratci;

import com.intellij.ide.AppLifecycleListener;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.IconLoader;
import com.intellij.openapi.util.IconPathPatcher;
import lermitage.intellij.extratci.iconlists.BaseIcons;
import lermitage.intellij.extratci.iconlists.ClassicUIIcons;
import lermitage.intellij.extratci.iconlists.NewUIIcons;
import lermitage.intellij.extratci.iconlists.NewUIRemixIcons;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class ExtraToolWindowIconsPatcher extends IconPathPatcher implements AppLifecycleListener {

    private final Logger LOG = Logger.getInstance(getClass().getName());

    private Map<String, IconItem> configuredIcons;

    private final boolean IS_LOG_PATCH_PATH = "true".equals(System.getProperty("extra.tci.log.patchPath", "false"));

    @Override
    public void appFrameCreated(@NotNull List<String> commandLineArgs) {
        IconLoader.installPathPatcher(this);
    }

    private static @NotNull IconTheme getIconTheme() {
        return switch (SettingsService.getInstance().getUiTypeIconsPreference()) {
            case BASED_ON_ACTIVE_UI_TYPE -> UIUtils.isNewUIEnabled() ? IconTheme.NEW_UI : IconTheme.CLASSIC;
            case PREFER_NEW_UI_ICONS -> IconTheme.NEW_UI;
            case PREFER_OLD_UI_ICONS -> IconTheme.CLASSIC;
            case USE_NEW_UI_REMIX_ICONS -> IconTheme.NEW_UI_REMIX;

        };
    }

    @NotNull
    public static synchronized Map<String, IconItem> allIcons(boolean isFreemiumMode) {
        IconTheme iconTheme = getIconTheme();

        if (isFreemiumMode) {
            if (UIUtils.isNewUIEnabled()) {
                return Collections.emptyMap();
            } else {
                iconTheme = IconTheme.CLASSIC;
            }
        }

        Map<String, IconItem> icons = new SafeMap<>();

        icons.putAll(BaseIcons.getOrderedIcons());
        if (iconTheme == IconTheme.NEW_UI) {
            icons.putAll(NewUIIcons.getOrderedIcons());
        } else if (iconTheme == IconTheme.NEW_UI_REMIX) {
            icons.putAll(NewUIRemixIcons.getOrderedIcons());
        } else if (iconTheme == IconTheme.CLASSIC) {
            icons.putAll(ClassicUIIcons.getOrderedIcons());
        }

        return icons;
    }

    public ExtraToolWindowIconsPatcher() {
        super();
        //loadConfig();
    }

    @Override
    public @Nullable ClassLoader getContextClassLoader(@NotNull String path, @Nullable ClassLoader originalClassLoader) {
        return ExtraToolWindowIconsPatcher.class.getClassLoader();
    }

    @Override
    public @Nullable String patchPath(@NotNull String path, @Nullable ClassLoader classLoader) {
        if (IS_LOG_PATCH_PATH) {
            LOG.info("patchPath=" + path + " // " + classLoader);
        }
        //LOG.warn("patchPath=" + path);

        //if (true) return "extratci/icons/custom-expui/searchEverywhere.svg";

        if (configuredIcons == null) {
            loadConfig();
        }

        if (path.contains("@20x20")) {
            path = path.replace("@20x20", "");
        }

        for (String iconID : configuredIcons.keySet()/*.stream().sorted(Comparator.comparing(String::toLowerCase)).toList()*/) {
            IconItem iconItem = configuredIcons.get(iconID);
            String iconPathToReturn = iconItem.icon();
            Set<String> iconPluginIds = iconItem.pluginIds();

            // IMPORTANT icons coming from a plugin (like Junie):
            //  ignore custom icons that are explicitly associated with different plugins
            if (classLoader != null && iconPluginIds != null && !iconPluginIds.isEmpty()) {
                // classLoaderStr looks like:  PluginClassLoader(plugin=PluginDescriptor(name=Name of the plugin, id=id.of.the.plugin, ...
                String classLoaderStr = classLoader.toString();
                boolean isIconFromThirdPartyPlugin = iconPluginIds.stream().anyMatch(s -> classLoaderStr.contains("id=" + s));
                if (!isIconFromThirdPartyPlugin && !classLoaderStr.contains("id=lermitage")) {
                    // IMPORTANT also check id=lermitage because sometimes the 3rd-party icon is loaded
                    //  from my plugin (why?).
                    //  This may lead to a problem if two 3rd-party plugins define the same icon name.
                    //  I dont know what will happen. For now, this situation does not exist.
                    //LOG.warn("path:" + path + ", classLoaderStr:" + classLoaderStr);
                    continue;
                }
            }

            String iconPath = iconID;
            if (iconPath.contains("__alt")) {
                iconPath = iconPath.substring(0, iconPath.indexOf("__alt"));
            }

            if (iconPath.contains("__plugin")) {
                iconPath = iconPath.substring(0, iconPath.indexOf("__plugin"));
            }

            /*if (configuredIcons.containsKey(path)) {
                return configuredIcons.get(path).icon();
            }*/
            if (iconPath.equals(path)) {
                return iconPathToReturn;
            }

            /*if (path.startsWith("/") && path.length() > 2) {
                String simplifiedPath = path.substring(1);
                if (configuredIcons.containsKey(simplifiedPath)) {
                    return configuredIcons.get(simplifiedPath).icon();
                }
            }*/
            if (path.startsWith("/") && path.length() > 2) {
                String simplifiedPath = path.substring(1);
                if (iconPath.equals(simplifiedPath)) {
                    return iconPathToReturn;
                }
            }

            /*String fileName = (new File(path)).getName();
            if (configuredIcons.containsKey(fileName)) {
                return configuredIcons.get(fileName).icon();
            }*/
            String fileName = (new File(path)).getName();
            if (iconPath.equals(fileName)) {
                return iconPathToReturn;
            }
        }

        return null;
    }

    private void loadConfig() {
        Map<String, IconItem> enabledIcons;
        enabledIcons = SettingsService.getAllIcons();
        int allIconsSize = enabledIcons.size();
        List<String> disabledIcons = SettingsService.getInstance().getDisabledIcons();
        disabledIcons.forEach(enabledIcons::remove);
        configuredIcons = enabledIcons;
        LOG.info("config loaded with success, enabled " + configuredIcons.size() + "/" + allIconsSize + " items");
    }
}

This worked for years.

Now, SettingsService.getInstance() (from loadConfig, used in patchPath) generates an error.

You do, because AwsIcons does it in their stack. Exceptions in this stack may ruin classloading unrecoverably.

generates an error.

And it always was a problem even before that, because we barely could find the reasons of crashes reported to us.

comes from another plugin (not mine). I’m only in an IconPathPatcher.

I believe since icons used often from static context you must adapt your solution to that. I can only repeat - this is a real problem that needs to be addressed.

Also, I cannot reproduce these errors. Some of my customers are facing these errors.

If I was accessing my services from static context, these errors should be easily reproducible?

I believe since icons used often from static context you must adapt your solution to that. I can only repeat - this is a real problem that needs to be addressed.

I’m completely lost. What should I do? IconPathPatcher worked for years.

They are racy I believe because classloading is concurrent and depends on init sequence. You may try to ask for icons classes fields in some early init stages, e.g. ApplicationActivity

And still works, LOG.error does not throw, but logs and shows errors

I’m still lost.

Should I replace AppLifecycleListener (which installs my IconPathPatcher) by ApplicationActivity? This is internal.

Just in case, this is my (settings) service:

@State(
    name = "ExtraTciSettings",
    storages = @Storage(value = "lermitage-extratci.xml", roamingType = RoamingType.DEFAULT),
    category = SettingsCategory.PLUGINS
)
public class SettingsService implements PersistentStateComponent<SettingsService> {

    private static final @NonNls Logger LOGGER = Logger.getInstance(SettingsService.class);

    // the implementation of PersistentStateComponent works by serializing public fields, so keep them public
    @SuppressWarnings("WeakerAccess")
    public List<String> disabledIcons = new ArrayList<>();
    @SuppressWarnings("WeakerAccess")
    public UITypeIconsPreference uiTypeIconsPreference;
    @SuppressWarnings("WeakerAccess")
    public Boolean lifetimeLicHintNotifDisplayed2;

    public static SettingsService getInstance() {
        return ApplicationManager.getApplication().getService(SettingsService.class);
    }
...

Should I replace getInstance() by ApplicationManager.getApplication().getService(SettingsService.class) everywhere? Is it why the IDE thinks I’m in an init block?

Again, all these errors are very new. Something changed recently in the IDE…

My customers are also facing `java.lang.ClassCastException: class lermitage.intellij.extraidetweaks.settings.SettingsIDEService cannot be cast to class lermitage.intellij.extraidetweaks.settings.SettingsIDEService` errors, which, I guess, breaks my plugins. I can’t explain these errors (the package name is different because I have 4 plugins. They’re all affected by the same errors. Their settings service are very similar).

This error is very new, and it seems related to the PluginClassLoader. I do not touch class loaders…

Do you have optionally loaded modules though in the plugin? Usually such errors caused by the fact that a class of an optionally loaded module touched from the main plugin classloader directly. Such things lead to double classloading in two class loaders