Perform action on/after plugin update

Hi,

I’m introducing a new functionality in a plugin, and depending on the workflow I

  • either perform some automatic configuration when the plugin is first installed (via DynamincPluginListener#pluginLoaded),
  • and want to perform a similar configuration when the plugin is already installed, but it’s got updated, and no IDE restart is needed.

For the latter case DynamicPluginListener doesn’t seem to be triggered, and I haven’t been able to find any listener to be called right after a plugin update, that I could wire my logic into.

Is there any solution for this, or am I missing something?

Thanks!

As far as I understand, a plugin capable of dynamic load/unload, which is installed and then updated in the IDE would first be dynamically unloaded and then be loaded again. Does anything in the log indicate that the unload failed?

Or do you want to do somehing in the older plugin‘s code before the new update is loaded? That‘s not possible, I think.

For plugins that support dynamic loading (no restart needed), updates should actually trigger both unload and load events:

  1. Old version gets unloaded (beforePluginUnload)
  2. New version gets loaded (pluginLoaded)

So your existing DynamicPluginListener#pluginLoaded should already be catching updates, not just fresh installs.

Also, you can’t run code from the old plugin version before the update happens.

To troubleshoot, start with logs and go for breakpoints in mentioned methods.
Since pluginLoaded fires for both scenarios, you might want to track whether you’ve already done initial setup. You could use PropertiesComponent or persistent state to store a flag or version number.

Thanks for your inputs.

I don’t intend to run any code from the old plugin version, so that is not an issue.

After a bit more investigation the only thing I see in the logs is (although it doesn’t always occur):

INFO - #c.i.i.p.DynamicPlugins - Plugin com.intellij.wiremock is not unload-safe because class loader cannot be unloaded

(com.intellij.wiremock is the plugin I’m working on.)

Based on the SDK docs, I think this would explain why I need to restart the IDE upon updating the plugin, and why I don’t need to when it is a clean install.

However, this message doesn’t provide any context for the root cause, so I’ll see if I can find anything more specific.

Just a side-note: IntelliJ Plugin Verifier might provide some additional hints about non-dynamic plugins in the verification results.

I may have found the (or at least a) root cause.

I have a projectConfigurable whose getComponent() function launches a coroutine (via a CoroutineScope injected into a service) to get the user’s up-to-date authentication status, which makes a remote call, and updates a specific label on the UI when it’s finished.

SomeService.getInstance().cs.launch {
  //check auth state
}

It seems that the plugin unloading has an issue with this corotuine.

Wrapping the logic inside a readAction seems to mitigate the problem in some cases (though not sure why),

SomeService.getInstance().cs.launch {
  readAction {
    //check auth state
  }
}

but in others I receive

2025-10-10 10:58:54,060 [2344264]   INFO - #c.i.o.p.u.AbstractProgressIndicatorBase - This progress indicator (Unloading plugin WireMock 683510604: running=true; canceled=false) is indeterminate, this may lead to visual inconsistency. Please call setIndeterminate(false) before you start progress. class com.intellij.openapi.progress.util.PotemkinProgress

Is there a sophisticated way of performing a potentially longer running background operation inside Configurable#getComponent() (or during the initialization of the plugin settings page) that doesn’t affect the plugin unloading process?

Launching I/O in a ReadAction would not be correct, because it’s not needed and it could block WriteActions for a long time if the I/O takes a long time.

Is the remote call using coroutines? If it’s not, it may prevent the scope’s cancellation.
Although it sounds a bit odd that the launched coroutine would keep running until the end of the plugin’s lifecycle. Is the I/O never returning?

Does the unload work if you remove the call to launch?

Ideally, the launched operation should be cancelled when the configurable is disposed.
I’m usually (and hopefully correctly) using scopes in Configurables like this:

val dialogScope = SomeService.getInstance(project).cs.childScope("myConfigurable").cancelledWith(this) // this == configurable implementing Disposable

cancelledWith() is available at intellij-community/platform/collaboration-tools/src/com/intellij/collaboration/async/CoroutineUtil.kt at 3ebc4199e9ae7bc856692dc645b9818fb28c578c · JetBrains/intellij-community · GitHub

Thanks!

I haven’t had much time to investigate the issue deeper, but using childScope + cancelledWith seems to improve the situtation significantly.