Skip to content

Commit fb23b6e

Browse files
authored
Add plugin auto update feature (#3975)
1 parent ad77056 commit fb23b6e

File tree

9 files changed

+434
-2
lines changed

9 files changed

+434
-2
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "feature",
3+
"description" : "Add configurable auto plugin update feature"
4+
}

plugins/toolkit/jetbrains-core/resources/META-INF/plugin.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,8 @@
316316
serviceImplementation="software.aws.toolkits.jetbrains.core.tools.DefaultToolManager"
317317
testServiceImplementation="software.aws.toolkits.jetbrains.core.tools.MockToolManager"/>
318318

319+
<applicationService serviceImplementation="software.aws.toolkits.jetbrains.core.plugins.ToolkitUpdateManager"/>
320+
319321
<applicationService serviceInterface="software.aws.toolkits.core.ToolkitClientManager"
320322
serviceImplementation="software.aws.toolkits.jetbrains.core.AwsClientManager"
321323
testServiceImplementation="software.aws.toolkits.jetbrains.core.MockClientManager"/>
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.core.plugins
5+
6+
import com.intellij.ide.plugins.IdeaPluginDescriptor
7+
import com.intellij.ide.plugins.InstalledPluginsState
8+
import com.intellij.notification.NotificationAction
9+
import com.intellij.openapi.application.ApplicationManager
10+
import com.intellij.openapi.application.runInEdt
11+
import com.intellij.openapi.components.service
12+
import com.intellij.openapi.extensions.PluginId
13+
import com.intellij.openapi.options.ShowSettingsUtil
14+
import com.intellij.openapi.progress.ProgressIndicator
15+
import com.intellij.openapi.progress.ProgressManager
16+
import com.intellij.openapi.progress.Task
17+
import com.intellij.openapi.updateSettings.impl.PluginDownloader
18+
import com.intellij.openapi.updateSettings.impl.PluginDownloader.compareVersionsSkipBrokenAndIncompatible
19+
import com.intellij.openapi.updateSettings.impl.UpdateChecker
20+
import com.intellij.util.Alarm
21+
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
22+
import org.jetbrains.annotations.VisibleForTesting
23+
import software.aws.toolkits.core.utils.debug
24+
import software.aws.toolkits.core.utils.getLogger
25+
import software.aws.toolkits.jetbrains.AwsToolkit
26+
import software.aws.toolkits.jetbrains.settings.AwsSettings
27+
import software.aws.toolkits.jetbrains.settings.AwsSettingsConfigurable
28+
import software.aws.toolkits.jetbrains.utils.notifyInfo
29+
import software.aws.toolkits.resources.message
30+
import software.aws.toolkits.telemetry.Component
31+
import software.aws.toolkits.telemetry.Result
32+
import software.aws.toolkits.telemetry.ToolkitTelemetry
33+
34+
class ToolkitUpdateManager {
35+
private val alarm = Alarm(Alarm.ThreadToUse.SWING_THREAD)
36+
37+
init {
38+
runTask()
39+
}
40+
41+
@VisibleForTesting
42+
internal fun runTask() {
43+
if (alarm.isDisposed) return
44+
scheduleUpdateTask()
45+
46+
val enabled = AwsSettings.getInstance().isAutoUpdateEnabled
47+
LOG.debug { "AWS Toolkit checking for new updates. Auto update enabled: $enabled" }
48+
49+
if (!enabled) return
50+
51+
runInEdt {
52+
ProgressManager.getInstance().run(
53+
object : Task.Backgroundable(null, message("aws.settings.auto_update.progress.message")) {
54+
override fun run(indicator: ProgressIndicator) {
55+
checkForUpdates(indicator)
56+
}
57+
}
58+
)
59+
}
60+
}
61+
62+
private fun scheduleUpdateTask() {
63+
alarm.addRequest({ runTask() }, UPDATE_CHECK_INTERVAL_IN_MS)
64+
}
65+
66+
@RequiresBackgroundThread
67+
fun checkForUpdates(progressIndicator: ProgressIndicator) {
68+
// Note: This will need to handle exceptions and ensure thread-safety
69+
try {
70+
// wasUpdatedWithRestart means that, it was an update and it needs to restart to apply
71+
if (InstalledPluginsState.getInstance().wasUpdatedWithRestart(PluginId.getId(AwsToolkit.PLUGIN_ID))) {
72+
LOG.debug { "AWS Toolkit was recently updated and needed restart, not performing auto-update again" }
73+
return
74+
}
75+
76+
val toolkitPlugin = AwsToolkit.DESCRIPTOR as IdeaPluginDescriptor? ?: return
77+
if (!toolkitPlugin.isEnabled) {
78+
LOG.debug { "AWS Toolkit is disabled, not performing auto-update" }
79+
return
80+
}
81+
LOG.debug { "Current version: ${toolkitPlugin.version}" }
82+
val latestToolkitPluginDownloader = getUpdate(toolkitPlugin)
83+
if (latestToolkitPluginDownloader == null) {
84+
LOG.debug { "No newer version found, not performing auto-update" }
85+
return
86+
} else {
87+
LOG.debug { "Found newer version: ${latestToolkitPluginDownloader.pluginVersion}" }
88+
}
89+
90+
if (!latestToolkitPluginDownloader.prepareToInstall(progressIndicator)) return
91+
latestToolkitPluginDownloader.install()
92+
ToolkitTelemetry.showAction(
93+
project = null,
94+
success = true,
95+
id = SOURCE_AUTO_UPDATE_FINISH_NOTIFY,
96+
source = SOURCE_AUTO_UPDATE_FINISH_NOTIFY,
97+
component = Component.Filesystem
98+
)
99+
} catch (e: Exception) {
100+
LOG.debug(e) { "Unable to update AWS Toolkit" }
101+
ToolkitTelemetry.showAction(
102+
project = null,
103+
success = false,
104+
id = SOURCE_AUTO_UPDATE_FINISH_NOTIFY,
105+
source = SOURCE_AUTO_UPDATE_FINISH_NOTIFY,
106+
component = Component.Filesystem,
107+
reason = e.message
108+
)
109+
return
110+
} catch (e: Error) {
111+
// Handle cases like NoSuchMethodError when the API is not available in certain versions
112+
LOG.debug(e) { "Unable to update AWS Toolkit" }
113+
ToolkitTelemetry.showAction(
114+
project = null,
115+
success = false,
116+
id = SOURCE_AUTO_UPDATE_FINISH_NOTIFY,
117+
source = SOURCE_AUTO_UPDATE_FINISH_NOTIFY,
118+
component = Component.Filesystem,
119+
reason = e.message
120+
)
121+
return
122+
}
123+
if (!AwsSettings.getInstance().isAutoUpdateNotificationEnabled) return
124+
notifyInfo(
125+
title = message("aws.notification.auto_update.title"),
126+
content = message("aws.settings.auto_update.notification.message"),
127+
project = null,
128+
notificationActions = listOf(
129+
NotificationAction.createSimpleExpiring(message("aws.settings.auto_update.notification.yes")) {
130+
ToolkitTelemetry.invokeAction(
131+
project = null,
132+
result = Result.Succeeded,
133+
id = "autoUpdateActionRestart",
134+
source = SOURCE_AUTO_UPDATE_FINISH_NOTIFY,
135+
component = Component.Filesystem
136+
)
137+
ApplicationManager.getApplication().restart()
138+
},
139+
NotificationAction.createSimpleExpiring(message("aws.settings.auto_update.notification.no")) {
140+
ToolkitTelemetry.invokeAction(
141+
project = null,
142+
result = Result.Succeeded,
143+
id = "autoUpdateActionNotNow",
144+
source = SOURCE_AUTO_UPDATE_FINISH_NOTIFY,
145+
component = Component.Filesystem
146+
)
147+
},
148+
NotificationAction.createSimple(message("aws.notification.auto_update.settings.title")) {
149+
ToolkitTelemetry.invokeAction(
150+
project = null,
151+
result = Result.Succeeded,
152+
id = ID_ACTION_AUTO_UPDATE_SETTINGS,
153+
source = SOURCE_AUTO_UPDATE_FINISH_NOTIFY,
154+
component = Component.Filesystem
155+
)
156+
ShowSettingsUtil.getInstance().showSettingsDialog(null, AwsSettingsConfigurable::class.java)
157+
}
158+
)
159+
)
160+
}
161+
162+
@VisibleForTesting
163+
internal fun getUpdate(pluginDescriptor: IdeaPluginDescriptor): PluginDownloader? =
164+
getUpdateInfo().firstOrNull {
165+
it.id == pluginDescriptor.pluginId &&
166+
compareVersionsSkipBrokenAndIncompatible(it.pluginVersion, pluginDescriptor) > 0
167+
}
168+
169+
// TODO: Optimize this to only search the result for AWS Toolkit
170+
@VisibleForTesting
171+
internal fun getUpdateInfo(): Collection<PluginDownloader> = UpdateChecker.getPluginUpdates() ?: emptyList()
172+
173+
companion object {
174+
fun getInstance(): ToolkitUpdateManager = service()
175+
private val LOG = getLogger<ToolkitUpdateManager>()
176+
private const val UPDATE_CHECK_INTERVAL_IN_MS = 4 * 60 * 60 * 1000 // 4 hours
177+
private const val SOURCE_AUTO_UPDATE_FINISH_NOTIFY = "autoUpdateFinishNotification"
178+
const val SOURCE_AUTO_UPDATE_FEATURE_INTRO_NOTIFY = "autoUpdateFeatureIntroNotification"
179+
const val ID_ACTION_AUTO_UPDATE_SETTINGS = "autoUpdateActionSettings"
180+
}
181+
}

plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/services/codewhisperer/startup/CodeWhispererProjectStartupActivity.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@
44
package software.aws.toolkits.jetbrains.services.codewhisperer.startup
55

66
import com.intellij.codeInsight.lookup.LookupManagerListener
7+
import com.intellij.notification.NotificationAction
78
import com.intellij.openapi.application.ApplicationManager
89
import com.intellij.openapi.application.invokeLater
10+
import com.intellij.openapi.options.ShowSettingsUtil
911
import com.intellij.openapi.project.Project
1012
import com.intellij.openapi.startup.StartupActivity
1113
import kotlinx.coroutines.delay
1214
import kotlinx.coroutines.isActive
1315
import kotlinx.coroutines.launch
1416
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
1517
import software.aws.toolkits.jetbrains.core.explorer.refreshCwQTree
18+
import software.aws.toolkits.jetbrains.core.plugins.ToolkitUpdateManager
1619
import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererLoginType
1720
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager
1821
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled
@@ -26,14 +29,23 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhisperer
2629
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.notifyErrorAccountless
2730
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.notifyWarnAccountless
2831
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.promptReAuth
32+
import software.aws.toolkits.jetbrains.settings.AwsSettings
33+
import software.aws.toolkits.jetbrains.settings.AwsSettingsConfigurable
34+
import software.aws.toolkits.jetbrains.utils.notifyInfo
35+
import software.aws.toolkits.resources.message
36+
import software.aws.toolkits.telemetry.Component
37+
import software.aws.toolkits.telemetry.Result
38+
import software.aws.toolkits.telemetry.ToolkitTelemetry
2939
import java.time.LocalDateTime
3040
import java.util.Date
3141
import java.util.Timer
42+
import java.util.concurrent.atomic.AtomicBoolean
3243
import kotlin.concurrent.schedule
3344

3445
// TODO: add logics to check if we want to remove recommendation suspension date when user open the IDE
3546
class CodeWhispererProjectStartupActivity : StartupActivity.DumbAware {
3647
private var runOnce = false
48+
private val autoUpdateRunOnce = AtomicBoolean(false)
3749

3850
/**
3951
* Should be invoked when
@@ -44,6 +56,16 @@ class CodeWhispererProjectStartupActivity : StartupActivity.DumbAware {
4456
if (!ApplicationManager.getApplication().isUnitTestMode) {
4557
CodeWhispererStatusBarManager.getInstance(project).updateWidget()
4658
}
59+
60+
// We want the auto-update feature to be triggered only once per running application
61+
if (!autoUpdateRunOnce.getAndSet(true)) {
62+
ToolkitUpdateManager.getInstance()
63+
if (!AwsSettings.getInstance().isAutoUpdateFeatureNotificationShownOnce) {
64+
notifyAutoUpdateFeature(project)
65+
AwsSettings.getInstance().isAutoUpdateFeatureNotificationShownOnce = true
66+
}
67+
}
68+
4769
if (!isCodeWhispererEnabled(project)) return
4870
if (runOnce) return
4971

@@ -64,6 +86,26 @@ class CodeWhispererProjectStartupActivity : StartupActivity.DumbAware {
6486
runOnce = true
6587
}
6688

89+
private fun notifyAutoUpdateFeature(project: Project) {
90+
notifyInfo(
91+
title = message("aws.notification.auto_update.feature_intro.title"),
92+
project = project,
93+
notificationActions = listOf(
94+
NotificationAction.createSimpleExpiring(message("aws.notification.auto_update.feature_intro.ok")) {},
95+
NotificationAction.createSimple(message("aws.notification.auto_update.settings.title")) {
96+
ToolkitTelemetry.invokeAction(
97+
project = null,
98+
result = Result.Succeeded,
99+
id = ToolkitUpdateManager.ID_ACTION_AUTO_UPDATE_SETTINGS,
100+
source = ToolkitUpdateManager.SOURCE_AUTO_UPDATE_FEATURE_INTRO_NOTIFY,
101+
component = Component.Filesystem
102+
)
103+
ShowSettingsUtil.getInstance().showSettingsDialog(project, AwsSettingsConfigurable::class.java)
104+
}
105+
)
106+
)
107+
}
108+
67109
private fun showAccountlessNotificationIfNeeded(project: Project) {
68110
if (CodeWhispererExplorerActionManager.getInstance().checkActiveCodeWhispererConnectionType(project) == CodeWhispererLoginType.Accountless) {
69111
// simply show a notification when user login with Accountless, and it's still supported by CodeWhisperer

plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/settings/AwsSettings.kt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ interface AwsSettings {
2323
var promptedForTelemetry: Boolean
2424
var useDefaultCredentialRegion: UseAwsCredentialRegion
2525
var profilesNotification: ProfilesNotification
26+
var isAutoUpdateEnabled: Boolean
27+
var isAutoUpdateNotificationEnabled: Boolean
28+
var isAutoUpdateFeatureNotificationShownOnce: Boolean
2629
val clientId: UUID
2730

2831
companion object {
@@ -83,6 +86,24 @@ class DefaultAwsSettings : PersistentStateComponent<AwsConfiguration>, AwsSettin
8386
state.profilesNotification = value.name
8487
}
8588

89+
override var isAutoUpdateEnabled: Boolean
90+
get() = state.isAutoUpdateEnabled ?: true
91+
set(value) {
92+
state.isAutoUpdateEnabled = value
93+
}
94+
95+
override var isAutoUpdateNotificationEnabled: Boolean
96+
get() = state.isAutoUpdateNotificationEnabled ?: true
97+
set(value) {
98+
state.isAutoUpdateNotificationEnabled = value
99+
}
100+
101+
override var isAutoUpdateFeatureNotificationShownOnce: Boolean
102+
get() = state.isAutoUpdateFeatureNotificationShownOnce ?: false
103+
set(value) {
104+
state.isAutoUpdateFeatureNotificationShownOnce = value
105+
}
106+
86107
override val clientId: UUID
87108
@Synchronized get() {
88109
val id = when {
@@ -107,7 +128,10 @@ data class AwsConfiguration(
107128
var isTelemetryEnabled: Boolean? = null,
108129
var promptedForTelemetry: Boolean? = null,
109130
var useDefaultCredentialRegion: String? = null,
110-
var profilesNotification: String? = null
131+
var profilesNotification: String? = null,
132+
var isAutoUpdateEnabled: Boolean? = null,
133+
var isAutoUpdateNotificationEnabled: Boolean? = null,
134+
var isAutoUpdateFeatureNotificationShownOnce: Boolean? = null
111135
)
112136

113137
class ShowSettingsAction : AnAction(message("aws.settings.show.label")), DumbAware {

0 commit comments

Comments
 (0)