From 7886095327f88447b5ab5175d9be6fd494aad865 Mon Sep 17 00:00:00 2001 From: Nikolay Krasko Date: Thu, 28 Feb 2019 19:13:58 +0300 Subject: [PATCH] Prevent constant reporting from released plugin versions in EA (KT-30388) Apply a compromise strategy between "no report" and "always report". Report only for several days after release and don't bother users afterwards. #KT-30388 Fixed All exceptions still can be found in Idea Log. --- .../kotlin/idea/KotlinPluginUpdater.kt | 48 ++++- .../kotlin/idea/PluginStartupComponent.java | 3 + .../idea/reporter/KotlinReportSubmitter.kt | 183 +++++++++++++++++- 3 files changed, 230 insertions(+), 4 deletions(-) diff --git a/idea/src/org/jetbrains/kotlin/idea/KotlinPluginUpdater.kt b/idea/src/org/jetbrains/kotlin/idea/KotlinPluginUpdater.kt index cb827e5cc44..a2dc0fa19c7 100644 --- a/idea/src/org/jetbrains/kotlin/idea/KotlinPluginUpdater.kt +++ b/idea/src/org/jetbrains/kotlin/idea/KotlinPluginUpdater.kt @@ -27,13 +27,18 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.Alarm import com.intellij.util.io.HttpRequests import com.intellij.util.text.VersionComparatorUtil -import org.jetbrains.kotlin.idea.update.PluginUpdateVerifier +import org.jdom.Attribute +import org.jdom.DataConversionException import org.jetbrains.kotlin.idea.update.verify import java.io.File import java.io.IOException import java.io.PrintWriter import java.io.StringWriter import java.net.URLEncoder +import java.time.DateTimeException +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneOffset import java.util.concurrent.TimeUnit sealed class PluginUpdateStatus { @@ -322,5 +327,46 @@ class KotlinPluginUpdater(val propertiesComponent: PropertiesComponent) : Dispos private val LOG = Logger.getInstance(KotlinPluginUpdater::class.java) fun getInstance(): KotlinPluginUpdater = ServiceManager.getService(KotlinPluginUpdater::class.java) + + class ResponseParseException(message: String, cause: Exception? = null) : IllegalStateException(message, cause) + + @Throws(IOException::class, ResponseParseException::class) + fun fetchPluginReleaseDate(pluginId: String, version: String): LocalDate? { + // Need a better request (MP-2157) + val url = "https://plugins.jetbrains.com/plugins/list?pluginId=$pluginId&pluginVersion=$version" + + val responseDoc = HttpRequests.request(url).connect { + JDOMUtil.load(it.inputStream) + } + + if (responseDoc.name != "plugin-repository") { + throw ResponseParseException("No plugin repository element") + } + + val dateAttribute: Attribute = responseDoc + .getChild("category") + ?.getChildren("idea-plugin") + ?.mapNotNull { pluginElement -> + if (pluginElement.getChild("version")?.text == version) { + pluginElement.getAttribute("date") + } else { + null + } + } + ?.singleOrNull() + ?: throw ResponseParseException("Can't find current plugin version") + + val dateLong = try { + dateAttribute.longValue + } catch (e: DataConversionException) { + throw ResponseParseException("Can't convert date value to long", e) + } + + return try { + Instant.ofEpochMilli(dateLong).atZone(ZoneOffset.UTC).toLocalDate() + } catch (e: DateTimeException) { + throw ResponseParseException("Can't convert to date", e) + } + } } } diff --git a/idea/src/org/jetbrains/kotlin/idea/PluginStartupComponent.java b/idea/src/org/jetbrains/kotlin/idea/PluginStartupComponent.java index 13104711fae..1b8fa218aa2 100644 --- a/idea/src/org/jetbrains/kotlin/idea/PluginStartupComponent.java +++ b/idea/src/org/jetbrains/kotlin/idea/PluginStartupComponent.java @@ -29,6 +29,7 @@ import com.intellij.openapi.updateSettings.impl.UpdateChecker; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.search.searches.IndexPatternSearch; import org.jetbrains.annotations.NotNull; +import org.jetbrains.kotlin.idea.reporter.KotlinReportSubmitter; import org.jetbrains.kotlin.idea.search.ideaExtensions.KotlinTodoSearcher; import org.jetbrains.kotlin.utils.PathUtil; @@ -82,6 +83,8 @@ public class PluginStartupComponent implements ApplicationComponent { KotlinPluginCompatibilityVerifier.checkCompatibility(); + KotlinReportSubmitter.Companion.setupReportingFromRelease(); + //todo[Sedunov]: wait for fix in platform to avoid misunderstood from Java newbies (also ConfigureKotlinInTempDirTest) //KotlinSdkType.Companion.setUpIfNeeded(); } diff --git a/idea/src/org/jetbrains/kotlin/idea/reporter/KotlinReportSubmitter.kt b/idea/src/org/jetbrains/kotlin/idea/reporter/KotlinReportSubmitter.kt index 17ab56fee9a..280a151f95f 100644 --- a/idea/src/org/jetbrains/kotlin/idea/reporter/KotlinReportSubmitter.kt +++ b/idea/src/org/jetbrains/kotlin/idea/reporter/KotlinReportSubmitter.kt @@ -18,30 +18,204 @@ package org.jetbrains.kotlin.idea.reporter import com.intellij.diagnostic.ReportMessages import com.intellij.ide.DataManager +import com.intellij.ide.util.PropertiesComponent import com.intellij.notification.NotificationType import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.IdeaLoggingEvent +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.SubmittedReportInfo import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages import com.intellij.util.Consumer +import com.intellij.util.ThreeState import org.jetbrains.kotlin.idea.KotlinPluginUpdater import org.jetbrains.kotlin.idea.KotlinPluginUtil import org.jetbrains.kotlin.idea.PluginUpdateStatus +import org.jetbrains.kotlin.idea.util.isEap import java.awt.Component +import java.io.IOException +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeParseException +import java.time.temporal.ChronoUnit import javax.swing.Icon /** * We need to wrap ITNReporter for force showing or errors from kotlin plugin even from released version of IDEA. */ class KotlinReportSubmitter : ITNReporterCompat() { + companion object { + private const val KOTLIN_FATAL_ERROR_NOTIFICATION_PROPERTY = "kotlin.fatal.error.notification" + private const val IDEA_FATAL_ERROR_NOTIFICATION_PROPERTY = "idea.fatal.error.notification" + private const val DISABLED_VALUE = "disabled" + private const val ENABLED_VALUE = "enabled" + + private const val KOTLIN_PLUGIN_RELEASE_DATE = "kotlin.plugin.releaseDate" + + private val LOG = Logger.getInstance(KotlinReportSubmitter::class.java) + + @Volatile + private var isFatalErrorReportingDisabledInRelease = ThreeState.UNSURE + + private val isIdeaAndKotlinRelease by lazy { + // Disabled in released version of IDEA and Android Studio + // Enabled in EAPs, Canary and Beta + val isReleaseLikeIdea = DISABLED_VALUE == System.getProperty(IDEA_FATAL_ERROR_NOTIFICATION_PROPERTY, ENABLED_VALUE) + + val isKotlinRelease = + !(KotlinPluginUtil.isSnapshotVersion() || KotlinPluginUtil.isDevVersion() || isEap(KotlinPluginUtil.getPluginVersion())) + + isReleaseLikeIdea && isKotlinRelease + } + + private const val NUMBER_OF_REPORTING_DAYS_FROM_RELEASE = 7 + + fun setupReportingFromRelease() { + if (ApplicationManager.getApplication().isUnitTestMode) { + return + } + + if (!isIdeaAndKotlinRelease) { + return + } + + val currentPluginReleaseDate = readStoredPluginReleaseDate() + if (currentPluginReleaseDate != null) { + isFatalErrorReportingDisabledInRelease = + if (isFatalErrorReportingDisabled(currentPluginReleaseDate)) ThreeState.YES else ThreeState.NO + return + } + + ApplicationManager.getApplication().executeOnPooledThread { + val releaseDate = + try { + KotlinPluginUpdater.fetchPluginReleaseDate( + KotlinPluginUtil.KOTLIN_PLUGIN_ID.idString, + KotlinPluginUtil.getPluginVersion() + ) + } catch (e: IOException) { + // Do not report connection problems + null + } catch (e: KotlinPluginUpdater.Companion.ResponseParseException) { + // Exception won't be shown, but will be logged + LOG.error(e) + return@executeOnPooledThread + } + + if (releaseDate != null) { + writePluginReleaseValue(releaseDate) + } else { + // Will try to fetch the same release date on IDE restart + } + + isFatalErrorReportingDisabledInRelease = isFatalErrorReportingWithDefault(releaseDate) + } + } + + private fun isFatalErrorReportingWithDefault(releaseDate: LocalDate?): ThreeState { + return if (releaseDate != null) { + if (isFatalErrorReportingDisabled(releaseDate)) ThreeState.YES else ThreeState.NO + } else { + // Disable reporting by default until we obtain a valid release date. + // We might fail reporting exceptions that happened before initialization but after successful release date fetching + // such exceptions maybe be reported after restart. + ThreeState.YES + } + } + + private fun isFatalErrorReportingDisabledWithUpdate(): Boolean { + val currentPluginReleaseDate = readStoredPluginReleaseDate() + isFatalErrorReportingDisabledInRelease = isFatalErrorReportingWithDefault(currentPluginReleaseDate) + + return isFatalErrorReportingDisabledInRelease == ThreeState.YES + } + + private fun isFatalErrorReportingDisabled(releaseDate: LocalDate): Boolean { + return ChronoUnit.DAYS.between(releaseDate, LocalDate.now()) > NUMBER_OF_REPORTING_DAYS_FROM_RELEASE + } + + private val RELEASE_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd") + + private fun readStoredPluginReleaseDate(): LocalDate? { + val pluginVersionToReleaseDate = PropertiesComponent.getInstance().getValue(KOTLIN_PLUGIN_RELEASE_DATE) ?: return null + + val parsedDate = fun(): LocalDate? { + val parts = pluginVersionToReleaseDate.split(":") + if (parts.size != 2) { + return null + } + + val pluginVersion = parts[0] + if (pluginVersion != KotlinPluginUtil.getPluginVersion()) { + // Stored for some other plugin version + return null + } + + return try { + val dateString = parts[1] + LocalDate.parse(dateString, RELEASE_DATE_FORMATTER) + } catch (e: DateTimeParseException) { + null + } + }.invoke() + + if (parsedDate == null) { + PropertiesComponent.getInstance().setValue(KOTLIN_PLUGIN_RELEASE_DATE, null) + } + + return parsedDate + } + + private fun writePluginReleaseValue(date: LocalDate) { + val currentKotlinVersion = KotlinPluginUtil.getPluginVersion() + val dateStr = RELEASE_DATE_FORMATTER.format(date) + PropertiesComponent.getInstance().setValue(KOTLIN_PLUGIN_RELEASE_DATE, "$currentKotlinVersion:$dateStr") + } + } + private var hasUpdate = false private var hasLatestVersion = false override fun showErrorInRelease(event: IdeaLoggingEvent): Boolean { - val notificationEnabled = "disabled" != System.getProperty("kotlin.fatal.error.notification", "enabled") - return notificationEnabled && (!hasUpdate || ApplicationManager.getApplication().isInternal) + if (ApplicationManager.getApplication().isInternal) { + // Reporting is always enabled for internal mode in the platform + return true + } + + if (ApplicationManager.getApplication().isUnitTestMode) { + return true + } + + if (hasUpdate) { + return false + } + + val kotlinNotificationEnabled = DISABLED_VALUE != System.getProperty(KOTLIN_FATAL_ERROR_NOTIFICATION_PROPERTY, ENABLED_VALUE) + if (!kotlinNotificationEnabled) { + // Kotlin notifications are explicitly disabled + return false + } + + if (!isIdeaAndKotlinRelease) { + return true + } + + return when (isFatalErrorReportingDisabledInRelease) { + ThreeState.YES -> + false + + ThreeState.NO -> { + // Reiterate the check for the case when there was no restart for long and reporting decision might expire + !isFatalErrorReportingDisabledWithUpdate() + } + + ThreeState.UNSURE -> { + // There might be an exception while initialization isn't complete. + // Decide urgently based on previously stored release version if already fetched. + !isFatalErrorReportingDisabledWithUpdate() + } + } } override fun submitCompat( @@ -54,6 +228,8 @@ class KotlinReportSubmitter : ITNReporterCompat() { if (ApplicationManager.getApplication().isInternal) { return super.submitCompat(events, additionalInfo, parentComponent, consumer) } + + // TODO: What happens here? User clicks report but no report is send? return true } @@ -68,7 +244,8 @@ class KotlinReportSubmitter : ITNReporterCompat() { ReportMessages.ERROR_REPORT, "Can't report exception from patched plugin", NotificationType.INFORMATION, - null) + null + ) .setImportant(false) .notify(project) return true