Plugin unload fails due to status bar widget

I have a status bar widget registered via com.intellij.openapi.wm.StatusBarWidgetFactory

but it seems to be the cause of a leak:

2026-02-13 09:07:24,878 [  18624]   INFO - #c.i.i.p.DynamicPlugins - Snapshot analysis result: Root 1:
  ROOT: Global JNI
  (root): com.intellij.openapi.wm.impl.IdeFrameImpl
  frameHelper: com.intellij.openapi.wm.impl.ProjectFrameHelper$1
  this$0: com.intellij.openapi.wm.impl.IdeProjectFrameHelper
  statusBar: com.intellij.openapi.wm.impl.status.IdeStatusBarImpl
  rightPanelLayout: java.awt.GridBagLayout
  componentAdjusting: com.intellij.openapi.wm.impl.status.IconPresentationComponent
  presentation: com.datadog.intellij.statusbar.StatusIcon
  <class>: java.lang.Class
* packageName: com.intellij.ide.plugins.cl.PluginClassLoader

I still see the icon in the status bar after the unload failure, does the platform not handle removal of widgets on unload?

Ah, I found the code for it. I think it may be that I was using the passed in scope in the com.intellij.openapi.wm.StatusBarWidgetFactory#createWidget(com.intellij.openapi.project.Project, kotlinx.coroutines.CoroutineScope) which is CoroutineName(com.intellij.openapi.wm.impl.status.widget.StatusBarWidgetsManager)

and not the plugin coroutine scope like I was expecting so some of the flows seem to be staying alive

Widget creation:

My code path hits line 253, and so it doesn’t create a child scope like WidgetPresentationWrapper does (which does cancel the ${factory.id}-widget scope on disposable of the wrapper)

so it looks like a StatusBarWidgetFactory that is not also a WidgetPresentationFactory can leak coroutine scopes on plugin unload

Hi Austin, nice catch! I created https://youtrack.jetbrains.com/issue/IJPL-234915/StatusBarWidgetFactorycreateWidget-coroutine-scope-should-be-plugin-scoped-otherwise-it-can-break-dynamic-unload . We will plan to fix it soon.

2 Likes

@dmitry.batkovich I saw this land, but shouldn’t it be this:

private fun createWidget(
  factory: StatusBarWidgetFactory,
  dataContext: WidgetPresentationDataContext,
  parentScope: CoroutineScope,
): StatusBarWidget {
  val widgetScope = parentScope.childScope("${factory.id}-widget")
  tryAttachWidgetScopeToPlugin(factory, widgetScope)

  if (factory !is WidgetPresentationFactory) {
    return factory.createWidget(dataContext.project, widgetScope)
  }

  return WidgetPresentationWrapper(id = factory.id, factory = factory, dataContext = dataContext, scope = widgetScope)
}

?

Though that still will leak on manual removal of a widget for the stable status bar API types, same as the status quo today, since removal of a widget doesn’t have access to the coroutine scope and the default behavior does not cancel the scope on dispose, which the WidgetPresentationWrapper does handle for the widget factory