Skip to content

Commit

Permalink
fix: make plugin compatible to 2024.2, reduce time in UI Thread [IDE-…
Browse files Browse the repository at this point in the history
…395] (#542)

* fix: enable 2024.2, suppress some incorrect/unneeded warnings

* fix: improve UI thread handling, update CHANGELOG.md

* fix: improve shutdown handling and debouncing of file changes

* fix: improve startup & progress handling
  • Loading branch information
bastiandoetsch authored Jun 11, 2024
1 parent 4413c3c commit 1ee7f6b
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 102 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Added
- Add folder path to report analytics request.
- Support for 2024.4
- Spend less time in the UI thread

## [2.8.2]

Expand Down
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pluginName=Snyk Security
# for insight into build numbers and IntelliJ Platform versions
# see https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild=233
pluginUntilBuild=241.*
pluginUntilBuild=242.*
platformVersion=2023.3
platformDownloadSources=true

Expand All @@ -13,7 +13,7 @@ platformDownloadSources=true
# see https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
platformPlugins=org.intellij.plugins.hcl:233.11799.172,org.jetbrains.plugins.yaml,org.jetbrains.kotlin,com.intellij.java,org.intellij.groovy
# list of versions for which to check the plugin for api compatibility
pluginVerifierIdeVersions=2023.3,2024.1
pluginVerifierIdeVersions=2023.3,2024.1,2024.2
localIdeDirectory=
# opt-out flag for bundling Kotlin standard library
# see https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library
Expand Down
6 changes: 3 additions & 3 deletions src/main/kotlin/io/snyk/plugin/SnykProjectManagerListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ import snyk.common.lsp.LanguageServerWrapper
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

private const val TIMEOUT = 5L
private const val TIMEOUT = 1L

class SnykProjectManagerListener : ProjectManagerListener {
override fun projectClosing(project: Project) {
val closingTask = object : Backgroundable(project, "Project closing ${project.name}") {
override fun run(indicator: ProgressIndicator) {
// limit clean up to 5s
try {
Executors.newSingleThreadExecutor().submit {
Executors.newCachedThreadPool().submit {
// lets all running ProgressIndicators release MUTEX first
val ls = LanguageServerWrapper.getInstance()
if (ls.ensureLanguageServerInitialized()) {
if (ls.isInitialized) {
ls.updateWorkspaceFolders(emptySet(), ls.getWorkspaceFolders(project))
}
}.get(TIMEOUT, TimeUnit.SECONDS)
Expand Down
77 changes: 47 additions & 30 deletions src/main/kotlin/io/snyk/plugin/snykcode/SnykCodeBulkFileListener.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package io.snyk.plugin.snykcode

import com.google.common.cache.CacheBuilder
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.ide.impl.ProjectUtil
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task.Backgroundable
Expand All @@ -11,51 +13,66 @@ import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.openapi.vfs.readText
import io.snyk.plugin.SnykBulkFileListener
import io.snyk.plugin.SnykFile
import io.snyk.plugin.getSnykCachedResults
import io.snyk.plugin.toLanguageServerURL
import io.snyk.plugin.toSnykFileSet
import org.eclipse.lsp4j.DidSaveTextDocumentParams
import org.eclipse.lsp4j.TextDocumentIdentifier
import snyk.common.lsp.LanguageServerWrapper
import java.time.Duration

class SnykCodeBulkFileListener : SnykBulkFileListener() {
// Cache for debouncing file updates that come in within one second of the last
// Key = path, Value is irrelevant
private val debounceFileCache =
CacheBuilder.newBuilder()
.expireAfterWrite(Duration.ofMillis(1000)).build<String, Boolean>()

override fun before(project: Project, virtualFilesAffected: Set<VirtualFile>) = Unit

override fun after(project: Project, virtualFilesAffected: Set<VirtualFile>) {
val filesAffected = toSnykFileSet(project, virtualFilesAffected)
updateCacheAndUI(filesAffected, project)
}

override fun forwardEvents(events: MutableList<out VFileEvent>) {
val languageServerWrapper = LanguageServerWrapper.getInstance()

if (!languageServerWrapper.isInitialized) return

val languageServer = languageServerWrapper.languageServer
for (event in events) {
if (event.file == null || !event.isFromSave) continue
val file = event.file!!
val activeProject = ProjectUtil.getActiveProject()
ProgressManager.getInstance().run(object : Backgroundable(
activeProject,
"Snyk: forwarding save event to Language Server"
) {
override fun run(indicator: ProgressIndicator) {
if (virtualFilesAffected.isEmpty()) return
ProgressManager.getInstance().run(object : Backgroundable(
project,
"Snyk: forwarding save event to Language Server"
) {
override fun run(indicator: ProgressIndicator) {
val languageServerWrapper = LanguageServerWrapper.getInstance()
if (!languageServerWrapper.isInitialized) return
val languageServer = languageServerWrapper.languageServer
val cache = getSnykCachedResults(project)?.currentSnykCodeResultsLS ?: return
val filesAffected = toSnykFileSet(project, virtualFilesAffected)
for (file in filesAffected) {
val virtualFile = file.virtualFile
if (!shouldProcess(virtualFile)) continue
cache.remove(file)
val param =
DidSaveTextDocumentParams(TextDocumentIdentifier(file.toLanguageServerURL()), file.readText())
DidSaveTextDocumentParams(
TextDocumentIdentifier(virtualFile.toLanguageServerURL()),
virtualFile.readText()
)
languageServer.textDocumentService.didSave(param)
}
})
}

VirtualFileManager.getInstance().asyncRefresh()
runInEdt { DaemonCodeAnalyzer.getInstance(project).restart() }
}
})

}

private fun updateCacheAndUI(filesAffected: Set<SnykFile>, project: Project) {
val cache = getSnykCachedResults(project)?.currentSnykCodeResultsLS ?: return
filesAffected.forEach {
cache.remove(it)
private fun shouldProcess(file: VirtualFile): Boolean {
val inCache = debounceFileCache.getIfPresent(file.path)

return if (inCache != null) {
logger<SnykCodeBulkFileListener>().info("not forwarding file event to ls, debouncing")
false
} else {
debounceFileCache.put(file.path, true)
logger<SnykCodeBulkFileListener>().info("forwarding file event to ls, not debouncing")
true
}
VirtualFileManager.getInstance().asyncRefresh()
DaemonCodeAnalyzer.getInstance(project).restart()
}

override fun forwardEvents(events: MutableList<out VFileEvent>) = Unit
}
19 changes: 11 additions & 8 deletions src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package snyk.common.lsp
import com.google.common.util.concurrent.CycleDetectingLockFactory
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.util.io.toNioPathOrNull
Expand Down Expand Up @@ -263,8 +264,10 @@ class LanguageServerWrapper(

fun sendScanCommand(project: Project) {
if (!ensureLanguageServerInitialized()) return
getTrustedContentRoots(project).forEach {
sendFolderScanCommand(it.path)
DumbService.getInstance(project).runWhenSmart {
getTrustedContentRoots(project).forEach {
sendFolderScanCommand(it.path)
}
}
}

Expand Down Expand Up @@ -325,12 +328,12 @@ class LanguageServerWrapper(
cliPath = getCliFile().absolutePath,
token = ps.token,
filterSeverity =
SeverityFilter(
critical = ps.criticalSeverityEnabled,
high = ps.highSeverityEnabled,
medium = ps.mediumSeverityEnabled,
low = ps.lowSeverityEnabled,
),
SeverityFilter(
critical = ps.criticalSeverityEnabled,
high = ps.highSeverityEnabled,
medium = ps.mediumSeverityEnabled,
low = ps.lowSeverityEnabled,
),
enableTrustedFoldersFeature = "false",
scanningMode = if (!ps.scanOnSave) "manual" else "auto",
integrationName = pluginInfo.integrationName,
Expand Down
111 changes: 53 additions & 58 deletions src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.LogLevel
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
Expand Down Expand Up @@ -47,18 +46,16 @@ import org.eclipse.lsp4j.WorkDoneProgressNotification
import org.eclipse.lsp4j.WorkDoneProgressReport
import org.eclipse.lsp4j.jsonrpc.services.JsonNotification
import org.eclipse.lsp4j.services.LanguageClient
import org.jetbrains.concurrency.runAsync
import org.jetbrains.kotlin.idea.util.application.executeOnPooledThread
import snyk.common.ProductType
import snyk.common.SnykFileIssueComparator
import snyk.trust.WorkspaceTrustService
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock

class SnykLanguageClient() : LanguageClient {
private val progressLock: ReentrantLock = ReentrantLock()

// TODO FIX Log Level
val logger = Logger.getInstance("Snyk Language Server")

Expand Down Expand Up @@ -199,7 +196,7 @@ class SnykLanguageClient() : LanguageClient {

@Suppress("UselessCallOnNotNull") // because lsp4j doesn't care about Kotlin non-null safety
private fun getSnykResult(project: Project, snykScan: SnykScanParams): Map<SnykFile, List<ScanIssue>> {
check(snykScan.product == "code" || snykScan.product == "oss" ) { "Expected Snyk Code or Snyk OSS scan result" }
check(snykScan.product == "code" || snykScan.product == "oss") { "Expected Snyk Code or Snyk OSS scan result" }
if (snykScan.issues.isNullOrEmpty()) return emptyMap()

val pluginSettings = pluginSettings()
Expand Down Expand Up @@ -251,77 +248,75 @@ class SnykLanguageClient() : LanguageClient {
}

private fun createProgressInternal(token: String, begin: WorkDoneProgressBegin) {
ProgressManager.getInstance().run(object : Task.Backgroundable(ProjectUtil.getActiveProject(), "Snyk: ${begin.title}", true) {
override fun run(indicator: ProgressIndicator) {
logger.debug("###### Creating progress indicator for: $token, title: ${begin.title}, message: ${begin.message}")
indicator.isIndeterminate = false
indicator.text = begin.title
indicator.text2 = begin.message
indicator.fraction = 0.1
progresses.put(token, indicator)
while (!indicator.isCanceled) {
Thread.sleep(1000)
ProgressManager.getInstance()
.run(object : Task.Backgroundable(ProjectUtil.getActiveProject(), "Snyk: ${begin.title}", true) {
override fun run(indicator: ProgressIndicator) {
logger.debug("Creating progress indicator for: $token, title: ${begin.title}, message: ${begin.message}")
indicator.isIndeterminate = false
indicator.text = begin.title
indicator.text2 = begin.message
indicator.fraction = 0.1
progresses.put(token, indicator)
while (!indicator.isCanceled) {
Thread.sleep(1000)
}
logger.debug("Progress indicator canceled for token: $token")
}
logger.debug("###### Progress indicator canceled for token: $token")
}
})
})
}

override fun notifyProgress(params: ProgressParams) {
// first: check if progress has begun
val token = params.token?.left ?: return
if (progresses.getIfPresent(token) != null) {
processProgress(params)
} else {
when (val progressNotification = params.value.left) {
is WorkDoneProgressEnd -> {
progressEndMsgCache.put(token, progressNotification)
}
runAsync {
val token = params.token?.left ?: return@runAsync
if (progresses.getIfPresent(token) != null) {
processProgress(params)
} else {
when (val progressNotification = params.value.left) {
is WorkDoneProgressEnd -> {
progressEndMsgCache.put(token, progressNotification)
}

is WorkDoneProgressReport -> {
val list = progressReportMsgCache.get(token) { mutableListOf() }
list.add(progressNotification)
}
is WorkDoneProgressReport -> {
val list = progressReportMsgCache.get(token) { mutableListOf() }
list.add(progressNotification)
}

else -> {
processProgress(params)
else -> {
processProgress(params)
}
}
return@runAsync
}
return
}
}

private fun processProgress(params: ProgressParams?) {
progressLock.lock()
try {
val token = params?.token?.left ?: return
val workDoneProgressNotification = params.value.left ?: return
when (workDoneProgressNotification.kind) {
begin -> {
val begin: WorkDoneProgressBegin = workDoneProgressNotification as WorkDoneProgressBegin
createProgressInternal(token, begin)
// wait until the progress indicator is created in the background thread
while (progresses.getIfPresent(token) == null) {
Thread.sleep(100)
}

// process previously reported progress and end messages for token
processCachedProgressReports(token)
processCachedEndReport(token)
val token = params?.token?.left ?: return
val workDoneProgressNotification = params.value.left ?: return
when (workDoneProgressNotification.kind) {
begin -> {
val begin: WorkDoneProgressBegin = workDoneProgressNotification as WorkDoneProgressBegin
createProgressInternal(token, begin)
// wait until the progress indicator is created in the background thread
while (progresses.getIfPresent(token) == null) {
Thread.sleep(100)
}

report -> {
progressReport(token, workDoneProgressNotification)
}
// process previously reported progress and end messages for token
processCachedProgressReports(token)
processCachedEndReport(token)
}

end -> {
progressEnd(token, workDoneProgressNotification)
}
report -> {
progressReport(token, workDoneProgressNotification)
}

null -> {}
end -> {
progressEnd(token, workDoneProgressNotification)
}
} finally {
progressLock.unlock()

null -> {}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/snyk/common/lsp/Types.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@file:Suppress("unused", "SENSELESS_COMPARISON")
@file:Suppress("unused", "SENSELESS_COMPARISON", "UNNECESSARY_SAFE_CALL", "USELESS_ELVIS", "DuplicatedCode")

package snyk.common.lsp

Expand Down

0 comments on commit 1ee7f6b

Please sign in to comment.