Skip to content

Commit 72312f6

Browse files
authored
refresh plugins metadata before showing update button
refresh plugins metadata before update Closes #2005
2 parents f773901 + 6ab1fa6 commit 72312f6

File tree

6 files changed

+152
-107
lines changed

6 files changed

+152
-107
lines changed

ide-common/src/main/kotlin/org/digma/intellij/plugin/updates/AggressiveUpdateService.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import org.digma.intellij.plugin.updates.CurrentUpdateState.OK
3434
import org.digma.intellij.plugin.updates.CurrentUpdateState.UPDATE_BACKEND
3535
import org.digma.intellij.plugin.updates.CurrentUpdateState.UPDATE_BOTH
3636
import org.digma.intellij.plugin.updates.CurrentUpdateState.UPDATE_PLUGIN
37+
import java.util.concurrent.TimeUnit
3738
import java.util.concurrent.atomic.AtomicBoolean
3839
import java.util.concurrent.atomic.AtomicReference
3940
import java.util.concurrent.locks.ReentrantLock
@@ -232,6 +233,17 @@ class AggressiveUpdateService : Disposable {
232233
updateStateRef.set(newUpdateState)
233234
if (prevState.updateState != newUpdateState.updateState) {
234235

236+
//the panel is going to show the update button.
237+
//in case when the plugin needs update, when user clicks the button we will open the intellij plugins settings.
238+
//sometimes the plugin list is not refreshed and user will not be able to update the plugin,
239+
// so we refresh plugins metadata before showing the button. waiting maximum 10 seconds for
240+
// the refresh to complete, and show the button anyway.
241+
if (listOf(UPDATE_PLUGIN, UPDATE_BOTH).any { it == updateTo }) {
242+
val future = refreshPluginsMetadata()
243+
//refreshPluginsMetadata returns a future that doesn't throw exception from get.
244+
future.get(10, TimeUnit.SECONDS)
245+
}
246+
235247
registerPosthogEvent(updateTo, versions)
236248

237249
Log.log(logger::debug, "state changed , firing event. prev state:{},new state:{}", prevState, newUpdateState)

ide-common/src/main/kotlin/org/digma/intellij/plugin/updates/UpdatesService.kt

Lines changed: 41 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package org.digma.intellij.plugin.updates
33
import com.intellij.collaboration.async.disposingScope
44
import com.intellij.openapi.Disposable
55
import com.intellij.openapi.components.Service
6+
import com.intellij.openapi.components.service
67
import com.intellij.openapi.diagnostic.Logger
78
import com.intellij.openapi.project.Project
9+
import kotlinx.coroutines.CancellationException
810
import kotlinx.coroutines.delay
11+
import kotlinx.coroutines.isActive
912
import kotlinx.coroutines.launch
1013
import org.apache.maven.artifact.versioning.ComparableVersion
1114
import org.digma.intellij.plugin.analytics.AnalyticsProviderException
@@ -23,41 +26,23 @@ import org.digma.intellij.plugin.model.rest.version.BackendVersionResponse
2326
import org.digma.intellij.plugin.model.rest.version.PluginVersionResponse
2427
import org.digma.intellij.plugin.model.rest.version.VersionResponse
2528
import org.digma.intellij.plugin.ui.panels.DigmaResettablePanel
26-
import org.jetbrains.annotations.VisibleForTesting
27-
import java.time.LocalDateTime
28-
import java.util.Timer
29-
import java.util.TimerTask
3029
import java.util.concurrent.TimeUnit
30+
import kotlin.time.Duration.Companion.minutes
3131

3232
@Service(Service.Level.PROJECT)
3333
class UpdatesService(private val project: Project) : Disposable {
3434

35-
companion object {
36-
private val logger = Logger.getInstance(UpdatesService::class.java)
35+
private val logger = Logger.getInstance(UpdatesService::class.java)
3736

37+
companion object {
3838
@JvmStatic
3939
fun getInstance(project: Project): UpdatesService {
40-
return project.getService(UpdatesService::class.java)
40+
return project.service<UpdatesService>()
4141
}
4242
}
4343

44-
private val BlackoutDurationSeconds =
45-
TimeUnit.MINUTES.toSeconds(5) // production value
46-
// TimeUnit.SECONDS.toSeconds(12) // use short period (few seconds) when debugging
47-
48-
// delay for first check for update since startup
49-
private val DelayMilliseconds = TimeUnit.SECONDS.toMillis(0)
50-
51-
private val PeriodMilliseconds =
52-
TimeUnit.MINUTES.toMillis(5) // production value is 5 minutes
53-
// TimeUnit.SECONDS.toMillis(12) // use short period (few seconds) when debugging
54-
55-
private val timer = Timer()
56-
5744
var affectedPanel: DigmaResettablePanel? = null // late init
5845

59-
private var blackoutStopTime: LocalDateTime = LocalDateTime.now().minusMonths(3)
60-
6146
private var prevBackendErrorsList: List<String> = emptyList()
6247
private var stateBackendVersion: BackendVersionResponse
6348
private var statePluginVersion: PluginVersion
@@ -66,29 +51,31 @@ class UpdatesService(private val project: Project) : Disposable {
6651
stateBackendVersion = BackendVersionResponse(false, "0.0.1", "0.0.1", BackendDeploymentType.Unknown)
6752
statePluginVersion = PluginVersion(getPluginVersion())
6853

69-
val fetchTask = object : TimerTask() {
70-
override fun run() {
71-
try {
54+
@Suppress("UnstableApiUsage")
55+
disposingScope().launch {
56+
try {
57+
58+
while (isActive) {
7259
checkForNewerVersions()
73-
} catch (e: Exception) {
74-
Log.warnWithException(logger, e, "Exception in checkForNewerVersions")
75-
ErrorReporter.getInstance().reportError(project, "UpdatesService.checkForNewerVersions", e)
60+
delay(5.minutes.inWholeMilliseconds)
7661
}
62+
63+
} catch (e: CancellationException) {
64+
Log.debugWithException(logger, e, "Exception in checkForNewerVersions")
65+
} catch (e: Throwable) {
66+
Log.warnWithException(logger, e, "Exception in checkForNewerVersions")
67+
ErrorReporter.getInstance().reportError("UpdatesService.timer", e)
7768
}
7869
}
79-
80-
timer.schedule(
81-
fetchTask, DelayMilliseconds, PeriodMilliseconds
82-
)
8370
}
8471

8572
override fun dispose() {
86-
timer.cancel()
73+
//nothing to do , used as parent disposable
8774
}
8875

89-
fun checkForNewerVersions() {
76+
private fun checkForNewerVersions() {
9077

91-
Log.log(logger::debug,"checking for new versions")
78+
Log.log(logger::debug, "checking for new versions")
9279

9380
val backendConnectionMonitor = BackendConnectionMonitor.getInstance(project)
9481
if (backendConnectionMonitor.isConnectionError()) {
@@ -100,7 +87,7 @@ class UpdatesService(private val project: Project) : Disposable {
10087
var versionsResp: VersionResponse? = null
10188
try {
10289
versionsResp = analyticsService.getVersions(buildVersionRequest())
103-
Log.log(logger::debug,"got version response {}",versionsResp)
90+
Log.log(logger::debug, "got version response {}", versionsResp)
10491
} catch (ase: AnalyticsServiceException) {
10592
var logException = true
10693
if (ase.cause is AnalyticsProviderException) {
@@ -139,13 +126,23 @@ class UpdatesService(private val project: Project) : Disposable {
139126
stateBackendVersion = versionsResp.backend
140127
statePluginVersion.latestVersion = versionsResp.plugin.latestVersion
141128

129+
//the panel is going to show the update button if shouldUpdatePlugin is true.
130+
//when user clicks the button we will open the intellij plugins settings.
131+
//sometimes the plugin list is not refreshed and user will not be able to update the plugin,
132+
// so we refresh plugins metadata before showing the button. waiting maximum 10 seconds for
133+
// the refresh to complete, and show the button anyway.
134+
if (shouldUpdatePlugin()) {
135+
//refreshPluginsMetadata returns a future that doesn't throw exception from get.
136+
val future = refreshPluginsMetadata()
137+
future.get(10, TimeUnit.SECONDS)
138+
}
139+
142140
EDT.ensureEDT {
143141
affectedPanel?.reset()
144142
}
145143
}
146144

147-
@VisibleForTesting
148-
protected fun createResponseToInduceBackendUpdate(): VersionResponse {
145+
private fun createResponseToInduceBackendUpdate(): VersionResponse {
149146
val pluginVersionResp = PluginVersionResponse(false, "0.0.1")
150147
val backendVersionResp = BackendVersionResponse(
151148
true, "0.0.2", "0.0.1",
@@ -155,18 +152,6 @@ class UpdatesService(private val project: Project) : Disposable {
155152
return resp
156153
}
157154

158-
fun updateButtonClicked() {
159-
// start blackout time that update-state won't be displayed
160-
blackoutStopTime = LocalDateTime.now().plusSeconds(BlackoutDurationSeconds)
161-
162-
// give some time for the user/system to make the desired update, and only then recheck for newer version
163-
@Suppress("UnstableApiUsage")
164-
disposingScope().launch {
165-
delay(TimeUnit.SECONDS.toMillis(BlackoutDurationSeconds) + 500)
166-
167-
checkForNewerVersions()
168-
}
169-
}
170155

171156
fun evalAndGetState(): UpdateState {
172157
Log.log(logger::debug, "evalAndGetState called")
@@ -177,39 +162,22 @@ class UpdatesService(private val project: Project) : Disposable {
177162
)
178163
}
179164

180-
@VisibleForTesting
181-
protected fun isDuringBlackout(): Boolean {
182-
val now = LocalDateTime.now()
183-
return now < blackoutStopTime
165+
private fun shouldUpdateBackend(): Boolean {
166+
return evalHasNewerVersion(stateBackendVersion)
184167
}
185168

186-
@VisibleForTesting
187-
protected fun shouldUpdateBackend(): Boolean {
188-
if (isDuringBlackout()) return false
189-
190-
var hasNewVersion = evalHasNewerVersion(stateBackendVersion)
191-
// hasNewVersion = true // use const only when debugging
192-
return hasNewVersion
169+
private fun shouldUpdatePlugin(): Boolean {
170+
return evalHasNewerVersion(statePluginVersion)
193171
}
194172

195-
@VisibleForTesting
196-
protected fun shouldUpdatePlugin(): Boolean {
197-
if (isDuringBlackout()) return false
198-
199-
var hasNewVersion = evalHasNewerVersion(statePluginVersion)
200-
// hasNewVersion = true // use const only when debugging
201-
return hasNewVersion
202-
}
203173

204-
@VisibleForTesting
205-
protected fun evalHasNewerVersion(backend: BackendVersionResponse): Boolean {
174+
private fun evalHasNewerVersion(backend: BackendVersionResponse): Boolean {
206175
val currCompVersion = ComparableVersion(backend.currentVersion)
207176
val latestCompVersion = ComparableVersion(backend.latestVersion)
208177
return latestCompVersion.newerThan(currCompVersion)
209178
}
210179

211-
@VisibleForTesting
212-
protected fun evalHasNewerVersion(plugin: PluginVersion): Boolean {
180+
private fun evalHasNewerVersion(plugin: PluginVersion): Boolean {
213181
val currCompVersion = ComparableVersion(plugin.currentVersion)
214182
val latestCompVersion = ComparableVersion(plugin.latestVersion)
215183
return latestCompVersion.newerThan(currCompVersion)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package org.digma.intellij.plugin.updates
2+
3+
import com.intellij.openapi.updateSettings.impl.UpdateChecker
4+
import com.intellij.util.concurrency.FutureResult
5+
import org.digma.intellij.plugin.errorreporting.ErrorReporter
6+
import java.util.concurrent.Future
7+
import java.util.concurrent.TimeUnit
8+
9+
10+
/**
11+
* This method is called just before showing the update button in update suggestion or the aggressive
12+
* update button. it is meant to refresh the updatable plugins in case intellij didn't refresh for some
13+
* time or user disabled refresh.
14+
* the refresh takes a few seconds and by the time user clicks the button the list in Settings -> Plugins
15+
* should be refreshed already.
16+
* the method returns a Future that doesn't throw exception from get. callers should not rely on the real
17+
* completion of the refresh and should do their thing even if not completed.
18+
* this method may be called on any thread, UpdateChecker.updateAndShowResult runs a background thread
19+
* and ends with calling invokeLater.
20+
*/
21+
/*
22+
* implementation note: initially we wanted to let user click the button and only then call
23+
* UpdateChecker.updateAndShowResult and let user know of the refresh and wait until its finished
24+
* using some modal dialog, for example calling intellij modal task will show a modal progress bar.
25+
* but, it is impossible to call this method while a modal dialog or progress is showing.
26+
* a modal dialog blocks execution of the event queue, no swing event is processed while a modal dialog
27+
* is visible. but, the method UpdateChecker.updateAndShowResult wants to invokeLater
28+
* after downloading the plugins metadata, so it can never finish while there is a modal dialog visible.
29+
* it's possible to show a non-modal popup, but with non-modal popup user can click somewhere in the IDE and the
30+
* popup will close and may be a weird experience to pop up the plugin settings after few seconds.
31+
*
32+
* So the solution is to call UpdateChecker.updateAndShowResult before showing the update button.
33+
* showing the update button is not urgent and can wait a few seconds before we show it. that way when
34+
* user clicks the update button, and we open the plugin settings the plugins list will be refreshed.
35+
*/
36+
fun refreshPluginsMetadata(): Future<Boolean> {
37+
38+
//a future that doesn't throw timeout exception and returns true on timeout.
39+
//callers just need to do something when UpdateChecker.updateAndShowResult is finished but should not rely
40+
// on success of the call. they should still do their thing even if the call didn't complete
41+
val future = object : FutureResult<Boolean>() {
42+
override fun get(timeout: Long, unit: TimeUnit): Boolean {
43+
return try {
44+
super.get(timeout, unit)
45+
} catch (e: Throwable) {
46+
true
47+
}
48+
}
49+
50+
override fun get(): Boolean {
51+
return get(0, TimeUnit.SECONDS)
52+
}
53+
}
54+
55+
return try {
56+
val actionCallback = UpdateChecker.updateAndShowResult()
57+
actionCallback.doWhenDone {
58+
future.set(true)
59+
}
60+
future
61+
} catch (e: Throwable) {
62+
ErrorReporter.getInstance().reportError("UpdatesUtilsKt.refreshPluginsMetadata", e)
63+
future.set(true)
64+
future
65+
}
66+
}

src/main/kotlin/org/digma/intellij/plugin/ui/common/MainToolWindowPanel.kt

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,22 +55,19 @@ fun createMainToolWindowPanel(project: Project, mainContentPanel: JPanel): JPane
5555
topPanel.layout = BoxLayout(topPanel, BoxLayout.Y_AXIS)
5656
topPanel.isOpaque = false
5757
topPanel.border = JBUI.Borders.empty()
58-
topPanel.add(updatePanel)
59-
topPanel.add(navigationPanel)
60-
topPanel.add(loadStatusPanel)
6158

6259
updateIDEPanel?.let {
6360
topPanel.add(it)
6461
}
65-
66-
62+
topPanel.add(updatePanel)
63+
topPanel.add(loadStatusPanel)
6764
quarkusConfigureDepsPanel?.let {
6865
topPanel.add(it)
6966
}
70-
7167
springBootConfigureDepsPanel?.let {
7268
topPanel.add(it)
7369
}
70+
topPanel.add(navigationPanel)
7471

7572
result.add(topPanel, BorderLayout.NORTH)
7673
result.add(mainContentPanel, BorderLayout.CENTER)
@@ -124,7 +121,8 @@ private fun createUpdateIntellijPanel(updatePanel: UpdateVersionPanel, project:
124121
updatePanel.tempEnableReset = true
125122
updatePanel.reset()
126123

127-
ActivityMonitor.getInstance(project).registerCustomEvent("update IDE button clicked",
124+
ActivityMonitor.getInstance(project).registerUserAction(
125+
"update IDE button clicked",
128126
mapOf(
129127
"ideVersion" to ApplicationInfo.getInstance().fullVersion
130128
))

0 commit comments

Comments
 (0)