[platform] javadoc: mention that methods which changes project model may be called...
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / updateSettings / impl / UpdateChecker.kt
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.openapi.updateSettings.impl
3
4 import com.intellij.ide.IdeBundle
5 import com.intellij.ide.externalComponents.ExternalComponentManager
6 import com.intellij.ide.plugins.*
7 import com.intellij.ide.util.PropertiesComponent
8 import com.intellij.notification.*
9 import com.intellij.notification.impl.NotificationsConfigurationImpl
10 import com.intellij.openapi.actionSystem.AnActionEvent
11 import com.intellij.openapi.actionSystem.PlatformDataKeys
12 import com.intellij.openapi.application.*
13 import com.intellij.openapi.application.ex.ApplicationInfoEx
14 import com.intellij.openapi.diagnostic.IdeaLoggingEvent
15 import com.intellij.openapi.diagnostic.LogUtil
16 import com.intellij.openapi.diagnostic.logger
17 import com.intellij.openapi.extensions.PluginId
18 import com.intellij.openapi.progress.EmptyProgressIndicator
19 import com.intellij.openapi.progress.ProgressIndicator
20 import com.intellij.openapi.progress.ProgressManager
21 import com.intellij.openapi.progress.Task
22 import com.intellij.openapi.project.Project
23 import com.intellij.openapi.ui.Messages
24 import com.intellij.openapi.util.ActionCallback
25 import com.intellij.openapi.util.BuildNumber
26 import com.intellij.openapi.util.JDOMUtil
27 import com.intellij.openapi.util.SystemInfo
28 import com.intellij.openapi.util.io.FileUtil
29 import com.intellij.openapi.util.text.StringUtil
30 import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeFrame
31 import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeFrameUpdater
32 import com.intellij.util.Url
33 import com.intellij.util.Urls
34 import com.intellij.util.containers.MultiMap
35 import com.intellij.util.io.HttpRequests
36 import com.intellij.util.io.URLUtil
37 import com.intellij.util.text.VersionComparatorUtil
38 import com.intellij.util.text.nullize
39 import com.intellij.util.ui.UIUtil
40 import com.intellij.xml.util.XmlStringUtil
41 import gnu.trove.THashMap
42 import org.jdom.JDOMException
43 import org.jetbrains.annotations.ApiStatus
44 import java.io.File
45 import java.io.IOException
46 import java.net.HttpURLConnection
47 import java.nio.file.Files
48 import java.nio.file.Paths
49 import java.util.*
50 import javax.swing.JComponent
51 import kotlin.collections.HashSet
52 import kotlin.collections.set
53
54 private val LOG = logger<UpdateChecker>()
55
56 private const val DISABLED_UPDATE = "disabled_update.txt"
57
58 private enum class NotificationUniqueType {
59   PLATFORM, PLUGINS, EXTERNAL
60 }
61
62 /**
63  * See XML file by [ApplicationInfoEx.getUpdateUrls] for reference.
64  */
65 object UpdateChecker {
66   private val notificationGroupRef by lazy {
67     NotificationGroup("IDE and Plugin Updates", NotificationDisplayType.STICKY_BALLOON, true, null, null, null, PluginManagerCore.CORE_ID)
68   }
69
70   @JvmField
71   @Deprecated(level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("getNotificationGroup()"), message = "Use getNotificationGroup()")
72   val NOTIFICATIONS = notificationGroupRef
73
74   @JvmStatic
75   fun getNotificationGroup() = notificationGroupRef
76
77   private var ourDisabledToUpdatePlugins: MutableSet<PluginId>? = null
78   private val ourAdditionalRequestOptions = THashMap<String, String>()
79   private val ourUpdatedPlugins = hashMapOf<PluginId, PluginDownloader>()
80   private val ourShownNotifications = MultiMap<NotificationUniqueType, Notification>()
81
82   /**
83    * Adding a plugin ID to this collection allows to exclude a plugin from a regular update check.
84    * Has no effect on non-bundled plugins.
85    */
86   @Suppress("MemberVisibilityCanBePrivate")
87   val excludedFromUpdateCheckPlugins: HashSet<String> = hashSetOf()
88
89   private val updateUrl: String
90     get() = System.getProperty("idea.updates.url") ?: ApplicationInfoEx.getInstanceEx().updateUrls!!.checkingUrl
91
92   /**
93    * For scheduled update checks.
94    */
95   @JvmStatic
96   fun updateAndShowResult(): ActionCallback {
97     val callback = ActionCallback()
98     ApplicationManager.getApplication().executeOnPooledThread {
99       doUpdateAndShowResult(null, true, false, false, UpdateSettings.getInstance(), null, callback)
100     }
101     return callback
102   }
103
104   /**
105    * For manual update checks (Help | Check for Updates, Settings | Updates | Check Now)
106    * (the latter action may pass customized update settings).
107    */
108   @JvmStatic
109   fun updateAndShowResult(project: Project?, customSettings: UpdateSettings?) {
110     val settings = customSettings ?: UpdateSettings.getInstance()
111     val fromSettings = customSettings != null
112
113     ProgressManager.getInstance().run(object : Task.Backgroundable(project, IdeBundle.message("updates.checking.progress"), true) {
114       override fun run(indicator: ProgressIndicator) = doUpdateAndShowResult(getProject(), !fromSettings,
115                                                                              fromSettings || WelcomeFrame.getInstance() != null, true,
116                                                                              settings, indicator, null)
117       override fun isConditionalModal(): Boolean = fromSettings
118       override fun shouldStartInBackground(): Boolean = !fromSettings
119     })
120   }
121
122   /**
123    * An immediate check for plugin updates for use from a command line (read "Toolbox").
124    */
125   @JvmStatic
126   fun getPluginUpdates(): Collection<PluginDownloader>? =
127     checkPluginsUpdate(EmptyProgressIndicator(), null, ApplicationInfo.getInstance().build)
128
129   private fun doUpdateAndShowResult(project: Project?,
130                                     showSettingsLink: Boolean,
131                                     showDialog: Boolean,
132                                     showEmptyNotification: Boolean,
133                                     updateSettings: UpdateSettings,
134                                     indicator: ProgressIndicator?,
135                                     callback: ActionCallback?) {
136     // check platform update
137
138     indicator?.text = IdeBundle.message("updates.checking.platform")
139
140     val result = checkPlatformUpdate(updateSettings)
141     if (result.state == UpdateStrategy.State.CONNECTION_ERROR) {
142       val e = result.error
143       if (e != null) LOG.debug(e)
144       showErrorMessage(showDialog, IdeBundle.message("updates.error.connection.failed", e?.message ?: "internal error"))
145       callback?.setRejected()
146       return
147     }
148
149     // check plugins update (with regard to potential platform update)
150
151     indicator?.text = IdeBundle.message("updates.checking.plugins")
152
153     val buildNumber: BuildNumber? = result.newBuild?.apiVersion
154     val incompatiblePlugins: MutableCollection<IdeaPluginDescriptor>? = if (buildNumber != null) HashSet() else null
155
156     val updatedPlugins: Collection<PluginDownloader>?
157     val externalUpdates: Collection<ExternalUpdate>?
158     try {
159       updatedPlugins = checkPluginsUpdate(indicator, incompatiblePlugins,
160                                           buildNumber)?.filter { downloader -> !PluginUpdateDialog.isIgnored(downloader.descriptor) }
161       externalUpdates = checkExternalUpdates(showDialog, updateSettings, indicator)
162     }
163     catch (e: IOException) {
164       showErrorMessage(showDialog, IdeBundle.message("updates.error.connection.failed", e.message))
165       callback?.setRejected()
166       return
167     }
168
169     // show result
170
171     UpdateSettings.getInstance().saveLastCheckedInfo()
172
173     ApplicationManager.getApplication().invokeLater {
174       showUpdateResult(project, result, updatedPlugins, incompatiblePlugins, externalUpdates, showSettingsLink, showDialog, showEmptyNotification)
175       callback?.setDone()
176     }
177   }
178
179   private fun checkPlatformUpdate(settings: UpdateSettings): CheckForUpdateResult {
180     val updateInfo = try {
181       var updateUrl = Urls.newFromEncoded(updateUrl)
182       if (updateUrl.scheme != URLUtil.FILE_PROTOCOL) {
183         updateUrl = prepareUpdateCheckArgs(updateUrl)
184       }
185       LogUtil.debug(LOG, "load update xml (UPDATE_URL='%s')", updateUrl)
186       HttpRequests.request(updateUrl).connect { UpdatesInfo(JDOMUtil.load(it.reader)) }
187     }
188     catch (e: JDOMException) {
189       // corrupted content, don't bother telling user
190       LOG.info(e)
191       null
192     }
193     catch (e: Exception) {
194       LOG.info(e)
195       return CheckForUpdateResult(UpdateStrategy.State.CONNECTION_ERROR, e)
196     }
197
198     if (updateInfo == null || !settings.isPlatformUpdateEnabled) {
199       return CheckForUpdateResult(UpdateStrategy.State.NOTHING_LOADED, null)
200     }
201
202     val strategy = UpdateStrategy(ApplicationInfo.getInstance().build, updateInfo, settings)
203     return strategy.checkForUpdates()
204   }
205
206   @JvmStatic
207   @Throws(IOException::class, JDOMException::class)
208   fun getUpdatesInfo(): UpdatesInfo? {
209     val updateUrl = Urls.newFromEncoded(updateUrl)
210     return HttpRequests.request(updateUrl).connect { UpdatesInfo(JDOMUtil.load(it.reader)) }
211   }
212
213   /**
214    * If [buildNumber] is null, returns new versions of plugins compatible with the current IDE version. If not null, returns
215    * new versions of plugins compatible with the specified build.
216    */
217   private fun checkPluginsUpdate(indicator: ProgressIndicator?,
218                                  incompatiblePlugins: MutableCollection<IdeaPluginDescriptor>?,
219                                  buildNumber: BuildNumber?): Collection<PluginDownloader>? {
220     val updateable = collectUpdateablePlugins()
221     if (updateable.isEmpty()) return null
222
223     val toUpdate = mutableMapOf<PluginId, PluginDownloader>()
224
225     val state = InstalledPluginsState.getInstance()
226     for (host in RepositoryHelper.getPluginHosts()) {
227       try {
228         if (host == null && ApplicationInfoEx.getInstanceEx().usesJetBrainsPluginRepository()) {
229           validateCompatibleUpdatesForCurrentPlugins(updateable, toUpdate, buildNumber, state, incompatiblePlugins, indicator)
230         }
231         else {
232           val list = RepositoryHelper.loadPlugins(host, buildNumber, indicator)
233           for (descriptor in list) {
234             val id = descriptor.pluginId
235             if (updateable.containsKey(id)) {
236               updateable.remove(id)
237               buildDownloaderAndPrepareToInstall(state, descriptor, buildNumber, toUpdate, incompatiblePlugins, indicator, host)
238             }
239           }
240         }
241       }
242       catch (e: IOException) {
243         LOG.debug(e)
244         LOG.info("failed to load plugin descriptions from ${host ?: "default repository"}: ${e.message}")
245       }
246     }
247
248     if (incompatiblePlugins != null && buildNumber != null) {
249       updateable.values.filterNotNull().filterTo(incompatiblePlugins) {
250         it.isEnabled && !PluginManagerCore.isCompatible(it, buildNumber)
251       }
252     }
253
254     return if (toUpdate.isEmpty()) null else toUpdate.values
255   }
256
257   /**
258    * Use Plugin Repository API for checking and loading compatible updates for updateable plugins.
259    * If current plugin version is out of date, schedule downloading of a newer version.
260    */
261   private fun validateCompatibleUpdatesForCurrentPlugins(
262     updateable: MutableMap<PluginId, IdeaPluginDescriptor?>,
263     toUpdate: MutableMap<PluginId, PluginDownloader>,
264     buildNumber: BuildNumber?,
265     state: InstalledPluginsState,
266     incompatiblePlugins: MutableCollection<IdeaPluginDescriptor>?,
267     indicator: ProgressIndicator?
268   ) {
269     val marketplacePluginIds = PluginsMetaLoader.getMarketplacePlugins(indicator)
270     val idsToUpdate = updateable.map { it.key.idString }.filter { it in marketplacePluginIds }
271     val updates = PluginsMetaLoader.getLastCompatiblePluginUpdate(idsToUpdate, buildNumber)
272     for ((id, descriptor) in updateable) {
273       val lastUpdate = updates.find { it.pluginId == id.idString } ?: continue
274       val isOutdated = descriptor == null || VersionComparatorUtil.compare(lastUpdate.version, descriptor.version) > 0
275       if (isOutdated) {
276         val newDescriptor = try {
277           PluginsMetaLoader.loadPluginDescriptor(id.idString, lastUpdate, indicator)
278         }
279         catch (e: HttpRequests.HttpStatusException) {
280           if (e.statusCode == HttpURLConnection.HTTP_NOT_FOUND) continue
281           else throw e
282         }
283         buildDownloaderAndPrepareToInstall(state, newDescriptor, buildNumber, toUpdate, incompatiblePlugins, indicator, null)
284       }
285     }
286     toUpdate.keys.forEach { updateable.remove(it) }
287   }
288
289   private fun buildDownloaderAndPrepareToInstall(
290     state: InstalledPluginsState,
291     descriptor: IdeaPluginDescriptor,
292     buildNumber: BuildNumber?,
293     toUpdate: MutableMap<PluginId, PluginDownloader>,
294     incompatiblePlugins: MutableCollection<IdeaPluginDescriptor>?,
295     indicator: ProgressIndicator?,
296     host: String?
297   ) {
298     val downloader = PluginDownloader.createDownloader(descriptor, host, buildNumber)
299     state.onDescriptorDownload(descriptor)
300     checkAndPrepareToInstall(downloader, state, toUpdate, incompatiblePlugins, indicator)
301   }
302
303   /**
304    * Returns a list of plugins that are currently installed or were installed in the previous installation from which
305    * we're importing the settings. Null values are for once-installed plugins.
306    */
307   private fun collectUpdateablePlugins(): MutableMap<PluginId, IdeaPluginDescriptor?> {
308     val updateable = mutableMapOf<PluginId, IdeaPluginDescriptor?>()
309
310     updateable += PluginManagerCore.getPlugins().filter { !it.isBundled || it.allowBundledUpdate() }.associateBy { it.pluginId }
311
312     val onceInstalled = PluginManager.getOnceInstalledIfExists()
313     if (onceInstalled != null) {
314       try {
315         Files.readAllLines(onceInstalled)
316           .asSequence()
317           .map { line -> PluginId.getId(line.trim { it <= ' ' }) }
318           .filter { it !in updateable }
319           .forEach { updateable[it] = null }
320       }
321       catch (e: IOException) {
322         LOG.error(onceInstalled.toString(), e)
323       }
324
325       //noinspection SSBasedInspection
326       onceInstalled.toFile().deleteOnExit()
327     }
328
329     if (!ApplicationManager.getApplication().isInternal && excludedFromUpdateCheckPlugins.isNotEmpty()) {
330       excludedFromUpdateCheckPlugins.forEach {
331         val excluded = PluginId.getId(it)
332         val plugin = updateable[excluded]
333         if (plugin != null && plugin.isBundled) {
334           updateable.remove(excluded)
335         }
336       }
337     }
338
339     return updateable
340   }
341
342   private fun checkExternalUpdates(manualCheck: Boolean,
343                                    updateSettings: UpdateSettings,
344                                    indicator: ProgressIndicator?): Collection<ExternalUpdate> {
345     val result = arrayListOf<ExternalUpdate>()
346     val manager = ExternalComponentManager.getInstance()
347     indicator?.text = IdeBundle.message("updates.external.progress")
348
349     for (source in manager.componentSources) {
350       indicator?.checkCanceled()
351       if (source.name in updateSettings.enabledExternalUpdateSources) {
352         try {
353           val siteResult = source.getAvailableVersions(indicator, updateSettings)
354             .filter { it.isUpdateFor(manager.findExistingComponentMatching(it, source)) }
355           if (siteResult.isNotEmpty()) {
356             result += ExternalUpdate(siteResult, source)
357           }
358         }
359         catch (e: Exception) {
360           LOG.warn(e)
361           showErrorMessage(manualCheck, IdeBundle.message("updates.external.error.message", source.name, e.message ?: "internal error"))
362         }
363       }
364     }
365
366     return result
367   }
368
369   @Throws(IOException::class)
370   @JvmStatic
371   fun checkAndPrepareToInstall(
372     downloader: PluginDownloader,
373     state: InstalledPluginsState,
374     toUpdate: MutableMap<PluginId, PluginDownloader>,
375     incompatiblePlugins: MutableCollection<IdeaPluginDescriptor>?,
376     indicator: ProgressIndicator?
377   ) {
378     @Suppress("NAME_SHADOWING")
379     var downloader = downloader
380     val pluginId = downloader.id
381     if (PluginManagerCore.isDisabled(pluginId)) return
382
383     val pluginVersion = downloader.pluginVersion
384     val installedPlugin = PluginManagerCore.getPlugin(pluginId)
385     if (installedPlugin == null || pluginVersion == null || PluginDownloader.compareVersionsSkipBrokenAndIncompatible(installedPlugin,
386                                                                                                                       pluginVersion) > 0) {
387       var descriptor: IdeaPluginDescriptor?
388
389       val oldDownloader = ourUpdatedPlugins[pluginId]
390       if (oldDownloader == null || StringUtil.compareVersionNumbers(pluginVersion, oldDownloader.pluginVersion) > 0) {
391         descriptor = downloader.descriptor
392         if (descriptor is PluginNode && descriptor.isIncomplete) {
393           if (downloader.prepareToInstall(indicator ?: EmptyProgressIndicator())) {
394             descriptor = downloader.descriptor
395           }
396           ourUpdatedPlugins[pluginId] = downloader
397         }
398       }
399       else {
400         downloader = oldDownloader
401         descriptor = oldDownloader.descriptor
402       }
403
404       if (PluginManagerCore.isCompatible(descriptor, downloader.buildNumber) && !state.wasUpdated(descriptor.pluginId)) {
405         toUpdate[pluginId] = downloader
406       }
407     }
408
409     // collect plugins that were not updated and would be incompatible with the new version
410     if (incompatiblePlugins != null && installedPlugin != null && installedPlugin.isEnabled &&
411         !toUpdate.containsKey(installedPlugin.pluginId) &&
412         !PluginManagerCore.isCompatible(installedPlugin, downloader.buildNumber)) {
413       incompatiblePlugins += installedPlugin
414     }
415   }
416
417   private fun showErrorMessage(showDialog: Boolean, message: String) {
418     LOG.info(message)
419     if (showDialog) {
420       UIUtil.invokeLaterIfNeeded { Messages.showErrorDialog(message, IdeBundle.message("updates.error.connection.title")) }
421     }
422   }
423
424   private fun showUpdateResult(project: Project?,
425                                checkForUpdateResult: CheckForUpdateResult,
426                                updatedPlugins: Collection<PluginDownloader>?,
427                                incompatiblePlugins: Collection<IdeaPluginDescriptor>?,
428                                externalUpdates: Collection<ExternalUpdate>?,
429                                showSettingsLink: Boolean,
430                                showDialog: Boolean,
431                                showEmptyNotification: Boolean) {
432     val updatedChannel = checkForUpdateResult.updatedChannel
433     val newBuild = checkForUpdateResult.newBuild
434
435     if (updatedChannel != null && newBuild != null) {
436       val runnable = {
437         UpdateInfoDialog(updatedChannel, newBuild, checkForUpdateResult.patches, showSettingsLink, updatedPlugins,
438                          incompatiblePlugins).show()
439       }
440
441       ourShownNotifications.remove(NotificationUniqueType.PLATFORM)?.forEach { it.expire() }
442
443       if (showDialog) {
444         runnable.invoke()
445       }
446       else {
447         IdeUpdateUsageTriggerCollector.trigger("notification.shown")
448         val title = IdeBundle.message("updates.new.build.notification.title", ApplicationNamesInfo.getInstance().fullProductName,
449                                       newBuild.version)
450         showNotification(project, title, "", {
451           IdeUpdateUsageTriggerCollector.trigger("notification.clicked")
452           runnable()
453         }, null, NotificationUniqueType.PLATFORM)
454       }
455       return
456     }
457
458     var updateFound = false
459
460     if (updatedPlugins != null && !updatedPlugins.isEmpty()) {
461       updateFound = true
462
463       ourShownNotifications.remove(NotificationUniqueType.PLUGINS)?.forEach { it.expire() }
464
465       if (showDialog || !canEnableNotifications()) {
466         PluginUpdateDialog(updatedPlugins).show()
467       }
468       else {
469         val runnable = { PluginManagerConfigurable.showPluginConfigurable(project, updatedPlugins) }
470
471         val ideFrame = WelcomeFrame.getInstance()
472         if (ideFrame is WelcomeFrameUpdater) {
473           ideFrame.showPluginUpdates(runnable)
474         }
475         else {
476           val title = IdeBundle.message("updates.plugins.ready.short.title.available")
477           val message = updatedPlugins.joinToString { downloader -> downloader.pluginName }
478           showNotification(project, title, message, runnable, { notification ->
479             notification.actions[0].templatePresentation.text = IdeBundle.message("plugin.settings.title")
480             notification.actions.add(0, object : NotificationAction(
481                 IdeBundle.message(if (updatedPlugins.size == 1) "plugins.configurable.update.button" else "plugin.manager.update.all")) {
482               override fun actionPerformed(e: AnActionEvent, notification: Notification) {
483                 notification.expire()
484                 PluginUpdateDialog.runUpdateAll(updatedPlugins, e.getData(PlatformDataKeys.CONTEXT_COMPONENT) as JComponent?)
485               }
486             })
487             notification.addAction(object : NotificationAction(
488               IdeBundle.message(if (updatedPlugins.size == 1) "updates.ignore.update.button" else "updates.ignore.updates.button")) {
489               override fun actionPerformed(e: AnActionEvent, notification: Notification) {
490                 notification.expire()
491                 PluginUpdateDialog.ignorePlugins(updatedPlugins.map { downloader -> downloader.descriptor })
492               }
493             })
494           }, NotificationUniqueType.PLUGINS)
495         }
496       }
497     }
498
499     if (externalUpdates != null && !externalUpdates.isEmpty()) {
500       updateFound = true
501
502       ourShownNotifications.remove(NotificationUniqueType.EXTERNAL)?.forEach { it.expire() }
503
504       for (update in externalUpdates) {
505         val runnable = { update.source.installUpdates(update.components) }
506
507         if (showDialog) {
508           runnable.invoke()
509         }
510         else {
511           val title = IdeBundle.message("updates.plugins.ready.title.available", ApplicationNamesInfo.getInstance().fullProductName)
512           val updates = update.components.joinToString(", ")
513           val message = IdeBundle.message("updates.external.ready.message", update.components.size, updates)
514           showNotification(project, title, message, runnable, null, NotificationUniqueType.EXTERNAL)
515         }
516       }
517     }
518
519     if (!updateFound) {
520       if (showDialog) {
521         NoUpdatesDialog(showSettingsLink).show()
522       }
523       else if (showEmptyNotification) {
524         ourShownNotifications.remove(NotificationUniqueType.PLUGINS)?.forEach { it.expire() }
525
526         val title = IdeBundle.message("updates.no.updates.notification")
527         showNotification(project, title, "", {}, { notification -> notification.actions.clear()}, NotificationUniqueType.PLUGINS)
528       }
529     }
530   }
531
532   private fun canEnableNotifications(): Boolean {
533     if (WelcomeFrame.getInstance() is WelcomeFrameUpdater) {
534       return true
535     }
536     return NotificationsConfigurationImpl.getInstanceImpl().SHOW_BALLOONS && NotificationsConfigurationImpl.getSettings(
537       getNotificationGroup().displayId).displayType != NotificationDisplayType.NONE
538   }
539
540   private fun showNotification(project: Project?,
541                                title: String,
542                                message: String,
543                                action: () -> Unit,
544                                extraBuilder: ((Notification) -> Unit)?,
545                                notificationType: NotificationUniqueType) {
546     val notification = getNotificationGroup().createNotification(title, XmlStringUtil.wrapInHtml(message), NotificationType.INFORMATION, null)
547     notification.collapseActionsDirection = Notification.CollapseActionsDirection.KEEP_LEFTMOST
548     notification.addAction(object : NotificationAction(IdeBundle.message("updates.notification.update.action")) {
549       override fun actionPerformed(e: AnActionEvent, notification: Notification) {
550         notification.expire()
551         action.invoke()
552       }
553     })
554     extraBuilder?.invoke(notification)
555     notification.whenExpired { ourShownNotifications.remove(notificationType, notification) }
556     notification.notify(project)
557     ourShownNotifications.putValue(notificationType, notification)
558   }
559
560   @JvmStatic
561   fun addUpdateRequestParameter(name: String, value: String) {
562     ourAdditionalRequestOptions[name] = value
563   }
564
565   private fun prepareUpdateCheckArgs(url: Url): Url {
566     addUpdateRequestParameter("build", ApplicationInfo.getInstance().build.asString())
567     addUpdateRequestParameter("uid", PermanentInstallationID.get())
568     addUpdateRequestParameter("os", SystemInfo.OS_NAME + ' ' + SystemInfo.OS_VERSION)
569     if (ExternalUpdateManager.ACTUAL != null) {
570       addUpdateRequestParameter("manager", ExternalUpdateManager.ACTUAL.toolName)
571     }
572     if (ApplicationInfoEx.getInstanceEx().isEAP) {
573       addUpdateRequestParameter("eap", "")
574     }
575     return url.addParameters(ourAdditionalRequestOptions)
576   }
577
578   @Deprecated("Replaced", ReplaceWith("PermanentInstallationID.get()", "com.intellij.openapi.application.PermanentInstallationID"))
579   @JvmStatic
580   @Suppress("unused", "UNUSED_PARAMETER")
581   fun getInstallationUID(c: PropertiesComponent): String = PermanentInstallationID.get()
582
583   @Deprecated(message = "Use disabledToUpdate", replaceWith = ReplaceWith("disabledToUpdate"))
584   @JvmStatic
585   @Suppress("unused")
586   val disabledToUpdatePlugins: Set<String>
587     get() = disabledToUpdate.mapTo(TreeSet()) { it.idString }
588
589   @JvmStatic
590   val disabledToUpdate: Set<PluginId>
591     get() {
592       var result = ourDisabledToUpdatePlugins
593       if (result == null) {
594         result = TreeSet()
595         if (!ApplicationManager.getApplication().isUnitTestMode) {
596           try {
597             val file = File(PathManager.getConfigPath(), DISABLED_UPDATE)
598             if (file.isFile) {
599               for (line in FileUtil.loadFile(file).split("[\\s]".toRegex())) {
600                 line.nullize(true)?.let {
601                   result.add(PluginId.getId(it))
602                 }
603               }
604             }
605           }
606           catch (e: IOException) {
607             LOG.error(e)
608           }
609         }
610
611         ourDisabledToUpdatePlugins = result
612       }
613       return result
614     }
615
616   @JvmStatic
617   fun saveDisabledToUpdatePlugins() {
618     val plugins = Paths.get(PathManager.getConfigPath(), DISABLED_UPDATE)
619     try {
620       PluginManagerCore.savePluginsList(disabledToUpdate, plugins, false)
621     }
622     catch (e: IOException) {
623       LOG.error(e)
624     }
625   }
626
627   private var ourHasFailedPlugins = false
628
629   @JvmStatic
630   fun checkForUpdate(event: IdeaLoggingEvent) {
631     if (!ourHasFailedPlugins) {
632       val app = ApplicationManager.getApplication()
633       if (app != null && !app.isDisposed && UpdateSettings.getInstance().isCheckNeeded) {
634         val pluginDescriptor = PluginManagerCore.getPlugin(
635           PluginUtil.getInstance().findPluginId(event.throwable))
636         if (pluginDescriptor != null && !pluginDescriptor.isBundled) {
637           ourHasFailedPlugins = true
638           updateAndShowResult()
639         }
640       }
641     }
642   }
643
644   /** A helper method for manually testing platform updates (see [com.intellij.internal.ShowUpdateInfoDialogAction]). */
645   @ApiStatus.Internal
646   fun testPlatformUpdate(project: Project?, updateInfoText: String, patchFilePath: String?, forceUpdate: Boolean) {
647     if (!ApplicationManager.getApplication().isInternal) {
648       throw IllegalStateException()
649     }
650
651     val channel: UpdateChannel?
652     val newBuild: BuildInfo?
653     val patches: UpdateChain?
654     if (forceUpdate) {
655       val node = JDOMUtil.load(updateInfoText).getChild("product")?.getChild("channel") ?: throw IllegalArgumentException(
656         "//channel missing")
657       channel = UpdateChannel(node)
658       newBuild = channel.builds.firstOrNull() ?: throw IllegalArgumentException("//build missing")
659       patches = newBuild.patches.firstOrNull()?.let { UpdateChain(listOf(it.fromBuild, newBuild.number), it.size) }
660     }
661     else {
662       val updateInfo = UpdatesInfo(JDOMUtil.load(updateInfoText))
663       val strategy = UpdateStrategy(ApplicationInfo.getInstance().build, updateInfo)
664       val checkForUpdateResult = strategy.checkForUpdates()
665       channel = checkForUpdateResult.updatedChannel
666       newBuild = checkForUpdateResult.newBuild
667       patches = checkForUpdateResult.patches
668     }
669
670     if (channel != null && newBuild != null) {
671       val patchFile = if (patchFilePath != null) File(FileUtil.toSystemDependentName(patchFilePath)) else null
672       UpdateInfoDialog(project, channel, newBuild, patches, patchFile).show()
673     }
674     else {
675       NoUpdatesDialog(true).show()
676     }
677   }
678 }