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