Skip to content

Commit

Permalink
Merge pull request #468 from snyk/fix/IDE-124_fix-CLI-update
Browse files Browse the repository at this point in the history
fix: CLI update & download [IDE-123][IDE-124]
  • Loading branch information
bastiandoetsch authored Jan 31, 2024
2 parents 97ab1e6 + 8ce27c1 commit 0696a57
Show file tree
Hide file tree
Showing 14 changed files with 202 additions and 168 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Snyk Changelog

## [2.7.1]
### Fixed
- only start up language server after CLI update (fixes lock error on Windows)
- only start up one instance of language server, manage projects via workspace folders

## [2.7.0]
### Added
- Snyk controller extension point
Expand Down
14 changes: 9 additions & 5 deletions src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import com.intellij.ide.plugins.IdeaPluginDescriptor
import com.intellij.ide.plugins.PluginInstaller
import com.intellij.ide.plugins.PluginStateListener
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.startup.ProjectActivity
Expand All @@ -26,7 +28,7 @@ import snyk.iac.IacBulkFileListener
import snyk.oss.OssBulkFileListener
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.*
import java.util.Date

private val LOG = logger<SnykPostStartupActivity>()

Expand All @@ -44,6 +46,7 @@ class SnykPostStartupActivity : ProjectActivity {
private var listenersActivated = false
val settings = pluginSettings()

@Suppress("TooGenericExceptionCaught")
override suspend fun execute(project: Project) {
PluginInstaller.addStateListener(UninstallListener())

Expand All @@ -62,6 +65,7 @@ class SnykPostStartupActivity : ProjectActivity {

if (!listenersActivated) {
val messageBusConnection = ApplicationManager.getApplication().messageBus.connect()
// TODO: add subscription for language server messages
messageBusConnection.subscribe(VirtualFileManager.VFS_CHANGES, OssBulkFileListener())
messageBusConnection.subscribe(VirtualFileManager.VFS_CHANGES, SnykCodeBulkFileListener())
messageBusConnection.subscribe(VirtualFileManager.VFS_CHANGES, IacBulkFileListener())
Expand All @@ -77,12 +81,12 @@ class SnykPostStartupActivity : ProjectActivity {
AnnotatorCommon.initRefreshing(project)

if (!ApplicationManager.getApplication().isUnitTestMode) {
getSnykTaskQueueService(project)?.downloadLatestRelease()
getSnykTaskQueueService(project)?.waitUntilCliDownloadedIfNeeded(EmptyProgressIndicator())
try {
getSnykTaskQueueService(project)?.initializeLanguageServer()
getSnykTaskQueueService(project)?.connectProjectToLanguageServer(project)
getAnalyticsScanListener(project)?.initScanListener()
} catch (ignored: Exception) {
// do nothing to not break UX for analytics
} catch (e: Exception) {
Logger.getInstance(SnykPostStartupActivity::class.java).error(e)
}
}

Expand Down
15 changes: 15 additions & 0 deletions src/main/kotlin/io/snyk/plugin/SnykProjectManagerListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package io.snyk.plugin

import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManagerListener
import com.intellij.serviceContainer.AlreadyDisposedException
import io.snyk.plugin.services.SnykTaskQueueService
import io.snyk.plugin.snykcode.core.AnalysisData
import io.snyk.plugin.snykcode.core.RunUtils
import io.snyk.plugin.snykcode.core.SnykCodeIgnoreInfoHolder
Expand All @@ -18,5 +20,18 @@ class SnykProjectManagerListener : ProjectManagerListener {
AnalysisData.instance.removeProjectFromCaches(project)
SnykCodeIgnoreInfoHolder.instance.removeProject(project)
}

// doesn't need to run in the background, as the called update is already running in a background thread
if (SnykTaskQueueService.ls.isInitialized) {
try {
SnykTaskQueueService.ls.updateWorkspaceFolders(
project,
emptySet(),
SnykTaskQueueService.ls.getWorkspaceFolders(project)
)
} catch (ignore: AlreadyDisposedException) {
// ignore
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.snyk.plugin.analytics
import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
import io.snyk.plugin.events.SnykScanListener
import io.snyk.plugin.getSnykTaskQueueService
import io.snyk.plugin.services.SnykTaskQueueService
import io.snyk.plugin.snykcode.SnykCodeResults
import snyk.common.SnykError
import snyk.common.lsp.commands.ScanDoneEvent
Expand Down Expand Up @@ -48,7 +48,7 @@ class AnalyticsScanListener(val project: Project) {
ossResult.mediumSeveritiesCount(),
ossResult.lowSeveritiesCount()
)
getSnykTaskQueueService(project)?.ls?.sendReportAnalyticsCommand(scanDoneEvent)
SnykTaskQueueService.ls.sendReportAnalyticsCommand(scanDoneEvent)
}

override fun scanningSnykCodeFinished(snykCodeResults: SnykCodeResults?) {
Expand All @@ -66,7 +66,7 @@ class AnalyticsScanListener(val project: Project) {
} else {
getScanDoneEvent(duration, product, 0, 0, 0, 0)
}
getSnykTaskQueueService(project)?.ls?.sendReportAnalyticsCommand(scanDoneEvent)
SnykTaskQueueService.ls.sendReportAnalyticsCommand(scanDoneEvent)
}

override fun scanningIacFinished(iacResult: IacResult) {
Expand All @@ -78,7 +78,7 @@ class AnalyticsScanListener(val project: Project) {
iacResult.mediumSeveritiesCount(),
iacResult.lowSeveritiesCount()
)
getSnykTaskQueueService(project)?.ls?.sendReportAnalyticsCommand(scanDoneEvent)
SnykTaskQueueService.ls.sendReportAnalyticsCommand(scanDoneEvent)
}

override fun scanningContainerFinished(containerResult: ContainerResult) {
Expand All @@ -90,7 +90,7 @@ class AnalyticsScanListener(val project: Project) {
containerResult.mediumSeveritiesCount(),
containerResult.lowSeveritiesCount()
)
getSnykTaskQueueService(project)?.ls?.sendReportAnalyticsCommand(scanDoneEvent)
SnykTaskQueueService.ls.sendReportAnalyticsCommand(scanDoneEvent)
}

override fun scanningOssError(snykError: SnykError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import com.intellij.util.PlatformIcons
import io.snyk.plugin.cli.ConsoleCommandRunner
import io.snyk.plugin.getCliFile
import io.snyk.plugin.getPluginPath
import io.snyk.plugin.getSnykCliDownloaderService
import io.snyk.plugin.getSnykTaskQueueService
import io.snyk.plugin.pluginSettings
import io.snyk.plugin.ui.SnykBalloonNotificationHelper
import io.snyk.plugin.ui.getReadOnlyClickableHtmlJEditorPane
Expand Down Expand Up @@ -55,7 +55,7 @@ class SnykCliAuthenticationService(val project: Project) {
val downloadCliTask: () -> Unit = {
if (!getCliFile().exists()) {
val currentIndicator = ProgressManager.getInstance().progressIndicator
getSnykCliDownloaderService().downloadLatestRelease(currentIndicator, project)
getSnykTaskQueueService(project)?.downloadLatestRelease(currentIndicator)
} else {
logger.debug("Skip CLI download, since it was already downloaded")
}
Expand Down
61 changes: 29 additions & 32 deletions src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.progress.BackgroundTaskQueue
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
Expand All @@ -29,10 +28,8 @@ import io.snyk.plugin.isSnykCodeRunning
import io.snyk.plugin.net.ClientException
import io.snyk.plugin.pluginSettings
import io.snyk.plugin.snykcode.core.RunUtils
import io.snyk.plugin.ui.SnykBalloonNotificationHelper
import io.snyk.plugin.ui.SnykBalloonNotifications
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jetbrains.annotations.TestOnly
import snyk.common.SnykError
import snyk.common.lsp.LanguageServerWrapper
Expand All @@ -45,7 +42,6 @@ class SnykTaskQueueService(val project: Project) {
private val taskQueue = BackgroundTaskQueue(project, "Snyk")
private val taskQueueIac = BackgroundTaskQueue(project, "Snyk: Iac")
private val taskQueueContainer = BackgroundTaskQueue(project, "Snyk: Container")
val ls = LanguageServerWrapper()

private val settings
get() = pluginSettings()
Expand Down Expand Up @@ -79,23 +75,19 @@ class SnykTaskQueueService(val project: Project) {
})
}

@OptIn(DelicateCoroutinesApi::class)
fun initializeLanguageServer() {
waitUntilCliDownloadedIfNeeded(EmptyProgressIndicator())
ls.initialize()
GlobalScope.launch {
ls.process.errorStream.bufferedReader().forEachLine { println(it) }
}
GlobalScope.launch {
ls.startListening()
fun connectProjectToLanguageServer(project: Project) {
synchronized(ls) {
if (!ls.isInitialized && !ls.isInitializing) {
ls.initialize()
}
}

ls.sendInitializeMessage(project)
ls.updateWorkspaceFolders(project, ls.getWorkspaceFolders(project), emptySet())
}

fun scan() {
taskQueue.run(object : Task.Backgroundable(project, "Snyk: initializing...", true) {
override fun run(indicator: ProgressIndicator) {
// FIXME: this should be using content roots instead of basePath
project.basePath?.let {
if (!confirmScanningAndSetWorkspaceTrustedStateIfNeeded(project, Paths.get(it))) return
}
Expand Down Expand Up @@ -123,21 +115,13 @@ class SnykTaskQueueService(val project: Project) {
})
}

private fun waitUntilCliDownloadedIfNeeded(indicator: ProgressIndicator) {
if (isCliInstalled()) return
// check if any CLI related scan enabled
val ossScanEnable = settings.ossScanEnable
val iacScanEnabled = isIacEnabled() && settings.iacScanEnabled
val containerScanEnabled = isContainerEnabled() && settings.containerScanEnabled
if (!(ossScanEnable || iacScanEnabled || containerScanEnabled)) {
return
}
fun waitUntilCliDownloadedIfNeeded(indicator: ProgressIndicator) {
indicator.text = "Snyk waits for CLI to be downloaded..."
downloadLatestRelease()
downloadLatestRelease(indicator)
do {
indicator.checkCanceled()
Thread.sleep(waitForDownloadMillis)
} while (isCliDownloading())
Thread.sleep(WAIT_FOR_DOWNLOAD_MILLIS)
} while (!isCliInstalled() || isCliDownloading())
}

private fun scheduleContainerScan() {
Expand Down Expand Up @@ -246,7 +230,7 @@ class SnykTaskQueueService(val project: Project) {
if (ossResult.isSuccessful()) {
scanPublisher?.scanningOssFinished(ossResult)
} else {
scanPublisher?.scanningOssError(ossResult.getFirstError()!!)
ossResult.getFirstError()?.let { scanPublisher?.scanningOssError(it) }
}
}
DaemonCodeAnalyzer.getInstance(project).restart()
Expand Down Expand Up @@ -297,12 +281,24 @@ class SnykTaskQueueService(val project: Project) {
})
}

fun downloadLatestRelease() {
fun downloadLatestRelease(indicator: ProgressIndicator) {
// abort even before submitting a task
if (!pluginSettings().manageBinariesAutomatically) {
if (!isCliInstalled()) {
val msg =
"The plugin cannot scan without Snyk CLI, but automatic download is disabled. " +
"Please put a Snyk CLI executable in ${pluginSettings().cliPath} and retry."
SnykBalloonNotificationHelper.showError(msg, project)
}
indicator.cancel()
return
}
val cliDownloader = getSnykCliDownloaderService()

taskQueue.run(object : Task.Backgroundable(project, "Check Snyk CLI presence", true) {
override fun run(indicator: ProgressIndicator) {
cliDownloadPublisher.checkCliExistsStarted()
if (project.isDisposed) return
val cliDownloader = getSnykCliDownloaderService()

if (!isCliInstalled()) {
cliDownloader.downloadLatestRelease(indicator, project)
Expand Down Expand Up @@ -331,6 +327,7 @@ class SnykTaskQueueService(val project: Project) {
}

companion object {
private const val waitForDownloadMillis = 500L
private const val WAIT_FOR_DOWNLOAD_MILLIS = 1000L
val ls = LanguageServerWrapper()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ import com.intellij.util.io.HttpRequests
import io.snyk.plugin.cli.Platform
import io.snyk.plugin.events.SnykCliDownloadListener
import io.snyk.plugin.getCliFile
import io.snyk.plugin.isCliInstalled
import io.snyk.plugin.pluginSettings
import io.snyk.plugin.services.download.HttpRequestHelper.createRequest
import io.snyk.plugin.tail
import io.snyk.plugin.ui.SnykBalloonNotificationHelper
import java.io.IOException
import java.time.LocalDate
import java.time.temporal.ChronoUnit
Expand Down Expand Up @@ -60,18 +58,9 @@ class SnykCliDownloaderService {
}

fun downloadLatestRelease(indicator: ProgressIndicator, project: Project) {
if (!pluginSettings().manageBinariesAutomatically) {
if (!isCliInstalled()) {
val msg =
"The plugin cannot scan without Snyk CLI, but automatic download is disabled. " +
"Please put a Snyk CLI executable in ${pluginSettings().cliPath} and retry."
SnykBalloonNotificationHelper.showError(msg, project)
}
return
}
currentProgressIndicator = indicator
cliDownloadPublisher.cliDownloadStarted()
indicator.isIndeterminate = true
currentProgressIndicator = indicator
var succeeded = false
val cliFile = getCliFile()
try {
Expand Down Expand Up @@ -118,7 +107,6 @@ class SnykCliDownloaderService {
latestReleaseInfo.tagName.isNotEmpty() &&
isNewVersionAvailable(settings.cliVersion, cliVersionNumbers(latestReleaseInfo.tagName))
) {

downloadLatestRelease(indicator, project)

settings.lastCheckDate = Date()
Expand All @@ -133,13 +121,11 @@ class SnykCliDownloaderService {
}

fun isNewVersionAvailable(currentCliVersion: String?, newCliVersion: String?): Boolean {
if (currentCliVersion == null ||
newCliVersion == null ||
currentCliVersion.isEmpty() ||
currentCliVersion.isEmpty()
) {
return true
}
val cliVersionsNullOrEmpty =
currentCliVersion == null || newCliVersion == null ||
currentCliVersion.isEmpty() || newCliVersion.isEmpty()

if (cliVersionsNullOrEmpty) return true

tailrec fun checkIsNewVersionAvailable(
currentCliVersionNumbers: List<String>,
Expand All @@ -158,7 +144,7 @@ class SnykCliDownloaderService {
}
}

return checkIsNewVersionAvailable(currentCliVersion.split('.'), newCliVersion.split('.'))
return checkIsNewVersionAvailable(currentCliVersion!!.split('.'), newCliVersion!!.split('.'))
}

fun getLatestReleaseInfo(): LatestReleaseInfo? = this.latestReleaseInfo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable {

init {
vulnerabilitiesTree.cellRenderer = SnykTreeCellRenderer()

layout = BorderLayout()
TreeSpeedSearch(vulnerabilitiesTree, TreeSpeedSearch.NODE_DESCRIPTOR_TOSTRING, true)

Expand Down
Loading

0 comments on commit 0696a57

Please sign in to comment.