Skip to content

Commit cf827a6

Browse files
chore: freeze & shutdown improvements (#575)
* fix: progress scanning * fix: ui freezes & display * fix: wait for ls initialization to retrieve active user add ff convenience method to keep things dry * fix: update feature flags when retrieving new token * fix: update jackson because of vulnerability * fix: better disposing/shutdown handling * fix: better shutdown * fix: better error logging (ignore stream closed exception) * docs: update CHANGELOG.md * chore: make annotators disposable * chore: remove login method (for now) * fix: init message * fix: ignore exceptions on shutdown * fix: move navigation to background thread * fix: use readactions and async where necessary to de-freeze UI * fix: warnings
1 parent 576936c commit cf827a6

24 files changed

+300
-148
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Snyk Security Changelog
22

3+
## [2.8.11]
4+
### Added
5+
- Improved UI thread usage and app shutdown handling
6+
37
## [2.8.9]
48
### Added
59
- Updated Open Source, Containers and IaC products to include `.snyk` in the list of supported build files.

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ dependencies {
4242
implementation("com.squareup.retrofit2:retrofit")
4343
implementation("com.squareup.okhttp3:okhttp")
4444
implementation("com.squareup.okhttp3:logging-interceptor")
45-
implementation("com.fasterxml.jackson.core:jackson-databind:2.12.7.1")
45+
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.0")
4646
implementation("org.json:json:20231013")
4747
implementation("org.slf4j:slf4j-api:2.0.5")
4848
implementation("ly.iterative.itly:plugin-iteratively:1.2.11") {

src/main/kotlin/io/snyk/plugin/Utils.kt

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.intellij.openapi.Disposable
99
import com.intellij.openapi.application.ApplicationManager
1010
import com.intellij.openapi.application.PathManager
1111
import com.intellij.openapi.application.ReadAction
12+
import com.intellij.openapi.application.invokeLater
1213
import com.intellij.openapi.application.runReadAction
1314
import com.intellij.openapi.components.service
1415
import com.intellij.openapi.diagnostic.Logger
@@ -44,6 +45,7 @@ import io.snyk.plugin.ui.SnykBalloonNotificationHelper
4445
import io.snyk.plugin.ui.toolwindow.SnykToolWindowFactory
4546
import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel
4647
import org.apache.commons.lang3.SystemUtils
48+
import org.jetbrains.concurrency.runAsync
4749
import snyk.advisor.AdvisorService
4850
import snyk.advisor.AdvisorServiceImpl
4951
import snyk.advisor.SnykAdvisorModel
@@ -197,7 +199,7 @@ fun isOssRunning(project: Project): Boolean {
197199
(indicator != null && indicator.isRunning && !indicator.isCanceled)
198200
}
199201

200-
fun cancelOss(project: Project) {
202+
fun cancelOssIndicator(project: Project) {
201203
val indicator = getSnykTaskQueueService(project)?.ossScanProgressIndicator
202204
indicator?.cancel()
203205
}
@@ -234,6 +236,7 @@ fun startSastEnablementCheckLoop(parentDisposable: Disposable, onSuccess: () ->
234236
var currentAttempt = 1
235237
val maxAttempts = 20
236238
lateinit var checkIfSastEnabled: () -> Unit
239+
// TODO use ls
237240
checkIfSastEnabled = {
238241
if (settings.sastOnServerEnabled != true) {
239242
settings.sastOnServerEnabled = try {
@@ -344,30 +347,40 @@ fun navigateToSource(
344347
project: Project,
345348
virtualFile: VirtualFile,
346349
selectionStartOffset: Int,
347-
selectionEndOffset: Int? = null
350+
selectionEndOffset: Int? = null,
348351
) {
349-
if (!virtualFile.isValid) return
350-
val textLength = virtualFile.contentsToByteArray().size
351-
if (selectionStartOffset in (0 until textLength)) {
352-
// jump to Source
353-
PsiNavigationSupport.getInstance().createNavigatable(
354-
project,
355-
virtualFile,
356-
selectionStartOffset
357-
).navigate(false)
358-
} else {
359-
logger.warn("Navigation to wrong offset: $selectionStartOffset with file length=$textLength")
360-
}
361-
362-
if (selectionEndOffset != null) {
363-
// highlight(by selection) suggestion range in source file
364-
if (selectionEndOffset in (0 until textLength) &&
365-
selectionStartOffset < selectionEndOffset
366-
) {
367-
val editor = FileEditorManager.getInstance(project).selectedTextEditor
368-
editor?.selectionModel?.setSelection(selectionStartOffset, selectionEndOffset)
352+
runAsync {
353+
if (!virtualFile.isValid) return@runAsync
354+
val textLength = virtualFile.contentsToByteArray().size
355+
if (selectionStartOffset in (0 until textLength)) {
356+
// jump to Source
357+
val navigatable =
358+
PsiNavigationSupport.getInstance().createNavigatable(
359+
project,
360+
virtualFile,
361+
selectionStartOffset,
362+
)
363+
invokeLater {
364+
if (navigatable.canNavigateToSource()) {
365+
navigatable.navigate(false)
366+
}
367+
}
369368
} else {
370-
logger.warn("Selection of wrong range: [$selectionStartOffset:$selectionEndOffset]")
369+
logger.warn("Navigation to wrong offset: $selectionStartOffset with file length=$textLength")
370+
}
371+
372+
if (selectionEndOffset != null) {
373+
// highlight(by selection) suggestion range in source file
374+
if (selectionEndOffset in (0 until textLength) &&
375+
selectionStartOffset < selectionEndOffset
376+
) {
377+
invokeLater {
378+
val editor = FileEditorManager.getInstance(project).selectedTextEditor
379+
editor?.selectionModel?.setSelection(selectionStartOffset, selectionEndOffset)
380+
}
381+
} else {
382+
logger.warn("Selection of wrong range: [$selectionStartOffset:$selectionEndOffset]")
383+
}
371384
}
372385
}
373386
}

src/main/kotlin/io/snyk/plugin/net/TokenInterceptor.kt

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ package io.snyk.plugin.net
22

33
import com.google.gson.Gson
44
import com.intellij.openapi.project.ProjectManager
5-
import io.snyk.plugin.getSnykCliAuthenticationService
65
import io.snyk.plugin.getUserAgentString
7-
import io.snyk.plugin.getWhoamiService
86
import io.snyk.plugin.pluginSettings
97
import okhttp3.Interceptor
108
import okhttp3.Response
9+
import snyk.common.lsp.LanguageServerWrapper
1110
import snyk.common.needsSnykToken
1211
import snyk.pluginInfo
1312
import java.time.OffsetDateTime
@@ -31,12 +30,11 @@ class TokenInterceptor(private var projectManager: ProjectManager? = null) : Int
3130
if (projectManager == null) {
3231
projectManager = ProjectManager.getInstance()
3332
}
34-
val project = projectManager?.openProjects!!.firstOrNull()
35-
val oAuthToken = Gson().fromJson(token, OAuthToken::class.java)
36-
val expiry = OffsetDateTime.parse(oAuthToken.expiry)
33+
// when the token is about to expire, call the whoami workflow to refresh it
34+
val oAuthToken = Gson().fromJson(token, OAuthToken::class.java) ?: return chain.proceed(request.build())
35+
val expiry = OffsetDateTime.parse(oAuthToken.expiry!!)
3736
if (expiry.isBefore(OffsetDateTime.now().plusMinutes(2))) {
38-
getWhoamiService(project)?.execute()
39-
getSnykCliAuthenticationService(project)?.executeGetConfigApiCommand()
37+
LanguageServerWrapper.getInstance().getAuthenticatedUser()
4038
}
4139
request.addHeader(authorizationHeaderName, "Bearer ${oAuthToken.access_token}")
4240
request.addHeader(oldSnykCodeHeaderName, "Bearer ${oAuthToken.access_token}")

src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import com.intellij.openapi.progress.BackgroundTaskQueue
99
import com.intellij.openapi.progress.ProgressIndicator
1010
import com.intellij.openapi.progress.Task
1111
import com.intellij.openapi.project.Project
12-
import io.snyk.plugin.cancelOss
12+
import io.snyk.plugin.cancelOssIndicator
1313
import io.snyk.plugin.events.SnykCliDownloadListener
1414
import io.snyk.plugin.events.SnykScanListener
1515
import io.snyk.plugin.events.SnykSettingsListener
@@ -295,7 +295,7 @@ class SnykTaskQueueService(val project: Project) {
295295

296296
fun stopScan() {
297297
val wasOssRunning = isOssRunning(project)
298-
cancelOss(project)
298+
cancelOssIndicator(project)
299299

300300
val wasSnykCodeRunning = isSnykCodeRunning(project)
301301

src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,11 @@ class SnykProjectSettingsConfigurable(val project: Project) : SearchableConfigur
9696

9797
runBackgroundableTask("Updating Snyk Code settings", project, true) {
9898
settingsStateService.isGlobalIgnoresFeatureEnabled =
99-
LanguageServerWrapper.getInstance().getFeatureFlagStatus("snykCodeConsistentIgnores")
100-
}
99+
LanguageServerWrapper.getInstance().isGlobalIgnoresFeatureEnabled()
101100

102-
if (snykSettingsDialog.getCliReleaseChannel().trim() != pluginSettings().cliReleaseChannel) {
103-
handleReleaseChannelChanged()
101+
if (snykSettingsDialog.getCliReleaseChannel().trim() != pluginSettings().cliReleaseChannel) {
102+
handleReleaseChannelChanged()
103+
}
104104
}
105105

106106
if (rescanNeeded) {

src/main/kotlin/io/snyk/plugin/ui/jcef/OpenFileLoadHandlerGenerator.kt

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.snyk.plugin.ui.jcef
22

3-
import com.intellij.openapi.application.ApplicationManager
43
import com.intellij.openapi.editor.colors.EditorColorsManager
54
import com.intellij.openapi.project.Project
65
import com.intellij.openapi.vfs.VirtualFile
@@ -26,25 +25,18 @@ class OpenFileLoadHandlerGenerator(
2625

2726
val virtualFile = virtualFiles[filePath] ?: return JBCefJSQuery.Response("success")
2827

29-
ApplicationManager.getApplication().invokeLater {
30-
val document = virtualFile.getDocument()
31-
val startLineStartOffset = document?.getLineStartOffset(startLine) ?: 0
32-
val startOffset = startLineStartOffset + (startCharacter)
33-
val endLineStartOffset = document?.getLineStartOffset(endLine) ?: 0
34-
val endOffset = endLineStartOffset + endCharacter - 1
35-
36-
navigateToSource(project, virtualFile, startOffset, endOffset)
37-
}
28+
val document = virtualFile.getDocument()
29+
val startLineStartOffset = document?.getLineStartOffset(startLine) ?: 0
30+
val startOffset = startLineStartOffset + (startCharacter)
31+
val endLineStartOffset = document?.getLineStartOffset(endLine) ?: 0
32+
val endOffset = endLineStartOffset + endCharacter - 1
3833

34+
navigateToSource(project, virtualFile, startOffset, endOffset)
3935
return JBCefJSQuery.Response("success")
4036
}
4137

4238
fun generate(jbCefBrowser: JBCefBrowserBase): CefLoadHandlerAdapter {
4339
val openFileQuery = JBCefJSQuery.create(jbCefBrowser)
44-
val isDarkTheme = EditorColorsManager.getInstance().isDarkEditor
45-
val isHighContrast =
46-
EditorColorsManager.getInstance().globalScheme.name.contains("High contrast", ignoreCase = true)
47-
4840
openFileQuery.addHandler { openFile(it) }
4941

5042
return object : CefLoadHandlerAdapter() {

src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykPluginDisposable.kt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
package io.snyk.plugin.ui.toolwindow
22

3+
import com.intellij.ide.AppLifecycleListener
34
import com.intellij.openapi.Disposable
45
import com.intellij.openapi.application.ApplicationManager
56
import com.intellij.openapi.components.Service
67
import com.intellij.openapi.project.Project
78
import org.jetbrains.annotations.NotNull
9+
import snyk.common.lsp.LanguageServerWrapper
10+
import java.util.concurrent.TimeUnit
811

912

1013
/**
1114
* Top-Level disposable for the Snyk plugin.
1215
*/
1316
@Service(Service.Level.APP, Service.Level.PROJECT)
14-
class SnykPluginDisposable : Disposable {
17+
class SnykPluginDisposable : Disposable, AppLifecycleListener {
1518
companion object {
1619
@NotNull
1720
fun getInstance(): Disposable {
@@ -24,5 +27,26 @@ class SnykPluginDisposable : Disposable {
2427
}
2528
}
2629

30+
init {
31+
ApplicationManager.getApplication().messageBus.connect().subscribe(AppLifecycleListener.TOPIC, this)
32+
}
33+
34+
override fun appClosing() {
35+
try {
36+
LanguageServerWrapper.getInstance().shutdown().get(2, TimeUnit.SECONDS)
37+
} catch (ignored: Exception) {
38+
// do nothing
39+
}
40+
}
41+
42+
override fun appWillBeClosed(isRestart: Boolean) {
43+
try {
44+
LanguageServerWrapper.getInstance().shutdown().get(2, TimeUnit.SECONDS)
45+
} catch (ignored: Exception) {
46+
// do nothing
47+
}
48+
}
49+
2750
override fun dispose() = Unit
51+
2852
}

src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.snyk.plugin.ui.toolwindow
33
import com.intellij.notification.NotificationAction
44
import com.intellij.openapi.Disposable
55
import com.intellij.openapi.application.ApplicationManager
6+
import com.intellij.openapi.application.ReadAction
67
import com.intellij.openapi.components.Service
78
import com.intellij.openapi.diagnostic.Logger
89
import com.intellij.openapi.project.Project
@@ -71,6 +72,7 @@ import io.snyk.plugin.ui.toolwindow.panels.StatePanel
7172
import io.snyk.plugin.ui.toolwindow.panels.TreePanel
7273
import io.snyk.plugin.ui.wrapWithScrollPane
7374
import org.jetbrains.annotations.TestOnly
75+
import org.jetbrains.concurrency.runAsync
7476
import snyk.analytics.AnalysisIsTriggered
7577
import snyk.analytics.WelcomeIsViewed
7678
import snyk.analytics.WelcomeIsViewed.Ide.JETBRAINS
@@ -578,8 +580,9 @@ class SnykToolWindowPanel(
578580
) {
579581
val settings = pluginSettings()
580582

581-
val realError = getSnykCachedResults(project)?.currentOssError != null
582-
&& ossResultsCount != NODE_NOT_SUPPORTED_STATE
583+
val realError =
584+
getSnykCachedResults(project)?.currentOssError != null &&
585+
ossResultsCount != NODE_NOT_SUPPORTED_STATE
583586

584587
val newOssTreeNodeText =
585588
when {
@@ -594,7 +597,7 @@ class SnykToolWindowPanel(
594597
count == 0 -> NO_ISSUES_FOUND_TEXT
595598
count > 0 -> ProductType.OSS.getCountText(count, isUniqueCount = true) + addHMLPostfix
596599
count == NODE_NOT_SUPPORTED_STATE -> NO_SUPPORTED_PACKAGE_MANAGER_FOUND
597-
else -> throw IllegalStateException("ResultsCount is meaningful")
600+
else -> throw IllegalStateException("ResultsCount is not meaningful")
598601
}
599602
}
600603
}
@@ -603,9 +606,7 @@ class SnykToolWindowPanel(
603606
val newSecurityIssuesNodeText =
604607
when {
605608
getSnykCachedResults(project)?.currentSnykCodeError != null -> "$CODE_SECURITY_ROOT_TEXT (error)"
606-
isSnykCodeRunning(
607-
project,
608-
) &&
609+
isSnykCodeRunning(project) &&
609610
settings.snykCodeSecurityIssuesScanEnable -> "$CODE_SECURITY_ROOT_TEXT (scanning...)"
610611

611612
else ->
@@ -747,19 +748,30 @@ class SnykToolWindowPanel(
747748
vulnerability: Vulnerability?,
748749
): () -> Unit =
749750
{
750-
val virtualFile = VirtualFileManager.getInstance().findFileByNioPath(Paths.get(filePath))
751-
if (virtualFile != null && virtualFile.isValid) {
751+
runAsync {
752+
var virtualFile: VirtualFile? = null
753+
ReadAction.run<RuntimeException> {
754+
virtualFile = VirtualFileManager.getInstance().findFileByNioPath(Paths.get(filePath))
755+
}
756+
val vf = virtualFile
757+
if (vf == null || !vf.isValid) {
758+
return@runAsync
759+
}
760+
752761
if (vulnerability == null) {
753-
navigateToSource(project, virtualFile, 0)
762+
navigateToSource(project, vf, 0)
754763
} else {
755-
val psiFile = PsiManager.getInstance(project).findFile(virtualFile)
756-
val textRange = psiFile?.let { getOssTextRangeFinderService().findTextRange(it, vulnerability) }
757-
navigateToSource(
758-
project = project,
759-
virtualFile = virtualFile,
760-
selectionStartOffset = textRange?.startOffset ?: 0,
761-
selectionEndOffset = textRange?.endOffset,
762-
)
764+
ReadAction.run<RuntimeException> {
765+
val psiFile = PsiManager.getInstance(project).findFile(vf)
766+
val textRange =
767+
psiFile?.let { getOssTextRangeFinderService().findTextRange(it, vulnerability) }
768+
navigateToSource(
769+
project = project,
770+
virtualFile = vf,
771+
selectionStartOffset = textRange?.startOffset ?: 0,
772+
selectionEndOffset = textRange?.endOffset,
773+
)
774+
}
763775
}
764776
}
765777
}

src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.intellij.openapi.vfs.VirtualFile
99
import com.intellij.util.ui.tree.TreeUtil
1010
import io.snyk.plugin.Severity
1111
import io.snyk.plugin.SnykFile
12+
import io.snyk.plugin.cancelOssIndicator
1213
import io.snyk.plugin.events.SnykScanListenerLS
1314
import io.snyk.plugin.getSnykCachedResults
1415
import io.snyk.plugin.pluginSettings
@@ -25,9 +26,8 @@ import io.snyk.plugin.ui.toolwindow.nodes.root.RootOssTreeNode
2526
import io.snyk.plugin.ui.toolwindow.nodes.root.RootQualityIssuesTreeNode
2627
import io.snyk.plugin.ui.toolwindow.nodes.root.RootSecurityIssuesTreeNode
2728
import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.InfoTreeNode
28-
import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.SnykCodeFileTreeNode
29+
import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.SnykFileTreeNode
2930
import snyk.common.ProductType
30-
import snyk.common.lsp.CliError
3131
import snyk.common.lsp.ScanIssue
3232
import snyk.common.lsp.SnykScanParams
3333
import javax.swing.JTree
@@ -78,6 +78,7 @@ class SnykToolWindowSnykScanListenerLS(
7878
override fun scanningOssFinished(snykResults: Map<SnykFile, List<ScanIssue>>) {
7979
if (disposed) return
8080
ApplicationManager.getApplication().invokeLater {
81+
cancelOssIndicator(project)
8182
this.snykToolWindowPanel.navigateToSourceEnabled = false
8283
displayOssResults(snykResults)
8384
refreshAnnotationsForOpenFiles(project)
@@ -341,7 +342,7 @@ class SnykToolWindowSnykScanListenerLS(
341342
}
342343

343344
val fileTreeNode =
344-
SnykCodeFileTreeNode(entry, productType)
345+
SnykFileTreeNode(entry, productType)
345346
rootNode.add(fileTreeNode)
346347
entry.value.sortedByDescending { it.priority() }
347348
.forEach { issue ->

0 commit comments

Comments
 (0)