Skip to content
Closed
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c3cc2bf
Display toast notifications with actions
manodnyab Nov 13, 2024
09729c7
Condition matcher for displaying notifications
manodnyab Nov 15, 2024
f0dbe5b
initial commit
samgst-amazon Nov 19, 2024
e909a0e
Modified deserialization cases and added tests
manodnyab Nov 20, 2024
5a84e39
Merge branch 'feature/ideNotifs' into manodnyb/checkRulesForNotificat…
manodnyab Nov 20, 2024
56c1888
not required file change
manodnyab Nov 20, 2024
a981227
Merge remote-tracking branch 'origin/manodnyb/checkRulesForNotificati…
manodnyab Nov 20, 2024
bd354fc
run on startup
samgst-amazon Nov 20, 2024
fea410a
detekt
samgst-amazon Nov 20, 2024
746e5ab
move vals
samgst-amazon Nov 20, 2024
b038d48
Merge main into feature/ideNotifs-polling
aws-toolkit-automation Nov 20, 2024
1d6e7d2
Merge main into feature/ideNotifs-polling
aws-toolkit-automation Nov 20, 2024
c4f3cac
remote resource implementation
samgst-amazon Nov 20, 2024
867bcda
comments
samgst-amazon Nov 20, 2024
b7f336e
Merge branch 'feature/ideNotifs' into feature/ideNotifs-polling
samgst-amazon Nov 20, 2024
1883530
detekt
samgst-amazon Nov 20, 2024
dbfcefc
feedback 1
manodnyab Nov 20, 2024
823ea7f
Merge remote-tracking branch 'origin/feature/ideNotifs' into manodnyb…
manodnyab Nov 20, 2024
218c8ed
modified the base class
manodnyab Nov 20, 2024
285650f
Merge main into feature/ideNotifs-polling
aws-toolkit-automation Nov 21, 2024
d2b7a6c
Merge branch 'manodnyb/checkRulesForNotifications' into feature/ideNo…
samgst-amazon Nov 21, 2024
c82ee66
Validate file before saving
samgst-amazon Nov 21, 2024
4c397f4
cache path
samgst-amazon Nov 21, 2024
033a3a9
merge conflicts
samgst-amazon Nov 21, 2024
b01283e
feat(amazonq): Introduce auto trigger changes officially (#5080)
andrewyuq Nov 21, 2024
5506ca8
Merge main into feature/ideNotifs-polling
aws-toolkit-automation Nov 21, 2024
dd72d29
observer implementation
samgst-amazon Nov 21, 2024
d6e7aa7
deserialize notifs from file
samgst-amazon Nov 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.core.notifications

import com.fasterxml.jackson.databind.DeserializationFeature

Check warning

Code scanning / QDJVMC

Unused import directive Warning

Unused import directive
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper

Check warning

Code scanning / QDJVMC

Unused import directive Warning

Unused import directive
import com.fasterxml.jackson.module.kotlin.readValue
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.Service
import com.intellij.util.Alarm
import com.intellij.util.AlarmFactory
import com.intellij.util.io.HttpRequests
import software.aws.toolkits.core.utils.RemoteResolveParser
import software.aws.toolkits.core.utils.RemoteResource
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.core.DefaultRemoteResourceResolverProvider
import software.aws.toolkits.jetbrains.core.RemoteResourceResolverProvider
import java.io.InputStream
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Duration

private const val NOTIFICATION_ENDPOINT = "https://idetoolkits-hostedfiles.amazonaws.com/Notifications/JetBrains/1.json" // TODO: Replace with actual endpoint
private const val MAX_RETRIES = 3
private const val RETRY_DELAY_MS = 1000L

interface NotificationPollingService {
fun startPolling()
fun dispose()
}

object NotificationFileValidator : RemoteResolveParser {
private val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
override fun canBeParsed(data: InputStream): Boolean {
return try {
mapper.readValue<NotificationsList>(data)
true
} catch (e: Exception) {
false
}
}
}

@Service(Service.Level.APP)
class NotificationPollingServiceImpl :

Check warning on line 50 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Extension class should be final and non-public

Service implementation should not be public. If a service is supposed to be used outside its module, extract an interface from it and specify it as serviceInterface in plugin.xml.

Check warning on line 50 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Extension class should be final and non-public

Service implementation should not be public. If a service is supposed to be used outside its module, extract an interface from it and specify it as serviceInterface in plugin.xml.

Check warning

Code scanning / QDJVMC

Extension class should be final and non-public Warning

Service implementation should not be public. If a service is supposed to be used outside its module, extract an interface from it and specify it as serviceInterface in plugin.xml.
NotificationPollingService,
PersistentStateComponent<NotificationPollingServiceImpl.State>,
Disposable {

private var state = State()
private val alarm = AlarmFactory.getInstance().create(Alarm.ThreadToUse.POOLED_THREAD, this)
private val pollingIntervalMs = Duration.ofMinutes(10).toMillis()
private val resourceResolver: RemoteResourceResolverProvider = DefaultRemoteResourceResolverProvider()
private val notificationsResource = object : RemoteResource {
override val name: String = "notifications.json"
override val urls: List<String> = listOf(NOTIFICATION_ENDPOINT)
override val remoteResolveParser: RemoteResolveParser = NotificationFileValidator
}

data class State(
var currentETag: String? = null,
var cachedFilePath: String? = null
)

override fun getState(): State = state

override fun loadState(state: State) {
this.state = state
}

override fun dispose() {
alarm.dispose()
}

override fun startPolling() {
pollForNotifications()
// todo notify observers
alarm.addRequest(
{ startPolling() },
pollingIntervalMs
)
}

/**
* Main polling function that checks for updates and downloads if necessary
* Returns the parsed notifications if successful, null otherwise
*/
private fun pollForNotifications(): Boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add tests for this?

var retryCount = 0
var lastException: Exception? = null

while (retryCount < MAX_RETRIES) {
try {
val newETag = getNotificationETag()
if (newETag == state.currentETag) {
LOG.debug { "No updates available for notifications" }
return false
}
val resolvedPath = resourceResolver.get()
.resolve(notificationsResource)
.toCompletableFuture()
.get()
state.currentETag = newETag
state.cachedFilePath = resolvedPath.toString()
return true
} catch (e: Exception) {
lastException = e
LOG.error(e) { "Failed to poll for notifications (attempt ${retryCount + 1}/$MAX_RETRIES)" }
retryCount++
if (retryCount < MAX_RETRIES) {
val backoffDelay = RETRY_DELAY_MS * (1L shl (retryCount - 1))
Thread.sleep(backoffDelay)
}
}
}
emitFailureMetric(lastException)
return false
}

private fun getNotificationETag(): String =
HttpRequests.request(NOTIFICATION_ENDPOINT)
.userAgent("AWS Toolkit for JetBrains")
.connect { request ->
request.connection.headerFields["ETag"]?.firstOrNull() ?: ""
}

// Helper method to get Path from stored String
fun getCachedPath(): Path? =

Check warning on line 133 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused symbol

Function "getCachedPath" is never used

Check notice

Code scanning / QDJVMC

Class member can have 'private' visibility Note

Function 'getCachedPath' could be private
state.cachedFilePath?.let { Paths.get(it) }

/**
* Emits telemetry metric for polling failures
*/
private fun emitFailureMetric(exception: Exception?) {
// todo: add metric
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this already exists in common, we can add this metric

}

companion object {
private val LOG = getLogger<NotificationPollingServiceImpl>()
fun getInstance(): NotificationPollingService =
ApplicationManager.getApplication().getService(NotificationPollingService::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.core.notifications

import com.intellij.ide.util.RunOnceUtil
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity

class NotificationServiceInitializer : ProjectActivity {

Check warning on line 11 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationServiceInitializer.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Extension class should be final and non-public

Extension class should not be public

Check warning on line 11 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationServiceInitializer.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Extension class should be final and non-public

Extension class should not be public

Check warning

Code scanning / QDJVMC

Extension class should be final and non-public Warning

Extension class should not be public

override suspend fun execute(project: Project) {
val service = ApplicationManager.getApplication().getService(NotificationPollingService::class.java)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use the getInstance method here?

RunOnceUtil.runOnceForApp(this::class.qualifiedName.toString()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this? Wont the startup activity anyway run just once?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this to explicitly only run once per IDE instance and not per-project. It's what I understood to work, don't know about a better way yet

service.startPolling()
}
}
}
4 changes: 4 additions & 0 deletions plugins/core/src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
<!-- each plugin needs its own instance of these -->
<applicationService serviceImplementation="migration.software.aws.toolkits.jetbrains.core.coroutines.PluginCoroutineScopeTracker"/>
<projectService serviceImplementation="migration.software.aws.toolkits.jetbrains.core.coroutines.PluginCoroutineScopeTracker"/>
<postStartupActivity implementation = "software.aws.toolkits.jetbrains.core.notifications.NotificationServiceInitializer"/>
<applicationService
serviceInterface="software.aws.toolkits.jetbrains.core.notifications.NotificationPollingService"
serviceImplementation="software.aws.toolkits.jetbrains.core.notifications.NotificationPollingServiceImpl"/>
</extensions>
<projectListeners>
<listener class="software.aws.toolkits.jetbrains.services.telemetry.OpenedFileTypesMetricsListener" topic="com.intellij.openapi.fileEditor.FileEditorManagerListener"/>
Expand Down
Loading