From 5282559023d2d1987bd893ea78f1df585ff553e9 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Tue, 2 Apr 2024 09:48:34 +0200 Subject: [PATCH] fix: update code vision after scan, fix multi-project scan, fix parallel resource contention [IDE-204] (#494) * fix: progress, refactor ui updates, content roots and such * fix: container bulk file listener test * fix: update code visions after scan * fix: UI freezes, code vision refreshes * fix: run more UI refreshes async & in read action * fix: auto-scan after config change, subscribe to config changes * fix: don't trigger LS initialization from bulk file listener * chore: optimize imports * fix: avoid recursion during initialization fix: avoid recursion during initialization * fix: changelog merge error --- CHANGELOG.md | 7 + .../snyk/plugin/SnykProjectManagerListener.kt | 2 +- src/main/kotlin/io/snyk/plugin/Utils.kt | 21 +- .../plugin/services/SnykTaskQueueService.kt | 31 ++- .../services/download/CliDownloaderService.kt | 2 +- .../SnykProjectSettingsConfigurable.kt | 7 +- .../snykcode/SnykCodeBulkFileListener.kt | 9 +- .../ui/toolwindow/SnykToolWindowPanel.kt | 3 +- src/main/kotlin/snyk/WelcomeNotifyActivity.kt | 1 - .../code/annotator/SnykCodeAnnotatorLS.kt | 2 +- .../kotlin/snyk/common/AnnotatorCommon.kt | 5 +- .../kotlin/snyk/common/CustomEndpoints.kt | 2 +- .../snyk/common/lsp/LSCodeVisionProvider.kt | 2 +- .../snyk/common/lsp/LanguageServerWrapper.kt | 65 +++-- .../snyk/common/lsp/SnykLanguageClient.kt | 253 ++++++++++++------ .../container/ContainerBulkFileListener.kt | 4 +- .../kotlin/snyk/iac/IacBulkFileListener.kt | 4 +- .../snyk/iac/IgnoreButtonActionListener.kt | 8 +- .../snyk/plugin/net/CliConfigServiceTest.kt | 4 - .../ContainerBulkFileListenerTest.kt | 1 + .../errorHandler/SentryErrorReporterTest.kt | 3 - 21 files changed, 278 insertions(+), 158 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab0e46699..d570272a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Snyk Security Changelog +## [2.7.13] +### Fixed +- (LS Preview) fix progress handling for Snyk Code scans +- (LS Preview) fix multi-project scanning for Snyk Code +- (LS Preview) fix auto-scan newly opened project, and ask for trust if needed +- (LS Preview) fix CodeVision for opened files + ## [2.7.12] ### Added - Mark ignored findings as ignored behind a feature flag. diff --git a/src/main/kotlin/io/snyk/plugin/SnykProjectManagerListener.kt b/src/main/kotlin/io/snyk/plugin/SnykProjectManagerListener.kt index c32fdcdc3..660f9317e 100644 --- a/src/main/kotlin/io/snyk/plugin/SnykProjectManagerListener.kt +++ b/src/main/kotlin/io/snyk/plugin/SnykProjectManagerListener.kt @@ -26,7 +26,7 @@ class SnykProjectManagerListener : ProjectManagerListener { AnalysisData.instance.removeProjectFromCaches(project) SnykCodeIgnoreInfoHolder.instance.removeProject(project) val ls = LanguageServerWrapper.getInstance() - if (ls.isInitialized) { + if (ls.ensureLanguageServerInitialized()) { ls.updateWorkspaceFolders(emptySet(), ls.getWorkspaceFolders(project)) } }.get(TIMEOUT, TimeUnit.SECONDS) diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index 2c44e2926..19b098589 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -2,6 +2,7 @@ package io.snyk.plugin +import com.intellij.codeInsight.codeVision.CodeVisionHost import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer import com.intellij.ide.util.PsiNavigationSupport import com.intellij.openapi.Disposable @@ -9,6 +10,7 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.PathManager import com.intellij.openapi.application.ReadAction import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.editor.Document import com.intellij.openapi.fileEditor.FileDocumentManager @@ -21,6 +23,7 @@ import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.util.registry.Registry import com.intellij.openapi.vfs.StandardFileSystems import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowManager import com.intellij.psi.PsiFile @@ -301,8 +304,15 @@ fun findPsiFileIgnoringExceptions(virtualFile: VirtualFile, project: Project): P fun refreshAnnotationsForOpenFiles(project: Project) { if (project.isDisposed) return + VirtualFileManager.getInstance().asyncRefresh() + val openFiles = FileEditorManager.getInstance(project).openFiles - FileContentUtil.reparseFiles(project, openFiles.asList(), true) + + ApplicationManager.getApplication().invokeLater { + FileContentUtil.reparseFiles(project, openFiles.asList(), true) + project.service().invalidateProvider(CodeVisionHost.LensInvalidateSignal(null)) + } + openFiles.forEach { val psiFile = findPsiFileIgnoringExceptions(it, project) if (psiFile != null) { @@ -416,8 +426,13 @@ fun getArch(): String { return archMap[value] ?: value } -fun String.toVirtualFile(): VirtualFile = - StandardFileSystems.local().refreshAndFindFileByPath(this) ?: throw FileNotFoundException(this) +fun String.toVirtualFile(): VirtualFile { + return if (!this.startsWith("file://")) { + StandardFileSystems.local().refreshAndFindFileByPath(this) ?: throw FileNotFoundException(this) + } else { + VirtualFileManager.getInstance().refreshAndFindFileByUrl(this) ?: throw FileNotFoundException(this) + } +} fun VirtualFile.getPsiFile(project: Project): PsiFile? { return runReadAction { PsiManager.getInstance(project).findFile(this) } diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt index 6da732987..7eadf0cbc 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt @@ -1,7 +1,7 @@ package io.snyk.plugin.services -import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.invokeLater import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.fileEditor.FileDocumentManager @@ -11,6 +11,7 @@ import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project import io.snyk.plugin.events.SnykCliDownloadListener import io.snyk.plugin.events.SnykScanListener +import io.snyk.plugin.events.SnykSettingsListener import io.snyk.plugin.events.SnykTaskQueueListener import io.snyk.plugin.getContainerService import io.snyk.plugin.getIacService @@ -19,6 +20,7 @@ import io.snyk.plugin.getSnykApiService import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.getSnykCliDownloaderService import io.snyk.plugin.getSnykCode +import io.snyk.plugin.getSnykToolWindowPanel import io.snyk.plugin.getSyncPublisher import io.snyk.plugin.isCliDownloading import io.snyk.plugin.isCliInstalled @@ -28,6 +30,7 @@ import io.snyk.plugin.isSnykCodeLSEnabled import io.snyk.plugin.isSnykCodeRunning import io.snyk.plugin.net.ClientException import io.snyk.plugin.pluginSettings +import io.snyk.plugin.refreshAnnotationsForOpenFiles import io.snyk.plugin.snykcode.core.RunUtils import io.snyk.plugin.ui.SnykBalloonNotificationHelper import io.snyk.plugin.ui.SnykBalloonNotifications @@ -77,9 +80,21 @@ class SnykTaskQueueService(val project: Project) { fun connectProjectToLanguageServer(project: Project) { synchronized(LanguageServerWrapper) { - val wrapper = LanguageServerWrapper.getInstance() - val added = wrapper.getWorkspaceFolders(project) - wrapper.updateWorkspaceFolders(added, emptySet()) + + // subscribe to the settings changed topic + getSnykToolWindowPanel(project)?.let { + project.messageBus.connect(it) + .subscribe( + SnykSettingsListener.SNYK_SETTINGS_TOPIC, + object : SnykSettingsListener { + override fun settingsChanged() { + val wrapper = LanguageServerWrapper.getInstance() + wrapper.updateConfiguration() + } + } + ) + } + LanguageServerWrapper.getInstance().addContentRoots(project) } } @@ -99,7 +114,7 @@ class SnykTaskQueueService(val project: Project) { if (!isSnykCodeLSEnabled()) { scheduleSnykCodeScan() } else { - LanguageServerWrapper.getInstance().sendScanCommand() + LanguageServerWrapper.getInstance().sendScanCommand(project) } } if (settings.ossScanEnable) { @@ -155,7 +170,7 @@ class SnykTaskQueueService(val project: Project) { } } logger.debug("Container scan completed") - DaemonCodeAnalyzer.getInstance(project).restart() + invokeLater { refreshAnnotationsForOpenFiles(project) } } }) } @@ -230,7 +245,7 @@ class SnykTaskQueueService(val project: Project) { ossResult.getFirstError()?.let { scanPublisher?.scanningOssError(it) } } } - DaemonCodeAnalyzer.getInstance(project).restart() + invokeLater { refreshAnnotationsForOpenFiles(project) } } }) } @@ -273,7 +288,7 @@ class SnykTaskQueueService(val project: Project) { } } logger.debug("IaC scan completed") - DaemonCodeAnalyzer.getInstance(project).restart() + invokeLater { refreshAnnotationsForOpenFiles(project) } } }) } diff --git a/src/main/kotlin/io/snyk/plugin/services/download/CliDownloaderService.kt b/src/main/kotlin/io/snyk/plugin/services/download/CliDownloaderService.kt index 5670a0f95..f02e4aa1c 100644 --- a/src/main/kotlin/io/snyk/plugin/services/download/CliDownloaderService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/download/CliDownloaderService.kt @@ -101,7 +101,7 @@ class SnykCliDownloaderService { } catch (e: ChecksumVerificationException) { errorHandler.handleChecksumVerificationException(e, indicator, project) } finally { - if (succeeded) languageServerWrapper.initialize() else stopCliDownload() + if (succeeded) languageServerWrapper.ensureLanguageServerInitialized() else stopCliDownload() } } finally { cliDownloadPublisher.cliDownloadFinished(succeeded) diff --git a/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt b/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt index 664dfb922..11802998b 100644 --- a/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt +++ b/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt @@ -21,7 +21,6 @@ import io.snyk.plugin.snykcode.core.SnykCodeParams import io.snyk.plugin.snykcode.newCodeRestApi import io.snyk.plugin.ui.SnykBalloonNotificationHelper import io.snyk.plugin.ui.SnykSettingsDialog -import org.eclipse.lsp4j.DidChangeConfigurationParams import snyk.amplitude.api.ExperimentUser import snyk.common.lsp.LanguageServerWrapper import snyk.common.toSnykCodeApiUrl @@ -99,14 +98,10 @@ class SnykProjectSettingsConfigurable(val project: Project) : SearchableConfigur snykProjectSettingsService?.additionalParameters = snykSettingsDialog.getAdditionalParameters() } - val wrapper = LanguageServerWrapper.getInstance() - val params = DidChangeConfigurationParams(wrapper.getSettings()) - wrapper.languageServer.workspaceService.didChangeConfiguration(params) - if (isSnykCodeLSEnabled()) { runBackgroundableTask("Updating Snyk Code settings", project, true) { settingsStateService.isGlobalIgnoresFeatureEnabled = - wrapper.getFeatureFlagStatus("snykCodeConsistentIgnores") + LanguageServerWrapper.getInstance().getFeatureFlagStatus("snykCodeConsistentIgnores") } } diff --git a/src/main/kotlin/io/snyk/plugin/snykcode/SnykCodeBulkFileListener.kt b/src/main/kotlin/io/snyk/plugin/snykcode/SnykCodeBulkFileListener.kt index aeb428439..4b0cca2cf 100644 --- a/src/main/kotlin/io/snyk/plugin/snykcode/SnykCodeBulkFileListener.kt +++ b/src/main/kotlin/io/snyk/plugin/snykcode/SnykCodeBulkFileListener.kt @@ -7,10 +7,10 @@ import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task.Backgroundable import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile +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.getPsiFile import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.getSnykTaskQueueService import io.snyk.plugin.isSnykCodeLSEnabled @@ -82,7 +82,7 @@ class SnykCodeBulkFileListener : SnykBulkFileListener() { val languageServerWrapper = LanguageServerWrapper.getInstance() if (!isSnykCodeLSEnabled()) return - if (!languageServerWrapper.ensureLanguageServerInitialized()) return + if (!languageServerWrapper.isInitialized) return val languageServer = languageServerWrapper.languageServer for (event in events) { @@ -105,10 +105,9 @@ class SnykCodeBulkFileListener : SnykBulkFileListener() { val cache = getSnykCachedResults(project)?.currentSnykCodeResultsLS ?: return filesAffected.forEach { cache.remove(it) - it.virtualFile.getPsiFile(project)?.let { psiFile -> - DaemonCodeAnalyzer.getInstance(project).restart(psiFile) - } } + VirtualFileManager.getInstance().asyncRefresh() + DaemonCodeAnalyzer.getInstance(project).restart() } private fun toSnykCodeFileSet(project: Project, virtualFiles: Set) = diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt index 7681a24d4..83207e65d 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt @@ -2,7 +2,6 @@ package io.snyk.plugin.ui.toolwindow import ai.deepcode.javaclient.core.MyTextRange import ai.deepcode.javaclient.core.SuggestionForFile -import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer import com.intellij.notification.NotificationAction import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager @@ -445,10 +444,10 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable { } } - DaemonCodeAnalyzer.getInstance(project).restart() ApplicationManager.getApplication().invokeLater { doCleanUi(true) + refreshAnnotationsForOpenFiles(project) } } diff --git a/src/main/kotlin/snyk/WelcomeNotifyActivity.kt b/src/main/kotlin/snyk/WelcomeNotifyActivity.kt index 495d8d2ec..f9a36cb9a 100644 --- a/src/main/kotlin/snyk/WelcomeNotifyActivity.kt +++ b/src/main/kotlin/snyk/WelcomeNotifyActivity.kt @@ -2,7 +2,6 @@ package snyk import com.intellij.openapi.project.Project import com.intellij.openapi.startup.ProjectActivity -import com.intellij.openapi.startup.StartupActivity import io.snyk.plugin.pluginSettings import io.snyk.plugin.services.SnykApplicationSettingsStateService import io.snyk.plugin.ui.SnykBalloonNotifications diff --git a/src/main/kotlin/snyk/code/annotator/SnykCodeAnnotatorLS.kt b/src/main/kotlin/snyk/code/annotator/SnykCodeAnnotatorLS.kt index f579be2f5..23852c6bf 100644 --- a/src/main/kotlin/snyk/code/annotator/SnykCodeAnnotatorLS.kt +++ b/src/main/kotlin/snyk/code/annotator/SnykCodeAnnotatorLS.kt @@ -185,7 +185,7 @@ class SnykCodeAnnotatorLS : ExternalAnnotator() { .resolveCodeAction(codeAction).get(TIMEOUT, TimeUnit.SECONDS) val edit = resolvedCodeAction.edit - if (edit.changes == null) return + if (edit == null || edit.changes == null) return changes = edit.changes } else { val codeActionCommand = resolvedCodeAction.command diff --git a/src/main/kotlin/snyk/common/AnnotatorCommon.kt b/src/main/kotlin/snyk/common/AnnotatorCommon.kt index 531f7b30e..651705f3d 100644 --- a/src/main/kotlin/snyk/common/AnnotatorCommon.kt +++ b/src/main/kotlin/snyk/common/AnnotatorCommon.kt @@ -1,5 +1,6 @@ package snyk.common +import com.intellij.openapi.application.invokeLater import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile @@ -33,7 +34,7 @@ object AnnotatorCommon { SnykProductsOrSeverityListener.SNYK_ENABLEMENT_TOPIC, object : SnykProductsOrSeverityListener { override fun enablementChanged() { - refreshAnnotationsForOpenFiles(project) + invokeLater { refreshAnnotationsForOpenFiles(project) } } } ) @@ -42,7 +43,7 @@ object AnnotatorCommon { SnykSettingsListener.SNYK_SETTINGS_TOPIC, object : SnykSettingsListener { override fun settingsChanged() { - refreshAnnotationsForOpenFiles(project) + invokeLater { refreshAnnotationsForOpenFiles(project) } } } ) diff --git a/src/main/kotlin/snyk/common/CustomEndpoints.kt b/src/main/kotlin/snyk/common/CustomEndpoints.kt index 45b65142a..af795f516 100644 --- a/src/main/kotlin/snyk/common/CustomEndpoints.kt +++ b/src/main/kotlin/snyk/common/CustomEndpoints.kt @@ -1,8 +1,8 @@ package snyk.common import io.snyk.plugin.pluginSettings -import io.snyk.plugin.suffixIfNot import io.snyk.plugin.prefixIfNot +import io.snyk.plugin.suffixIfNot import java.net.URI import java.net.URISyntaxException diff --git a/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt b/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt index f2ab2f91f..62e99dd75 100644 --- a/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt +++ b/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt @@ -46,7 +46,7 @@ class LSCodeVisionProvider : CodeVisionProvider { override fun computeCodeVision(editor: Editor, uiData: Unit): CodeVisionState { if (editor.project == null) return CodeVisionState.READY_EMPTY if (!isSnykCodeLSEnabled()) return CodeVisionState.READY_EMPTY - if (!LanguageServerWrapper.getInstance().ensureLanguageServerInitialized()) return CodeVisionState.READY_EMPTY + if (!LanguageServerWrapper.getInstance().isInitialized) return CodeVisionState.READY_EMPTY if (isSnykCodeRunning(editor.project!!)) return CodeVisionState.READY_EMPTY return ReadAction.compute { diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 99bbf38b3..ad040db31 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -1,6 +1,5 @@ package snyk.common.lsp -import com.intellij.ide.impl.ProjectUtil import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project @@ -21,6 +20,7 @@ import org.eclipse.lsp4j.CodeActionCapabilities import org.eclipse.lsp4j.CodeLensCapabilities import org.eclipse.lsp4j.CodeLensWorkspaceCapabilities import org.eclipse.lsp4j.DiagnosticCapabilities +import org.eclipse.lsp4j.DidChangeConfigurationParams import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams import org.eclipse.lsp4j.ExecuteCommandParams import org.eclipse.lsp4j.InitializeParams @@ -39,13 +39,14 @@ import snyk.common.getEndpointUrl import snyk.common.lsp.commands.ScanDoneEvent import snyk.pluginInfo import snyk.trust.WorkspaceTrustService +import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.Future import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.ReentrantLock import kotlin.io.path.exists -private const val DEFAULT_SLEEP_TIME = 100L private const val INITIALIZATION_TIMEOUT = 20L @Suppress("TooGenericExceptionCaught") @@ -76,24 +77,24 @@ class LanguageServerWrapper( */ lateinit var process: Process - private var isInitializing: Boolean = false + private var isInitializing: ReentrantLock = ReentrantLock() - val isInitialized: Boolean + internal val isInitialized: Boolean get() = ::languageClient.isInitialized && ::languageServer.isInitialized && ::process.isInitialized && process.info().startInstant().isPresent && - process.isAlive + process.isAlive && + !isInitializing.isLocked @OptIn(DelicateCoroutinesApi::class) - internal fun initialize() { + private fun initialize() { if (lsPath.toNioPathOrNull()?.exists() == false) { val message = "Snyk Language Server not found. Please make sure the Snyk CLI is installed at $lsPath." logger.warn(message) return } try { - isInitializing = true val snykLanguageClient = SnykLanguageClient() languageClient = snykLanguageClient val logLevel = if (snykLanguageClient.logger.isDebugEnabled) "debug" else "info" @@ -111,16 +112,15 @@ class LanguageServerWrapper( } launcher.startListening() - sendInitializeMessage() } catch (e: Exception) { logger.warn(e) - } finally { - isInitializing = false + process.destroy() } // update feature flags - pluginSettings().isGlobalIgnoresFeatureEnabled = getFeatureFlagStatus("snykCodeConsistentIgnores") + pluginSettings().isGlobalIgnoresFeatureEnabled = + getFeatureFlagStatusInternal("snykCodeConsistentIgnores") } fun shutdown(): Future<*> { @@ -143,6 +143,8 @@ class LanguageServerWrapper( } private fun getTrustedContentRoots(project: Project): MutableSet { + if (!confirmScanningAndSetWorkspaceTrustedStateIfNeeded(project)) return mutableSetOf() + // the sort is to ensure that parent folders come first // e.g. /a/b should come before /a/b/c val contentRoots = project.getContentRootVirtualFiles().filterNotNull().sortedBy { it.path } @@ -219,11 +221,13 @@ class LanguageServerWrapper( } fun ensureLanguageServerInitialized(): Boolean { - while (isInitializing) { - Thread.sleep(DEFAULT_SLEEP_TIME) - } if (!isInitialized) { - initialize() + try { + isInitializing.lock() + initialize() + } finally { + isInitializing.unlock() + } } return isInitialized } @@ -241,13 +245,8 @@ class LanguageServerWrapper( } } - fun sendScanCommand() { + fun sendScanCommand(project: Project) { if (!ensureLanguageServerInitialized()) return - val project = ProjectUtil.getActiveProject() - if (project == null) { - logger.warn("No active project found, not sending scan command.") - return - } getTrustedContentRoots(project).forEach { sendFolderScanCommand(it.path) } @@ -255,6 +254,10 @@ class LanguageServerWrapper( fun getFeatureFlagStatus(featureFlag: String): Boolean { ensureLanguageServerInitialized() + return getFeatureFlagStatusInternal(featureFlag) + } + + private fun getFeatureFlagStatusInternal(featureFlag: String): Boolean { if (!isSnykCodeLSEnabled()) { return false } @@ -269,17 +272,16 @@ class LanguageServerWrapper( val ok = resultMap?.get("ok") as? Boolean ?: false val userMessage = resultMap?.get("userMessage") as? String ?: "No message provided" - if (ok) { logger.info("Feature flag $featureFlag is enabled.") return true } else { - logger.warn("Feature flag $featureFlag is disabled. Message: $userMessage") + logger.info("Feature flag $featureFlag is disabled. Message: $userMessage") return false } } catch (e: Exception) { - logger.error("Error while checking feature flag: ${e.message}", e) + logger.warn("Error while checking feature flag: ${e.message}", e) return false } } @@ -320,6 +322,21 @@ class LanguageServerWrapper( ) } + fun updateConfiguration() { + ensureLanguageServerInitialized() + val params = DidChangeConfigurationParams(getInstance().getSettings()) + languageServer.workspaceService.didChangeConfiguration(params) + if (pluginSettings().scanOnSave) { + ProjectManager.getInstance().openProjects.forEach { sendScanCommand(it) } + } + } + + fun addContentRoots(project: Project) { + if (!isInitialized) return + val added = getWorkspaceFolders(project) + updateWorkspaceFolders(added, emptySet()) + } + companion object { private var INSTANCE: LanguageServerWrapper? = null fun getInstance() = INSTANCE ?: LanguageServerWrapper().also { INSTANCE = it } diff --git a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt index a9307457e..0dad73059 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -1,9 +1,12 @@ package snyk.common.lsp -import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer +import com.github.benmanes.caffeine.cache.Cache +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.RemovalListener import com.intellij.ide.impl.ProjectUtil import com.intellij.openapi.actionSystem.AnAction 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 @@ -12,13 +15,16 @@ import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectLocator import com.intellij.openapi.project.getOpenedProjects import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.openapi.vfs.VirtualFileManager import io.snyk.plugin.events.SnykCodeScanListenerLS import io.snyk.plugin.getContentRootVirtualFiles import io.snyk.plugin.getSyncPublisher import io.snyk.plugin.isSnykCodeLSEnabled import io.snyk.plugin.pluginSettings +import io.snyk.plugin.refreshAnnotationsForOpenFiles import io.snyk.plugin.snykcode.core.SnykCodeFile import io.snyk.plugin.toVirtualFile import io.snyk.plugin.ui.SnykBalloonNotificationHelper @@ -37,24 +43,37 @@ import org.eclipse.lsp4j.WorkDoneProgressEnd import org.eclipse.lsp4j.WorkDoneProgressKind.begin import org.eclipse.lsp4j.WorkDoneProgressKind.end import org.eclipse.lsp4j.WorkDoneProgressKind.report +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.kotlin.idea.util.application.executeOnPooledThread import snyk.common.ProductType import snyk.common.SnykCodeFileIssueComparator import snyk.trust.WorkspaceTrustService -import java.util.Collections import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.CompletableFuture -import java.util.concurrent.Future import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.ReentrantLock + +class SnykLanguageClient() : LanguageClient { + private val progressLock: ReentrantLock = ReentrantLock() -class SnykLanguageClient : LanguageClient { // TODO FIX Log Level val logger = Logger.getInstance("Snyk Language Server").also { it.setLevel(LogLevel.DEBUG) } - val progresses: MutableMap = - Collections.synchronizedMap(HashMap()) + + private val progresses: Cache = + Caffeine.newBuilder() + .expireAfterAccess(10, TimeUnit.SECONDS) + .removalListener( + RemovalListener { _, indicator, _ -> + indicator?.cancel() + } + ) + .build() + private val progressReportMsgCache: Cache> = + Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.SECONDS).build() + private val progressEndMsgCache: Cache = + Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.SECONDS).build() override fun telemetryEvent(`object`: Any?) { // do nothing @@ -65,9 +84,12 @@ class SnykLanguageClient : LanguageClient { } override fun applyEdit(params: ApplyWorkspaceEditParams?): CompletableFuture { - val project = ProjectUtil.getActiveProject() ?: return CompletableFuture.completedFuture( - ApplyWorkspaceEditResponse(false) - ) + val project = params?.edit?.changes?.keys + ?.firstNotNullOfOrNull { + ProjectLocator.getInstance().guessProjectForFile(it.toVirtualFile()) + } + ?: ProjectUtil.getActiveProject() + ?: return CompletableFuture.completedFuture(ApplyWorkspaceEditResponse(false)) WriteCommandAction.runWriteCommandAction(project) { params?.edit?.changes?.forEach { @@ -75,22 +97,27 @@ class SnykLanguageClient : LanguageClient { } } - DaemonCodeAnalyzer.getInstance(project).restart() + refreshUI() return CompletableFuture.completedFuture(ApplyWorkspaceEditResponse(true)) } override fun refreshCodeLenses(): CompletableFuture { - val activeProject = ProjectUtil.getActiveProject() ?: return CompletableFuture.completedFuture(null) - DaemonCodeAnalyzer.getInstance(activeProject).restart() - return CompletableFuture.completedFuture(null) + return refreshUI() } override fun refreshInlineValues(): CompletableFuture { + return refreshUI() + } + + private fun refreshUI(): CompletableFuture { val completedFuture: CompletableFuture = CompletableFuture.completedFuture(null) if (!isSnykCodeLSEnabled()) return completedFuture - val activeProject = ProjectUtil.getActiveProject() ?: return completedFuture - - DaemonCodeAnalyzer.getInstance(activeProject).restart() + ProjectUtil.getOpenProjects().forEach { project -> + ReadAction.run { + refreshAnnotationsForOpenFiles(project) + } + } + VirtualFileManager.getInstance().asyncRefresh() return completedFuture } @@ -106,11 +133,7 @@ class SnykLanguageClient : LanguageClient { } } - private fun processSnykScan( - snykScan: SnykScanParams, - scanPublisher: SnykCodeScanListenerLS, - project: Project - ) { + private fun processSnykScan(snykScan: SnykScanParams, scanPublisher: SnykCodeScanListenerLS, project: Project) { val product = when (snykScan.product) { "code" -> ProductType.CODE_SECURITY else -> return @@ -135,11 +158,7 @@ class SnykLanguageClient : LanguageClient { } } - private fun processSuccessfulScan( - snykScan: SnykScanParams, - scanPublisher: SnykCodeScanListenerLS, - project: Project - ) { + private fun processSuccessfulScan(snykScan: SnykScanParams, scanPublisher: SnykCodeScanListenerLS, project: Project) { logger.info("Scan completed") when (snykScan.product) { "oss" -> { @@ -161,13 +180,11 @@ class SnykLanguageClient : LanguageClient { * containing that content root, we need to notify all of them. */ private fun getScanPublishersFor(snykScan: SnykScanParams): Set> { - return getOpenedProjects() - .filter { - it.getContentRootVirtualFiles().map { virtualFile -> virtualFile.path }.contains(snykScan.folderPath) - } - .mapNotNull { p -> - getSyncPublisher(p, SnykCodeScanListenerLS.SNYK_SCAN_TOPIC)?.let { Pair(p, it) } - }.toSet() + return getOpenedProjects().filter { + it.getContentRootVirtualFiles().map { virtualFile -> virtualFile.path }.contains(snykScan.folderPath) + }.mapNotNull { p -> + getSyncPublisher(p, SnykCodeScanListenerLS.SNYK_SCAN_TOPIC)?.let { Pair(p, it) } + }.toSet() } @Suppress("UselessCallOnNotNull") // because lsp4j doesn't care about Kotlin non-null safety @@ -199,27 +216,13 @@ class SnykLanguageClient : LanguageClient { return map.toSortedMap(SnykCodeFileIssueComparator(map)) } - override fun createProgress(params: WorkDoneProgressCreateParams?): CompletableFuture { - return CompletableFuture.completedFuture(null) - } - - private fun createProgressInternal(token: String, begin: WorkDoneProgressBegin) { - ProgressManager.getInstance() - .run(object : Task.Backgroundable(ProjectUtil.getActiveProject(), "Snyk: ${begin.title}", true) { - override fun run(indicator: ProgressIndicator) { - progresses[token] = indicator - while (!indicator.isCanceled) { - Thread.sleep(100) - } - progresses.remove(token) - } - }) - } - @JsonNotification(value = "$/snyk.hasAuthenticated") fun hasAuthenticated(param: HasAuthenticatedParam) { pluginSettings().token = param.token - LanguageServerWrapper.getInstance().sendScanCommand() + + ProjectUtil.getOpenProjects().forEach { + LanguageServerWrapper.getInstance().sendScanCommand(it) + } if (!param.token.isNullOrBlank()) { SnykBalloonNotificationHelper.showInfo("Authentication successful", ProjectUtil.getActiveProject()!!) @@ -232,44 +235,125 @@ class SnykLanguageClient : LanguageClient { param.trustedFolders.forEach { it.toNioPathOrNull()?.let { path -> trustService.addTrustedPath(path) } } } - override fun notifyProgress(params: ProgressParams?) { - 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) - val indicator = progresses[token] ?: return + + override fun createProgress(params: WorkDoneProgressCreateParams?): CompletableFuture { + return CompletableFuture.completedFuture(null) + } + + 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 = begin.percentage / 100.0 + indicator.fraction = 0.1 + progresses.put(token, indicator) + while (!indicator.isCanceled) { + Thread.sleep(1000) + } + logger.debug("###### Progress indicator canceled for token: $token") } + }) + } - report -> { - executeOnPooledThread { while (progresses[token] == null) { Thread.sleep(1000) } }.get(5, TimeUnit.SECONDS) - val indicator = progresses[token] ?: return - val report: WorkDoneProgressReport = workDoneProgressNotification as WorkDoneProgressReport - indicator.text = report.message - indicator.isIndeterminate = false - indicator.fraction = report.percentage / 100.0 - if (report.percentage == 100) { - indicator.cancel() + 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) + } + + is WorkDoneProgressReport -> { + val list = progressReportMsgCache.get(token) { mutableListOf() } + list.add(progressNotification) + } + + else -> { + processProgress(params) } } + 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) + } + + report -> { + progressReport(token, workDoneProgressNotification) + } + + end -> { + progressEnd(token, workDoneProgressNotification) + } - end -> { - executeOnPooledThread { while (progresses[token] == null) { Thread.sleep(1000) } }.get(5, TimeUnit.SECONDS) - val indicator = progresses[token] ?: return - val workDoneProgressEnd = workDoneProgressNotification as WorkDoneProgressEnd - indicator.text = workDoneProgressEnd.message - indicator.cancel() + null -> {} } + } finally { + progressLock.unlock() + } + } - null -> {} + private fun processCachedEndReport(token: String) { + val endReport = progressEndMsgCache.getIfPresent(token) + if (endReport != null) { + progressEnd(token, endReport) + } + progressEndMsgCache.invalidate(token) + } + + private fun processCachedProgressReports(token: String) { + val reportParams = progressReportMsgCache.getIfPresent(token) + if (reportParams != null) { + reportParams.forEach { report -> + progressReport(token, report) + } + progressReportMsgCache.invalidate(token) } } + private fun progressReport(token: String, workDoneProgressNotification: WorkDoneProgressNotification) { + logger.debug("###### Received progress report notification for token: $token") + val indicator = progresses.getIfPresent(token)!! + val report: WorkDoneProgressReport = workDoneProgressNotification as WorkDoneProgressReport + logger.debug("###### Token: $token, progress: ${report.percentage}%, message: ${report.message}") + + indicator.text = report.message + indicator.isIndeterminate = false + indicator.fraction = report.percentage / 100.0 + return + } + + private fun progressEnd(token: String, workDoneProgressNotification: WorkDoneProgressNotification) { + logger.debug("###### Received progress end notification for token: $token") + val indicator = progresses.getIfPresent(token)!! + val workDoneProgressEnd = workDoneProgressNotification as WorkDoneProgressEnd + indicator.text = workDoneProgressEnd.message + progresses.invalidate(token) + return + } + override fun logTrace(params: LogTraceParams?) { logger.info(params?.message) } @@ -292,17 +376,16 @@ class SnykLanguageClient : LanguageClient { override fun showMessageRequest(requestParams: ShowMessageRequestParams): CompletableFuture { val project = ProjectUtil.getActiveProject() ?: return CompletableFuture.completedFuture(MessageActionItem("")) showMessageRequestFutures.clear() - val actions = requestParams.actions - .map { - object : AnAction(it.title) { - override fun actionPerformed(p0: AnActionEvent) { - showMessageRequestFutures.put(MessageActionItem(it.title)) - } + val actions = requestParams.actions.map { + object : AnAction(it.title) { + override fun actionPerformed(p0: AnActionEvent) { + showMessageRequestFutures.put(MessageActionItem(it.title)) } - }.toSet().toTypedArray() + } + }.toSet().toTypedArray() val notification = SnykBalloonNotificationHelper.showInfo(requestParams.message, project, *actions) - val messageActionItem = showMessageRequestFutures.poll(5, TimeUnit.SECONDS) + val messageActionItem = showMessageRequestFutures.poll(10, TimeUnit.SECONDS) notification.hideBalloon() return CompletableFuture.completedFuture(messageActionItem ?: MessageActionItem("")) } diff --git a/src/main/kotlin/snyk/container/ContainerBulkFileListener.kt b/src/main/kotlin/snyk/container/ContainerBulkFileListener.kt index 601540c97..f9553361a 100644 --- a/src/main/kotlin/snyk/container/ContainerBulkFileListener.kt +++ b/src/main/kotlin/snyk/container/ContainerBulkFileListener.kt @@ -1,6 +1,5 @@ package snyk.container -import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project @@ -11,6 +10,7 @@ import io.snyk.plugin.getKubernetesImageCache import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.getSnykToolWindowPanel import io.snyk.plugin.isContainerEnabled +import io.snyk.plugin.refreshAnnotationsForOpenFiles class ContainerBulkFileListener : SnykBulkFileListener() { @@ -74,8 +74,8 @@ class ContainerBulkFileListener : SnykBulkFileListener() { snykCachedResults.currentContainerResult = newContainerCache ApplicationManager.getApplication().invokeLater { getSnykToolWindowPanel(project)?.displayContainerResults(newContainerCache) + refreshAnnotationsForOpenFiles(project) } - DaemonCodeAnalyzer.getInstance(project).restart() } private fun makeObsolete(containerIssuesForImage: ContainerIssuesForImage): ContainerIssuesForImage = diff --git a/src/main/kotlin/snyk/iac/IacBulkFileListener.kt b/src/main/kotlin/snyk/iac/IacBulkFileListener.kt index 2a9ea6826..5b8c46ba6 100644 --- a/src/main/kotlin/snyk/iac/IacBulkFileListener.kt +++ b/src/main/kotlin/snyk/iac/IacBulkFileListener.kt @@ -1,6 +1,5 @@ package snyk.iac -import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project @@ -10,6 +9,7 @@ import com.intellij.openapi.vfs.VirtualFile import io.snyk.plugin.SnykBulkFileListener import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.getSnykToolWindowPanel +import io.snyk.plugin.refreshAnnotationsForOpenFiles class IacBulkFileListener : SnykBulkFileListener() { @@ -55,8 +55,8 @@ class IacBulkFileListener : SnykBulkFileListener() { currentIacResult.iacScanNeeded = true ApplicationManager.getApplication().invokeLater { getSnykToolWindowPanel(project)?.displayIacResults(currentIacResult) + refreshAnnotationsForOpenFiles(project) } - DaemonCodeAnalyzer.getInstance(project).restart() } } diff --git a/src/main/kotlin/snyk/iac/IgnoreButtonActionListener.kt b/src/main/kotlin/snyk/iac/IgnoreButtonActionListener.kt index 9ac3ead3d..baac7f5a3 100644 --- a/src/main/kotlin/snyk/iac/IgnoreButtonActionListener.kt +++ b/src/main/kotlin/snyk/iac/IgnoreButtonActionListener.kt @@ -1,12 +1,12 @@ package snyk.iac -import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile +import io.snyk.plugin.refreshAnnotationsForOpenFiles import io.snyk.plugin.relativePathToContentRoot import io.snyk.plugin.ui.SnykBalloonNotificationHelper import org.jetbrains.annotations.NotNull @@ -52,11 +52,7 @@ class IgnoreButtonActionListener( text = IGNORED_ISSUE_BUTTON_TEXT } ApplicationManager.getApplication().invokeLater { - if (psiFile != null) { - DaemonCodeAnalyzer.getInstance(project).restart(psiFile) - } else { - DaemonCodeAnalyzer.getInstance(project).restart() - } + refreshAnnotationsForOpenFiles(project) } } catch (e: IgnoreException) { SnykBalloonNotificationHelper.showError( diff --git a/src/test/kotlin/io/snyk/plugin/net/CliConfigServiceTest.kt b/src/test/kotlin/io/snyk/plugin/net/CliConfigServiceTest.kt index 6f0cbcabf..04c200904 100644 --- a/src/test/kotlin/io/snyk/plugin/net/CliConfigServiceTest.kt +++ b/src/test/kotlin/io/snyk/plugin/net/CliConfigServiceTest.kt @@ -1,9 +1,6 @@ package io.snyk.plugin.net -import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer -import okio.buffer -import okio.source import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.core.IsEqual.equalTo import org.hamcrest.core.IsNull.nullValue @@ -13,7 +10,6 @@ import org.junit.Test import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import snyk.net.HttpClient -import java.nio.charset.StandardCharsets class CliConfigServiceTest { @JvmField diff --git a/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt b/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt index 7e37e0192..f4c65e760 100644 --- a/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt +++ b/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt @@ -64,6 +64,7 @@ class ContainerBulkFileListenerTest : BasePlatformTestCase() { setUpContainerTest() val path = createNewFileInProjectRoot().toPath() Files.write(path, "\n".toByteArray(Charsets.UTF_8)) + VirtualFileManager.getInstance().syncRefresh() val virtualFile = VirtualFileManager.getInstance().findFileByNioPath(path) require(virtualFile != null) diff --git a/src/test/kotlin/snyk/errorHandler/SentryErrorReporterTest.kt b/src/test/kotlin/snyk/errorHandler/SentryErrorReporterTest.kt index f7ceb9c8f..de6f54428 100644 --- a/src/test/kotlin/snyk/errorHandler/SentryErrorReporterTest.kt +++ b/src/test/kotlin/snyk/errorHandler/SentryErrorReporterTest.kt @@ -11,14 +11,11 @@ import io.sentry.Sentry import io.sentry.protocol.SentryId import io.snyk.plugin.pluginSettings import io.snyk.plugin.services.SnykApplicationSettingsStateService -import junit.framework.TestCase import org.junit.After import org.junit.Before import org.junit.Test import snyk.PluginInformation -import snyk.common.isAnalyticsPermitted import snyk.pluginInfo -import java.net.URI class SentryErrorReporterTest { @Before