From 12dcac8eb91432e3f66bcf6c924e2d0559ac6421 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 12 Jul 2024 17:59:09 +0200 Subject: [PATCH] fix: better error handling for OSS scans via LS [IDE-486] (#572) * fix: propagate error from ls * fix: warning * fix: add error code (for iac) * fix: improve error handling * fix: ktlint warning --- .../ui/toolwindow/SnykToolWindowPanel.kt | 48 ++++++++++++------- .../SnykToolWindowSnykScanListenerLS.kt | 15 +++++- .../kotlin/snyk/common/SnykCachedResults.kt | 30 +++++++++--- .../snyk/common/lsp/LanguageServerWrapper.kt | 38 ++++++++------- src/main/kotlin/snyk/common/lsp/Types.kt | 16 ++++++- 5 files changed, 104 insertions(+), 43 deletions(-) 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 5ac455c6c..ad1a991b4 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt @@ -104,8 +104,11 @@ import javax.swing.tree.TreePath * Main panel for Snyk tool window. */ @Service(Service.Level.PROJECT) -class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable { - private val descriptionPanel = SimpleToolWindowPanel(true, true).apply { name = "descriptionPanel" } +class SnykToolWindowPanel( + val project: Project, +) : JPanel(), + Disposable { + internal val descriptionPanel = SimpleToolWindowPanel(true, true).apply { name = "descriptionPanel" } private val logger = Logger.getInstance(this::class.java) private val rootTreeNode = DefaultMutableTreeNode("") private val rootOssTreeNode = RootOssTreeNode(project) @@ -174,7 +177,8 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable { scanListener } - project.messageBus.connect(this) + project.messageBus + .connect(this) .subscribe( SnykScanListener.SNYK_SCAN_TOPIC, object : SnykScanListener { @@ -233,7 +237,7 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable { override fun scanningOssError(snykError: SnykError) { var ossResultsCount: Int? = null ApplicationManager.getApplication().invokeLater { - if (snykError.message.startsWith(NO_OSS_FILES)) { + if (snykError.message.contains(NO_OSS_FILES)) { rootOssTreeNode.originalCliErrorMessage = snykError.message ossResultsCount = NODE_NOT_SUPPORTED_STATE } else { @@ -288,7 +292,8 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable { }, ) - project.messageBus.connect(this) + project.messageBus + .connect(this) .subscribe( SnykResultsFilteringListener.SNYK_FILTERING_TOPIC, object : SnykResultsFilteringListener { @@ -319,7 +324,10 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable { }, ) - ApplicationManager.getApplication().messageBus.connect(this) + ApplicationManager + .getApplication() + .messageBus + .connect(this) .subscribe( SnykCliDownloadListener.CLI_DOWNLOAD_TOPIC, object : SnykCliDownloadListener { @@ -333,7 +341,8 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable { }, ) - project.messageBus.connect(this) + project.messageBus + .connect(this) .subscribe( SnykSettingsListener.SNYK_SETTINGS_TOPIC, object : SnykSettingsListener { @@ -344,7 +353,8 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable { }, ) - project.messageBus.connect(this) + project.messageBus + .connect(this) .subscribe( SnykTaskQueueListener.TASK_QUEUE_TOPIC, object : SnykTaskQueueListener { @@ -484,7 +494,8 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable { private fun triggerScan() { getSnykAnalyticsService().logAnalysisIsTriggered( - AnalysisIsTriggered.builder() + AnalysisIsTriggered + .builder() .analysisType(getSelectedProducts(pluginSettings())) .ide(AnalysisIsTriggered.Ide.JETBRAINS) .triggeredByUser(true) @@ -504,7 +515,8 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable { revalidate() getSnykAnalyticsService().logWelcomeIsViewed( - WelcomeIsViewed.builder() + WelcomeIsViewed + .builder() .ide(JETBRAINS) .build(), ) @@ -566,10 +578,13 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable { ) { val settings = pluginSettings() + val realError = getSnykCachedResults(project)?.currentOssError != null + && ossResultsCount != NODE_NOT_SUPPORTED_STATE + val newOssTreeNodeText = when { - getSnykCachedResults(project)?.currentOssError != null -> "$OSS_ROOT_TEXT (error)" isOssRunning(project) && settings.ossScanEnable -> "$OSS_ROOT_TEXT (scanning...)" + realError -> "$OSS_ROOT_TEXT (error)" else -> ossResultsCount?.let { count -> @@ -590,7 +605,8 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable { getSnykCachedResults(project)?.currentSnykCodeError != null -> "$CODE_SECURITY_ROOT_TEXT (error)" isSnykCodeRunning( project, - ) && settings.snykCodeSecurityIssuesScanEnable -> "$CODE_SECURITY_ROOT_TEXT (scanning...)" + ) && + settings.snykCodeSecurityIssuesScanEnable -> "$CODE_SECURITY_ROOT_TEXT (scanning...)" else -> securityIssuesCount?.let { count -> @@ -610,7 +626,8 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable { getSnykCachedResults(project)?.currentSnykCodeError != null -> "$CODE_QUALITY_ROOT_TEXT (error)" isSnykCodeRunning( project, - ) && settings.snykCodeQualityIssuesScanEnable -> "$CODE_QUALITY_ROOT_TEXT (scanning...)" + ) && + settings.snykCodeQualityIssuesScanEnable -> "$CODE_QUALITY_ROOT_TEXT (scanning...)" else -> qualityIssuesCount?.let { count -> @@ -1059,9 +1076,6 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable { @TestOnly fun getRootOssIssuesTreeNode() = rootOssTreeNode - @TestOnly - fun getRootCodeQualityIssuesTreeNode() = rootQualityIssuesTreeNode - fun getTree() = vulnerabilitiesTree @TestOnly @@ -1089,7 +1103,7 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable { const val NO_SUPPORTED_PACKAGE_MANAGER_FOUND = " - No supported package manager found" private const val TOOL_WINDOW_SPLITTER_PROPORTION_KEY = "SNYK_TOOL_WINDOW_SPLITTER_PROPORTION" internal const val NODE_INITIAL_STATE = -1 - private const val NODE_NOT_SUPPORTED_STATE = -2 + const val NODE_NOT_SUPPORTED_STATE = -2 private val CONTAINER_DOCS_TEXT_WITH_LINK = """ diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt index 47f82e094..b081c2ad2 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt @@ -15,6 +15,8 @@ import io.snyk.plugin.pluginSettings import io.snyk.plugin.refreshAnnotationsForOpenFiles import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.CODE_QUALITY_ROOT_TEXT import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.CODE_SECURITY_ROOT_TEXT +import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.NODE_NOT_SUPPORTED_STATE +import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.NO_OSS_FILES import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.OSS_ROOT_TEXT import io.snyk.plugin.ui.toolwindow.nodes.leaf.SuggestionTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootContainerIssuesTreeNode @@ -25,6 +27,7 @@ import io.snyk.plugin.ui.toolwindow.nodes.root.RootSecurityIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.InfoTreeNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.SnykCodeFileTreeNode import snyk.common.ProductType +import snyk.common.lsp.CliError import snyk.common.lsp.ScanIssue import snyk.common.lsp.SnykScanParams import javax.swing.JTree @@ -87,7 +90,6 @@ class SnykToolWindowSnykScanListenerLS( "oss" -> { this.rootOssIssuesTreeNode.removeAllChildren() this.rootOssIssuesTreeNode.userObject = "$OSS_ROOT_TEXT (error)" - refreshAnnotationsForOpenFiles(project) } "code" -> { @@ -219,10 +221,19 @@ class SnykToolWindowSnykScanListenerLS( } } + val currentOssError = getSnykCachedResults(project)?.currentOssError + val cliErrorMessage = currentOssError?.message + + var ossResultsCountForDisplay = ossResultsCount + if (cliErrorMessage?.contains(NO_OSS_FILES) == true) { + snykToolWindowPanel.getRootOssIssuesTreeNode().originalCliErrorMessage = cliErrorMessage + ossResultsCountForDisplay = NODE_NOT_SUPPORTED_STATE + } + snykToolWindowPanel.updateTreeRootNodesPresentation( securityIssuesCount = securityIssuesCount, qualityIssuesCount = qualityIssuesCount, - ossResultsCount = ossResultsCount, + ossResultsCount = ossResultsCountForDisplay, iacResultsCount = iacResultsCount, containerResultsCount = containerResultsCount, addHMLPostfix = rootNodePostFix, diff --git a/src/main/kotlin/snyk/common/SnykCachedResults.kt b/src/main/kotlin/snyk/common/SnykCachedResults.kt index 9caa8e52d..33144fc5d 100644 --- a/src/main/kotlin/snyk/common/SnykCachedResults.kt +++ b/src/main/kotlin/snyk/common/SnykCachedResults.kt @@ -21,7 +21,9 @@ import snyk.iac.IacResult import snyk.oss.OssResult @Service(Service.Level.PROJECT) -class SnykCachedResults(val project: Project) : Disposable { +class SnykCachedResults( + val project: Project, +) : Disposable { private var disposed = false get() { return project.isDisposed || ApplicationManager.getApplication().isDisposed || field @@ -155,25 +157,41 @@ class SnykCachedResults(val project: Project) : Disposable { when (snykScan.product) { "oss" -> { currentOssError = - SnykError("Failed to run Snyk OpenSource scan", snykScan.folderPath) + SnykError( + snykScan.cliError?.error ?: snykScan.errorMessage + ?: "Failed to run Snyk Open Source Scan", + snykScan.cliError?.path ?: snykScan.folderPath, + snykScan.cliError?.code, + ) } "code" -> { currentSnykCodeError = - SnykError("Failed to run Snyk Code scan", snykScan.folderPath) + SnykError( + snykScan.cliError?.error ?: snykScan.errorMessage + ?: "Failed to run Snyk Code Scan", + snykScan.cliError?.path ?: snykScan.folderPath, + snykScan.cliError?.code, + ) } "iac" -> { currentIacError = SnykError( - "Failed to run Snyk Infrastructure as Code scan", - snykScan.folderPath, + snykScan.cliError?.error ?: snykScan.errorMessage ?: "Failed to run Snyk IaC Scan", + snykScan.cliError?.path ?: snykScan.folderPath, + snykScan.cliError?.code, ) } "container" -> { currentContainerError = - SnykError("Failed to run Snyk Container scan", snykScan.folderPath) + SnykError( + snykScan.cliError?.error ?: snykScan.errorMessage + ?: "Failed to run Snyk Container Scan", + snykScan.cliError?.path ?: snykScan.folderPath, + snykScan.cliError?.code, + ) } } SnykBalloonNotificationHelper diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 42de1510c..c6c7dfc5c 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -14,12 +14,10 @@ import com.intellij.openapi.vfs.VirtualFile import io.snyk.plugin.getCliFile import io.snyk.plugin.getContentRootVirtualFiles import io.snyk.plugin.getSnykTaskQueueService -import io.snyk.plugin.isCliInstalled import io.snyk.plugin.isSnykIaCLSEnabled import io.snyk.plugin.isSnykOSSLSEnabled import io.snyk.plugin.pluginSettings import io.snyk.plugin.toLanguageServerURL -import io.snyk.plugin.ui.SnykBalloonNotificationHelper import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope @@ -64,10 +62,13 @@ private const val INITIALIZATION_TIMEOUT = 20L class LanguageServerWrapper( private val lsPath: String = getCliFile().absolutePath, private val executorService: ExecutorService = Executors.newCachedThreadPool(), -): Disposable { +) : Disposable { private var initializeResult: InitializeResult? = null private val gson = com.google.gson.Gson() - private var disposed = false ; get() { return ApplicationManager.getApplication().isDisposed || field } + private var disposed = false + get() { + return ApplicationManager.getApplication().isDisposed || field + } fun isDisposed() = disposed @@ -95,7 +96,8 @@ class LanguageServerWrapper( @Suppress("MemberVisibilityCanBePrivate") // because we want to test it var isInitializing: ReentrantLock = - CycleDetectingLockFactory.newInstance(CycleDetectingLockFactory.Policies.THROW) + CycleDetectingLockFactory + .newInstance(CycleDetectingLockFactory.Policies.THROW) .newReentrantLock("initializeLock") internal val isInitialized: Boolean @@ -148,11 +150,10 @@ class LanguageServerWrapper( getFeatureFlagStatusInternal("snykCodeConsistentIgnores") } - fun shutdown(): Future<*> { - return executorService.submit { + fun shutdown(): Future<*> = + executorService.submit { process.destroyForcibly() } - } private fun determineWorkspaceFolders(): List { val workspaceFolders = mutableSetOf() @@ -322,7 +323,10 @@ class LanguageServerWrapper( } } - fun sendFolderScanCommand(folder: String, project: Project) { + fun sendFolderScanCommand( + folder: String, + project: Project, + ) { if (!ensureLanguageServerInitialized()) return if (DumbService.getInstance(project).isDumb) return try { @@ -348,12 +352,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, @@ -391,9 +395,9 @@ class LanguageServerWrapper( } companion object { - private var INSTANCE: LanguageServerWrapper? = null + private var instance: LanguageServerWrapper? = null - fun getInstance() = INSTANCE ?: LanguageServerWrapper().also { INSTANCE = it } + fun getInstance() = instance ?: LanguageServerWrapper().also { instance = it } } override fun dispose() { diff --git a/src/main/kotlin/snyk/common/lsp/Types.kt b/src/main/kotlin/snyk/common/lsp/Types.kt index cd9958866..0bf517ada 100644 --- a/src/main/kotlin/snyk/common/lsp/Types.kt +++ b/src/main/kotlin/snyk/common/lsp/Types.kt @@ -18,12 +18,26 @@ import java.util.Date import java.util.Locale import javax.swing.Icon +// type CliOutput struct { +// Code int `json:"code,omitempty"` +// ErrorMessage string `json:"error,omitempty"` +// Path string `json:"path,omitempty"` +// Command string `json:"command,omitempty"` +//} +data class CliError ( + val code: Int? = 0, + val error: String? = null, + val path: String? = null, + val command: String? = null, +) // Define the SnykScanParams data class data class SnykScanParams( - val status: String, // Status can be either Initial, InProgress or Success + val status: String, // Status can be either Initial, InProgress, Success or Error val product: String, // Product under scan (Snyk Code, Snyk Open Source, etc...) val folderPath: String, // FolderPath is the root-folder of the current scan val issues: List, // Issues contain the scan results in the common issues model + val errorMessage: String? = null, // Error Message if applicable + val cliError: CliError? = null, // structured error information if applicable ) // Define the ScanIssue data class