Skip to content

Commit

Permalink
fix: better error handling for OSS scans via LS [IDE-486] (#572)
Browse files Browse the repository at this point in the history
* fix: propagate error from ls

* fix: warning

* fix: add error code (for iac)

* fix: improve error handling

* fix: ktlint warning
  • Loading branch information
bastiandoetsch authored Jul 12, 2024
1 parent 849f18b commit 12dcac8
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 43 deletions.
48 changes: 31 additions & 17 deletions src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -504,7 +515,8 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable {
revalidate()

getSnykAnalyticsService().logWelcomeIsViewed(
WelcomeIsViewed.builder()
WelcomeIsViewed
.builder()
.ide(JETBRAINS)
.build(),
)
Expand Down Expand Up @@ -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 ->
Expand All @@ -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 ->
Expand All @@ -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 ->
Expand Down Expand Up @@ -1059,9 +1076,6 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable {
@TestOnly
fun getRootOssIssuesTreeNode() = rootOssTreeNode

@TestOnly
fun getRootCodeQualityIssuesTreeNode() = rootQualityIssuesTreeNode

fun getTree() = vulnerabilitiesTree

@TestOnly
Expand Down Expand Up @@ -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 =
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -87,7 +90,6 @@ class SnykToolWindowSnykScanListenerLS(
"oss" -> {
this.rootOssIssuesTreeNode.removeAllChildren()
this.rootOssIssuesTreeNode.userObject = "$OSS_ROOT_TEXT (error)"
refreshAnnotationsForOpenFiles(project)
}

"code" -> {
Expand Down Expand Up @@ -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,
Expand Down
30 changes: 24 additions & 6 deletions src/main/kotlin/snyk/common/SnykCachedResults.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
38 changes: 21 additions & 17 deletions src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<WorkspaceFolder> {
val workspaceFolders = mutableSetOf<WorkspaceFolder>()
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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() {
Expand Down
16 changes: 15 additions & 1 deletion src/main/kotlin/snyk/common/lsp/Types.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<ScanIssue>, // 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
Expand Down

0 comments on commit 12dcac8

Please sign in to comment.