Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move PluginUpdateManager to core and enable both Toolkit and Q auto-update #4258

Merged
merged 12 commits into from
Apr 15, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
<applicationService serviceInterface="software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator"
serviceImplementation="software.aws.toolkits.jetbrains.services.codewhisperer.customization.DefaultCodeWhispererModelConfigurator"/>
<applicationService serviceImplementation="software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererFeatureConfigService"/>
<applicationService serviceImplementation="software.aws.toolkits.jetbrains.services.codewhisperer.startup.QPluginUpdateManager"/>

<projectService serviceImplementation="software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager"/>
<projectService serviceInterface="software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
testServiceImplementation="software.aws.toolkits.jetbrains.core.credentials.sso.MockSsoLoginCallbackProvider"/>
<applicationService serviceInterface="software.aws.toolkits.jetbrains.core.credentials.profiles.ProfileWatcher"
serviceImplementation="software.aws.toolkits.jetbrains.core.credentials.profiles.DefaultProfileWatcher"/>
<applicationService serviceImplementation="software.aws.toolkits.jetbrains.core.plugin.PluginUpdateManager"/>

<projectService serviceInterface="software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager"
serviceImplementation="software.aws.toolkits.jetbrains.core.credentials.DefaultAwsConnectionManager"
Expand All @@ -54,6 +55,16 @@
<projectService serviceInterface="software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager"
serviceImplementation="software.aws.toolkits.jetbrains.core.credentials.DefaultToolkitConnectionManager"/>

<applicationConfigurable
id="aws"
groupId="tools"
bundle="software.aws.toolkits.resources.MessagesBundle"
key="aws.settings.title"
instance="software.aws.toolkits.jetbrains.settings.AwsSettingsCoreConfigurable"
/>

<postStartupActivity implementation="software.aws.toolkits.jetbrains.core.plugin.PluginAutoUpdater"/>

<registryKey key="aws.dev.pkceAuth" description="True if new authorization requests should be using the PKCE grant flow"
defaultValue="false" restartRequired="false"/>
<registryKey key="aws.telemetry.endpoint" description="Endpoint to use for publishing AWS client-side telemetry"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains

import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.extensions.PluginDescriptor
import com.intellij.openapi.extensions.PluginId
import java.nio.file.Path
import java.nio.file.Paths

object AwsToolkit {
const val TOOLKIT_PLUGIN_ID = "aws.toolkit"

// TODO: change this to real plugin id
const val Q_PLUGIN_ID = "plugin-amazonq"
andrewyuq marked this conversation as resolved.
Show resolved Hide resolved

// TODO: change this
const val CORE_PLUGIN_ID = "plugin-core"
andrewyuq marked this conversation as resolved.
Show resolved Hide resolved

private val TOOLKIT_PLUGIN_INFO = PluginInfo(TOOLKIT_PLUGIN_ID)
private val Q_PLUGIN_INFO = PluginInfo(Q_PLUGIN_ID)
private val CORE_PLUGIN_INFO = PluginInfo(CORE_PLUGIN_ID)

val PLUGINS_INFO = mapOf(
andrewyuq marked this conversation as resolved.
Show resolved Hide resolved
AwsPlugin.TOOLKIT to TOOLKIT_PLUGIN_INFO,
AwsPlugin.Q to Q_PLUGIN_INFO,
AwsPlugin.CORE to CORE_PLUGIN_INFO
)

const val GITHUB_URL = "https://github.com/aws/aws-toolkit-jetbrains"
const val AWS_DOCS_URL = "https://docs.aws.amazon.com/console/toolkit-for-jetbrains"
const val TOOLKIT_PLUGIN_NAME = "AWS Toolkit"
}

data class PluginInfo(val id: String) {
val descriptor: PluginDescriptor? = PluginManagerCore.getPlugin(PluginId.getId(id))
andrewyuq marked this conversation as resolved.
Show resolved Hide resolved
val name: String = when (id) {
AwsToolkit.TOOLKIT_PLUGIN_ID -> "AWS Toolkit"
AwsToolkit.Q_PLUGIN_ID -> "Amazon Q"
// TODO: change name
AwsToolkit.CORE_PLUGIN_ID -> "AWS Plugin Core"
else -> "AWS Toolkit"
andrewyuq marked this conversation as resolved.
Show resolved Hide resolved
}
val version: String = descriptor?.version ?: "Unknown"
val path: Path? =
if (ApplicationManager.getApplication().isUnitTestMode) {
Paths.get(System.getProperty("plugin.path"))
} else {
descriptor?.pluginPath
}
}

enum class AwsPlugin {
TOOLKIT,
Q,
CORE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.core.plugin

import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
import software.aws.toolkits.jetbrains.settings.AwsSettings
import java.util.concurrent.atomic.AtomicBoolean

class PluginAutoUpdater : ProjectActivity {
private val autoUpdateRunOnce = AtomicBoolean(false)

override suspend fun execute(project: Project) {
// We want the auto-update feature to be triggered only once per running application
if (!autoUpdateRunOnce.getAndSet(true)) {
PluginUpdateManager.getInstance().scheduleAutoUpdate()
if (!AwsSettings.getInstance().isAutoUpdateFeatureNotificationShownOnce) {
PluginUpdateManager.getInstance().notifyAutoUpdateFeature(project)
AwsSettings.getInstance().isAutoUpdateFeatureNotificationShownOnce = true
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.codewhisperer.startup
package software.aws.toolkits.jetbrains.core.plugin

import com.intellij.ide.plugins.IdeaPluginDescriptor
import com.intellij.ide.plugins.InstalledPluginsState
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.notification.NotificationAction
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runInEdt
Expand All @@ -14,86 +15,94 @@ import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.updateSettings.impl.PluginDownloader
import com.intellij.openapi.updateSettings.impl.PluginDownloader.compareVersionsSkipBrokenAndIncompatible
import com.intellij.openapi.updateSettings.impl.UpdateChecker
import com.intellij.util.Alarm
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import org.jetbrains.annotations.VisibleForTesting
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.AwsPlugin
import software.aws.toolkits.jetbrains.AwsToolkit
import software.aws.toolkits.jetbrains.settings.AwsSettings
import software.aws.toolkits.jetbrains.settings.AwsSettingsConfigurable
import software.aws.toolkits.jetbrains.settings.AwsSettingsCoreConfigurable
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.Component
import software.aws.toolkits.telemetry.Result
import software.aws.toolkits.telemetry.ToolkitTelemetry

// TODO: Still need to keep one for toolkit standalone
class QPluginUpdateManager {
class PluginUpdateManager {
private val alarm = Alarm(Alarm.ThreadToUse.SWING_THREAD)

init {
runTask()
}

@VisibleForTesting
internal fun runTask() {
fun scheduleAutoUpdate() {
if (alarm.isDisposed) return
scheduleUpdateTask()

val enabled = AwsSettings.getInstance().isAutoUpdateEnabled
LOG.debug { "AWS Toolkit checking for new updates. Auto update enabled: $enabled" }
LOG.debug { "AWS plugins checking for new updates. Auto update enabled: $enabled" }

if (!enabled) return

runInEdt {
ProgressManager.getInstance().run(
object : Task.Backgroundable(null, message("aws.settings.auto_update.progress.message")) {
override fun run(indicator: ProgressIndicator) {
checkForUpdates(indicator)
}
ProgressManager.getInstance().run(object : Task.Backgroundable(
null,
message("aws.settings.auto_update.progress.message")
) {
override fun run(indicator: ProgressIndicator) {
checkForUpdates(indicator, AwsPlugin.CORE)
checkForUpdates(indicator, AwsPlugin.TOOLKIT)
checkForUpdates(indicator, AwsPlugin.Q)
}
)
})
}
}

private fun scheduleUpdateTask() {
alarm.addRequest({ runTask() }, UPDATE_CHECK_INTERVAL_IN_MS)
alarm.addRequest({ scheduleAutoUpdate() }, UPDATE_CHECK_INTERVAL_IN_MS)
}

@RequiresBackgroundThread
fun checkForUpdates(progressIndicator: ProgressIndicator) {
fun checkForUpdates(progressIndicator: ProgressIndicator, plugin: AwsPlugin) {
val pluginInfo = AwsToolkit.PLUGINS_INFO[plugin] ?: return
val pluginId = pluginInfo.id
val pluginDescriptor = pluginInfo.descriptor as? IdeaPluginDescriptor ?: return
val pluginName = pluginInfo.name
// Note: This will need to handle exceptions and ensure thread-safety
try {
// If plugin is not installed, do not perform auto-update
if (!PluginManagerCore.isPluginInstalled(pluginDescriptor.pluginId)) {
LOG.debug { "$pluginName is not detected as installed, not performing auto-update" }
return
}

// wasUpdatedWithRestart means that, it was an update and it needs to restart to apply
if (InstalledPluginsState.getInstance().wasUpdatedWithRestart(PluginId.getId(AwsToolkit.Q_PLUGIN_ID))) {
LOG.debug { "AWS Toolkit was recently updated and needed restart, not performing auto-update again" }
if (InstalledPluginsState.getInstance().wasUpdatedWithRestart(PluginId.getId(pluginId))) {
LOG.debug { "$pluginName was recently updated and needed restart, not performing auto-update again" }
return
}

val toolkitPlugin = AwsToolkit.Q_DESCRIPTOR as IdeaPluginDescriptor? ?: return
if (toolkitPlugin.version.contains("SNAPSHOT", ignoreCase = true)) {
LOG.debug { "AWS Toolkit is a SNAPSHOT version, not performing auto-update" }
if (pluginDescriptor.version.contains("SNAPSHOT", ignoreCase = true)) {
LOG.debug { "$pluginName is a SNAPSHOT version, not performing auto-update" }
return
}
if (!toolkitPlugin.isEnabled) {
LOG.debug { "AWS Toolkit is disabled, not performing auto-update" }
if (!pluginDescriptor.isEnabled) {
LOG.debug { "$pluginName is disabled, not performing auto-update" }
return
}
LOG.debug { "Current version: ${toolkitPlugin.version}" }
val latestToolkitPluginDownloader = getUpdate(toolkitPlugin)
if (latestToolkitPluginDownloader == null) {
LOG.debug { "No newer version found, not performing auto-update" }
LOG.debug { "Current version: ${pluginDescriptor.version}" }
val latestPluginDownloader = getUpdate(pluginDescriptor)
if (latestPluginDownloader == null) {
LOG.debug { "$pluginName no newer version found, not performing auto-update" }
return
} else {
LOG.debug { "Found newer version: ${latestToolkitPluginDownloader.pluginVersion}" }
LOG.debug { "$pluginName found newer version: ${latestPluginDownloader.pluginVersion}" }
}

if (!latestToolkitPluginDownloader.prepareToInstall(progressIndicator)) return
latestToolkitPluginDownloader.install()
if (!latestPluginDownloader.prepareToInstall(progressIndicator)) return
latestPluginDownloader.install()
// TODO: distinguish telemetry
ToolkitTelemetry.showAction(
project = null,
success = true,
Expand All @@ -102,7 +111,8 @@ class QPluginUpdateManager {
component = Component.Filesystem
)
} catch (e: Exception) {
LOG.debug(e) { "Unable to update AWS Toolkit" }
LOG.debug(e) { "Unable to update $pluginName" }
// TODO: distinguish telemetry
ToolkitTelemetry.showAction(
project = null,
success = false,
Expand All @@ -114,7 +124,8 @@ class QPluginUpdateManager {
return
} catch (e: Error) {
// Handle cases like NoSuchMethodError when the API is not available in certain versions
LOG.debug(e) { "Unable to update AWS Toolkit" }
LOG.debug(e) { "Unable to update $pluginName" }
// TODO: distinguish telemetry
ToolkitTelemetry.showAction(
project = null,
success = false,
Expand All @@ -126,12 +137,14 @@ class QPluginUpdateManager {
return
}
if (!AwsSettings.getInstance().isAutoUpdateNotificationEnabled) return
if (plugin == AwsPlugin.CORE) return
notifyInfo(
title = message("aws.notification.auto_update.title"),
title = message("aws.notification.auto_update.title", pluginName),
content = message("aws.settings.auto_update.notification.message"),
project = null,
notificationActions = listOf(
NotificationAction.createSimpleExpiring(message("aws.settings.auto_update.notification.yes")) {
// TODO: distinguish telemetry
ToolkitTelemetry.invokeAction(
project = null,
result = Result.Succeeded,
Expand All @@ -142,6 +155,7 @@ class QPluginUpdateManager {
ApplicationManager.getApplication().restart()
},
NotificationAction.createSimpleExpiring(message("aws.settings.auto_update.notification.no")) {
// TODO: distinguish telemetry
ToolkitTelemetry.invokeAction(
project = null,
result = Result.Succeeded,
Expand All @@ -151,33 +165,52 @@ class QPluginUpdateManager {
)
},
NotificationAction.createSimple(message("aws.notification.auto_update.settings.title")) {
// TODO: distinguish telemetry
ToolkitTelemetry.invokeAction(
project = null,
result = Result.Succeeded,
id = ID_ACTION_AUTO_UPDATE_SETTINGS,
source = SOURCE_AUTO_UPDATE_FINISH_NOTIFY,
component = Component.Filesystem
)
ShowSettingsUtil.getInstance().showSettingsDialog(null, AwsSettingsConfigurable::class.java)
ShowSettingsUtil.getInstance().showSettingsDialog(null, AwsSettingsCoreConfigurable::class.java)
}
)
)
}

@VisibleForTesting
internal fun getUpdate(pluginDescriptor: IdeaPluginDescriptor): PluginDownloader? =
fun getUpdate(pluginDescriptor: IdeaPluginDescriptor): PluginDownloader? =
getUpdateInfo().firstOrNull {
it.id == pluginDescriptor.pluginId &&
compareVersionsSkipBrokenAndIncompatible(it.pluginVersion, pluginDescriptor) > 0
}

// TODO: Optimize this to only search the result for AWS Toolkit
@VisibleForTesting
internal fun getUpdateInfo(): Collection<PluginDownloader> = UpdateChecker.getPluginUpdates() ?: emptyList()
// TODO: Optimize this to only search the result for AWS plugins
fun getUpdateInfo(): Collection<PluginDownloader> = UpdateChecker.getPluginUpdates() ?: emptyList()

fun notifyAutoUpdateFeature(project: Project) {
notifyInfo(
title = message("aws.notification.auto_update.feature_intro.title"),
project = project,
notificationActions = listOf(
NotificationAction.createSimpleExpiring(message("aws.notification.auto_update.feature_intro.ok")) {},
NotificationAction.createSimple(message("aws.notification.auto_update.settings.title")) {
ToolkitTelemetry.invokeAction(
project = null,
result = Result.Succeeded,
id = ID_ACTION_AUTO_UPDATE_SETTINGS,
source = SOURCE_AUTO_UPDATE_FEATURE_INTRO_NOTIFY,
component = Component.Filesystem
)
ShowSettingsUtil.getInstance().showSettingsDialog(project, AwsSettingsCoreConfigurable::class.java)
}
)
)
}

companion object {
fun getInstance(): QPluginUpdateManager = service()
private val LOG = getLogger<QPluginUpdateManager>()
fun getInstance(): PluginUpdateManager = service()
private val LOG = getLogger<PluginUpdateManager>()
private const val UPDATE_CHECK_INTERVAL_IN_MS = 4 * 60 * 60 * 1000 // 4 hours
private const val SOURCE_AUTO_UPDATE_FINISH_NOTIFY = "autoUpdateFinishNotification"
const val SOURCE_AUTO_UPDATE_FEATURE_INTRO_NOTIFY = "autoUpdateFeatureIntroNotification"
Expand Down
Loading
Loading