What is the correct way to dipose from a corutine scope

Calling Disposer.dispose() from a coroutine scope can cause exceptions, I have seen it most from releaseEditor

java.lang.RuntimeException: CE must not be thrown from a dispose() implementation
	at com.intellij.openapi.util.ObjectTree.handleExceptions(ObjectTree.java:197)
	at com.intellij.openapi.util.ObjectTree.runWithTrace(ObjectTree.java:142)
	at com.intellij.openapi.util.ObjectTree.executeAll(ObjectTree.java:163)
	at com.intellij.openapi.util.Disposer.dispose(Disposer.java:211)
	at com.intellij.openapi.util.Disposer.dispose(Disposer.java:199)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:44)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:166)
	at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
	at com.datadog.intellij.errorTracking.toolWindow.ErrorTrackingToolWindow$1$1$1$1.invokeSuspend(ErrorTrackingToolWindow.kt:61)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:98)
	at com.intellij.openapi.application.TransactionGuardImpl$2.run(TransactionGuardImpl.java:224)
	at com.intellij.openapi.application.impl.NonBlockingFlushQueue.runNextEvent$lambda$4(NonBlockingFlushQueue.kt:358)
	at com.intellij.concurrency.ThreadContext.resetThreadContext(threadContext.kt:294)
	at com.intellij.openapi.application.impl.NonBlockingFlushQueue.runNextEvent(NonBlockingFlushQueue.kt:357)
	at com.intellij.openapi.application.impl.NonBlockingFlushQueue.flushNow(NonBlockingFlushQueue.kt:305)
	at com.intellij.openapi.application.impl.NonBlockingFlushQueue.FLUSH_NOW$lambda$0(NonBlockingFlushQueue.kt:167)
	at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318)
	at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:781)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:728)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:722)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:87)
	at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:750)
	at com.intellij.ide.IdeEventQueue.defaultDispatchEvent(IdeEventQueue.kt:664)
	at com.intellij.ide.IdeEventQueue._dispatchEvent(IdeEventQueue.kt:517)
	at com.intellij.ide.IdeEventQueue.dispatchEvent$lambda$0$0$0(IdeEventQueue.kt:333)
	at com.intellij.ide.IdeEventQueueKt.performActivity$lambda$1(IdeEventQueue.kt:1065)
	at com.intellij.openapi.application.TransactionGuardImpl.performActivity(TransactionGuardImpl.java:109)
	at com.intellij.ide.IdeEventQueueKt.performActivity(IdeEventQueue.kt:1065)
	at com.intellij.ide.IdeEventQueue.dispatchEvent$lambda$0(IdeEventQueue.kt:331)
	at com.intellij.ide.IdeEventQueue.dispatchEvent(IdeEventQueue.kt:371)
	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:207)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:128)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:117)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:113)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:105)
	at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:92)
Caused by: com.intellij.openapi.progress.ProcessCanceledException: com.intellij.platform.instanceContainer.internal.ContainerDisposedException: Container 'ProjectImpl@1343411026 services' was disposed
	at com.intellij.serviceContainer.ComponentManagerImpl.doGetService(ComponentManagerImpl.kt:686)
	at com.intellij.serviceContainer.ComponentManagerImpl.getService(ComponentManagerImpl.kt:653)
	at org.jetbrains.plugins.scala.settings.ScalaProjectSettings.getInstance(ScalaProjectSettings.java:193)
	at org.jetbrains.plugins.scala.settings.ScalaProjectSettings.in(ScalaProjectSettings.java:189)
	at org.jetbrains.plugins.scala.incremental.Highlighting$.enabledIn(Highlighting.scala:25)
	at org.jetbrains.plugins.scala.incremental.Listener.editorReleased(Listener.scala:97)
	at com.intellij.openapi.editor.impl.EditorFactoryImpl.releaseEditor$lambda$0(EditorFactoryImpl.kt:236)
	at com.intellij.openapi.editor.impl.EditorFactoryImpl.releaseEditor$lambda$1(EditorFactoryImpl.kt:236)
	at com.intellij.openapi.extensions.ExtensionPointName.forEachExtensionSafe(ExtensionPointName.kt:61)
	at com.intellij.openapi.editor.impl.EditorFactoryImpl.releaseEditor(EditorFactoryImpl.kt:236)
	at com.intellij.execution.impl.ConsoleViewImpl.disposeEditor$lambda$0$0(ConsoleViewImpl.kt:523)
	at com.intellij.openapi.application.impl.AppImplKt$rethrowCheckedExceptions$2.invoke(appImpl.kt:126)
	at com.intellij.platform.locking.impl.NestedLocksThreadingSupport.doRunWriteIntentReadAction(NestedLocksThreadingSupport.kt:736)
	at com.intellij.platform.locking.impl.NestedLocksThreadingSupport.runPreventiveWriteIntentReadAction(NestedLocksThreadingSupport.kt:710)
	at com.intellij.platform.locking.impl.NestedLocksThreadingSupport.runWriteIntentReadAction(NestedLocksThreadingSupport.kt:664)
	at com.intellij.openapi.application.impl.ApplicationImpl.runWriteIntentReadAction(ApplicationImpl.java:1185)
	at com.intellij.execution.impl.ConsoleViewImpl.disposeEditor$lambda$0(ConsoleViewImpl.kt:520)
	at com.intellij.util.ui.EdtInvocationManager.invokeAndWaitIfNeeded(EdtInvocationManager.java:74)
	at com.intellij.util.ui.UIUtil.invokeAndWaitIfNeeded(UIUtil.java:2036)
	at com.intellij.execution.impl.ConsoleViewImpl.disposeEditor(ConsoleViewImpl.kt:519)
	at com.intellij.execution.impl.ConsoleViewImpl.dispose(ConsoleViewImpl.kt:463)
	at com.intellij.openapi.util.ObjectTree.runWithTrace(ObjectTree.java:131)

What is the correct pattern to trigger a Dispose chain from within a coroutine scope? Do I need to do a invokeLater?

Could you share some code snippets and the task you are solving? You must switch to EDT at least to dispose editors.

CE must not be thrown from a dispose()

Potentially Scala plugin here is not right if they are touching getService() from dispose.

The full code is, I have tried a few things and this is the final version to try to work around the issue :confused:

 private val uiScope = cs.newChildScope(CoroutineName("ErrorTrackingToolWindow") + Dispatchers.EDT)

...

uiScope.launch {
            var uiCoroutine: Job? = null
            viewModelFlow.collectLatest { viewModel ->
                uiCoroutine?.cancelAndJoin()

                val modalityState = ModalityState.stateForComponent(this@ErrorTrackingToolWindow)
                uiCoroutine = uiScope.launch(modalityState.asContextElement()) {
                    supervisorScope {
                        val disposable = Disposer.newDisposable()
                        val mainComponent = viewModel.createComponent(this)
                        if (mainComponent is Disposable) {
                            Disposer.register(disposable, mainComponent)
                        }

                        setContent(
                            BorderLayoutPanel()
                                .addToTop(BannerService.getInstance(viewModel.project).createNotificationBanner())
                                .addToCenter(mainComponent)
                        )

                        try {
                            awaitCancellation()
                        } finally {
                            // Don't throw PCE, we are disposing, so it must finish
                            withContext(NonCancellable + Dispatchers.EDT + ModalityState.any().asContextElement()) {
                                setContent(null) // Avoids a green flash if the stack trace console is disposed while visible
                                Disposer.dispose(disposable)
                            }
                        }
                    }
                }
            }
        }

Basically a poor man’s Component.launchOnShow from the platform, but also allows a disposable attached to it.

I tried to clean it up with com.intellij.util.CoroutineScopeKt#asDisposable and ran into the same issue

Container ‘ProjectImpl@1343411026 services’ was disposed

Does it happen on project close?

BannerService

Can BannerService dispose its UI children instead of doing it from coroutine scope? It might be easier, then project will be disposed after it probably. Or other container which shows UI

Ya, I think so, it was reported through the marketplace exception system

No sure what you mean.

The main content is a Disposable, and the ConsoleView registers itself as a child as of it.

The line that triggers the exception is the Disposer.dispose(disposable) in the finally block

Here are some other examples of a releaseEditor failing:

Caused by: com.intellij.openapi.progress.ProcessCanceledException: com.intellij.platform.instanceContainer.internal.ContainerDisposedException: Container 'ProjectImpl@2038223021 services' was disposed
	at com.intellij.serviceContainer.ComponentManagerImpl.doGetService(ComponentManagerImpl.kt:724)
	at com.intellij.serviceContainer.ComponentManagerImpl.getService(ComponentManagerImpl.kt:691)
	at dev.nx.console.nxls.NxlsService$Companion.getInstance(NxlsService.kt:290)
	at dev.nx.console.listeners.NxEditorListener.editorReleased(NxEditorListener.kt:18)
	at com.intellij.openapi.editor.impl.EditorFactoryImpl.releaseEditor$lambda$0(EditorFactoryImpl.kt:239)
	at com.intellij.openapi.editor.impl.EditorFactoryImpl.releaseEditor$lambda$1(EditorFactoryImpl.kt:239)
Caused by: com.intellij.openapi.progress.ProcessCanceledException: com.intellij.platform.instanceContainer.internal.ContainerDisposedException: Container 'ProjectImpl@1619531508 services' was disposed
	at com.intellij.serviceContainer.ComponentManagerImpl.doGetService(ComponentManagerImpl.kt:686)
	at com.intellij.serviceContainer.ComponentManagerImpl.getService(ComponentManagerImpl.kt:653)
	at com.intellij.mermaid.fus.DiagramReportingFactoryListener.report(DiagramReportingFactoryListener.kt:87)
	at com.intellij.mermaid.fus.DiagramReportingFactoryListener.editorReleased(DiagramReportingFactoryListener.kt:39)
	at com.intellij.openapi.editor.impl.EditorFactoryImpl.releaseEditor$lambda$0(EditorFactoryImpl.kt:236)
	at com.intellij.openapi.editor.impl.EditorFactoryImpl.releaseEditor$lambda$1(EditorFactoryImpl.kt:236)
1 Like

I mean your UI is shown somewhere, in some component with setContent probably. It may dispose children and you don’t need to dispose them from scope additionally.

Just dispose old content on content switching, explicitly

Just dispose old content on content switching, explicitly

That was my intention, but I guess that can’t safely be done inside of a coroutine scope (due to bugs in other plugins)?

I think here coroutine scope is mostly unrelated and the issue would happen even without it. Depends on dispose order and we need to fix misbehaving code.

One may not touch project, services, message bus, other unrelated disposables in their dispose sequence.