Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.ui.components.JBLoadingPanel
Expand All @@ -18,8 +19,10 @@
import com.intellij.ui.dsl.builder.AlignY
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.jcef.JBCefApp
import kotlinx.coroutines.runBlocking
import software.aws.toolkits.jetbrains.isDeveloperMode
import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactHelper

Check warning on line 24 in plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQPanel.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused import directive

Unused import directive
import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactManager
import software.aws.toolkits.jetbrains.services.amazonq.webview.Browser
import java.awt.event.ActionListener
import java.util.concurrent.CompletableFuture
Expand Down Expand Up @@ -88,7 +91,7 @@
wrapper.setContent(loadingPanel)

ApplicationManager.getApplication().executeOnPooledThread {
val webUri = ArtifactHelper().getLatestLocalLspArtifact().resolve("amazonq-ui.js").toUri()
val webUri = runBlocking { service<ArtifactManager>().fetchArtifact(project).resolve("amazonq-ui.js").toUri() }
loadingPanel.stopLoading()
runInEdt {
browser.complete(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ class Browser(parent: Disposable, private val webUri: URI, val project: Project)
): String {
val quickActionConfig = generateQuickActionConfig()
val postMessageToJavaJsCode = receiveMessageQuery.inject("JSON.stringify(message)")
// language=HTML
val jsScripts = """
<script type="text/javascript" src="$webUri" defer onload="init()"></script>
<script type="text/javascript">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs

init {
// will cause slow service init, but maybe fine for now. will not block UI since fetch/extract will be under background progress
val artifact = runBlocking { ArtifactManager(project, manifestRange = null).fetchArtifact() }.toAbsolutePath()
val artifact = runBlocking { service<ArtifactManager>().fetchArtifact(project) }.toAbsolutePath()
val node = if (SystemInfo.isWindows) "node.exe" else "node"
val cmd = GeneralCommandLine(
artifact.resolve(node).toString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts

import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.util.io.createDirectories
Expand All @@ -18,16 +19,14 @@
import software.aws.toolkits.jetbrains.core.saveFileFromUrl
import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
import software.aws.toolkits.resources.AwsCoreBundle
import java.nio.file.Files
import java.nio.file.Path
import java.util.concurrent.atomic.AtomicInteger

class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, private val maxDownloadAttempts: Int = MAX_DOWNLOAD_ATTEMPTS) {

companion object {
private val DEFAULT_ARTIFACT_PATH = getToolkitsCommonCacheRoot().resolve("aws").resolve("toolkits").resolve("language-servers")
private val logger = getLogger<ArtifactHelper>()
private const val MAX_DOWNLOAD_ATTEMPTS = 3
}
class ArtifactHelper internal constructor(
private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
private val maxDownloadAttempts: Int = MAX_DOWNLOAD_ATTEMPTS,
) {
private val currentAttempt = AtomicInteger(0)

fun removeDelistedVersions(delistedVersions: List<ManifestManager.Version>) {
Expand Down Expand Up @@ -79,16 +78,6 @@
.sortedByDescending { (_, semVer) -> semVer }
}

fun getLatestLocalLspArtifact(): Path {
val localFolders = getSubFolders(lspArtifactsPath)
return localFolders.map { localFolder ->
localFolder to SemVer.parseFromText(localFolder.fileName.toString())
}
.sortedByDescending { (_, semVer) -> semVer }
.first()
.first
}

fun getExistingLspArtifacts(versions: List<ManifestManager.Version>, target: ManifestManager.VersionTarget?): Boolean {
if (versions.isEmpty() || target?.contents == null) return false

Expand All @@ -114,7 +103,7 @@
}

suspend fun tryDownloadLspArtifacts(project: Project, versions: List<ManifestManager.Version>, target: ManifestManager.VersionTarget?): Path? {
val temporaryDownloadPath = lspArtifactsPath.resolve("temp")
val temporaryDownloadPath = Files.createTempDirectory("lsp-dl")

Check warning on line 106 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Possibly blocking call in non-blocking context

Possibly blocking call in non-blocking context could lead to thread starvation

Check warning

Code scanning / QDJVMC

Possibly blocking call in non-blocking context Warning

Possibly blocking call in non-blocking context could lead to thread starvation
val downloadPath = lspArtifactsPath.resolve(versions.first().serverVersion.toString())

while (currentAttempt.get() < maxDownloadAttempts) {
Expand Down Expand Up @@ -188,7 +177,7 @@
try {
if (!filePath.exists()) {
logger.info { "Downloading file: ${filePath.fileName}" }
saveFileFromUrl(url, filePath)
saveFileFromUrl(url, filePath, ProgressManager.getInstance().progressIndicator)
}
if (!validateFileHash(filePath, expectedHash)) {
logger.warn { "Hash mismatch for ${filePath.fileName}, re-downloading" }
Expand Down Expand Up @@ -222,4 +211,10 @@
throw LspException(errorMessage, LspException.ErrorCode.DOWNLOAD_FAILED)
}
}

companion object {
private val DEFAULT_ARTIFACT_PATH = getToolkitsCommonCacheRoot().resolve("aws").resolve("toolkits").resolve("language-servers")
private val logger = getLogger<ArtifactHelper>()
private const val MAX_DOWNLOAD_ATTEMPTS = 3
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,29 @@

package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts

import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
import com.intellij.util.text.SemVer
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.jetbrains.annotations.VisibleForTesting
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
import java.nio.file.Path

class ArtifactManager(
private val project: Project,
private val manifestFetcher: ManifestFetcher = ManifestFetcher(),
private val artifactHelper: ArtifactHelper = ArtifactHelper(),
manifestRange: SupportedManifestVersionRange?,
) {
@Service
class ArtifactManager {
private val manifestFetcher: ManifestFetcher = ManifestFetcher()
private val artifactHelper: ArtifactHelper = ArtifactHelper()

// we currently cannot handle the versions swithing in the middle of a user's session
private val mutex = Mutex()
private var artifactDeferred: Deferred<Path>? = null

data class SupportedManifestVersionRange(
val startVersion: SemVer,
Expand All @@ -28,8 +36,6 @@ class ArtifactManager(
val inRangeVersions: List<ManifestManager.Version>,
)

private val manifestVersionRanges: SupportedManifestVersionRange = manifestRange ?: DEFAULT_VERSION_RANGE

companion object {
private val DEFAULT_VERSION_RANGE = SupportedManifestVersionRange(
startVersion = SemVer("0.0.0", 0, 0, 0),
Expand All @@ -38,35 +44,47 @@ class ArtifactManager(
private val logger = getLogger<ArtifactManager>()
}

suspend fun fetchArtifact(): Path {
val manifest = manifestFetcher.fetch() ?: throw LspException(
"Language Support is not available, as manifest is missing.",
LspException.ErrorCode.MANIFEST_FETCH_FAILED
)
val lspVersions = getLSPVersionsFromManifestWithSpecifiedRange(manifest)
suspend fun fetchArtifact(project: Project): Path {
mutex.withLock { artifactDeferred }?.let {
return it.await()
}

this.artifactHelper.removeDelistedVersions(lspVersions.deListedVersions)
return mutex.withLock {
coroutineScope {
async {
val manifest = manifestFetcher.fetch() ?: throw LspException(
"Language Support is not available, as manifest is missing.",
LspException.ErrorCode.MANIFEST_FETCH_FAILED
)
val lspVersions = getLSPVersionsFromManifestWithSpecifiedRange(manifest)

if (lspVersions.inRangeVersions.isEmpty()) {
// No versions are found which are in the given range. Fallback to local lsp artifacts.
val localLspArtifacts = this.artifactHelper.getAllLocalLspArtifactsWithinManifestRange(manifestVersionRanges)
if (localLspArtifacts.isNotEmpty()) {
return localLspArtifacts.first().first
}
throw LspException("Language server versions not found in manifest.", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION)
}
artifactHelper.removeDelistedVersions(lspVersions.deListedVersions)

// If there is an LSP Manifest with the same version
val target = getTargetFromLspManifest(lspVersions.inRangeVersions)
// Get Local LSP files and check if we can re-use existing LSP Artifacts
val artifactPath: Path = if (this.artifactHelper.getExistingLspArtifacts(lspVersions.inRangeVersions, target)) {
this.artifactHelper.getAllLocalLspArtifactsWithinManifestRange(manifestVersionRanges).first().first
} else {
this.artifactHelper.tryDownloadLspArtifacts(project, lspVersions.inRangeVersions, target)
?: throw LspException("Failed to download LSP artifacts", LspException.ErrorCode.DOWNLOAD_FAILED)
}
this.artifactHelper.deleteOlderLspArtifacts(manifestVersionRanges)
return artifactPath
if (lspVersions.inRangeVersions.isEmpty()) {
// No versions are found which are in the given range. Fallback to local lsp artifacts.
val localLspArtifacts = artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE)
if (localLspArtifacts.isNotEmpty()) {
return@async localLspArtifacts.first().first
}
throw LspException("Language server versions not found in manifest.", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION)
}

// If there is an LSP Manifest with the same version
val target = getTargetFromLspManifest(lspVersions.inRangeVersions)
// Get Local LSP files and check if we can re-use existing LSP Artifacts
val artifactPath: Path = if (artifactHelper.getExistingLspArtifacts(lspVersions.inRangeVersions, target)) {
artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE).first().first
} else {
artifactHelper.tryDownloadLspArtifacts(project, lspVersions.inRangeVersions, target)
?: throw LspException("Failed to download LSP artifacts", LspException.ErrorCode.DOWNLOAD_FAILED)
}
artifactHelper.deleteOlderLspArtifacts(DEFAULT_VERSION_RANGE)
return@async artifactPath
}
}.also {
artifactDeferred = it
}
}.await()
}

@VisibleForTesting
Expand All @@ -78,7 +96,7 @@ class ArtifactManager(
SemVer.parseFromText(serverVersion)?.let { semVer ->
when {
version.isDelisted != false -> Pair(version, true) // Is deListed
semVer in manifestVersionRanges.startVersion..manifestVersionRanges.endVersion -> Pair(version, false) // Is in range
semVer in DEFAULT_VERSION_RANGE.let { it.startVersion..it.endVersion } -> Pair(version, false) // Is in range
else -> null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,31 @@

package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts

import com.intellij.openapi.project.Project
import com.intellij.testFramework.ProjectExtension
import com.intellij.util.text.SemVer
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.jetbrains.annotations.TestOnly
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
import org.junit.jupiter.api.io.TempDir
import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactManager.SupportedManifestVersionRange
import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
import java.nio.file.Path

@TestOnly
class ArtifactManagerTest {
companion object {
@JvmField
@RegisterExtension
val projectExtension = ProjectExtension()
}

@TempDir
lateinit var tempDir: Path
Expand All @@ -33,7 +36,6 @@ class ArtifactManagerTest {
private lateinit var artifactManager: ArtifactManager
private lateinit var manifestFetcher: ManifestFetcher
private lateinit var manifestVersionRanges: SupportedManifestVersionRange
private lateinit var mockProject: Project

@BeforeEach
fun setUp() {
Expand All @@ -43,19 +45,15 @@ class ArtifactManagerTest {
startVersion = SemVer("1.0.0", 1, 0, 0),
endVersion = SemVer("2.0.0", 2, 0, 0)
)
mockProject = mockk<Project>(relaxed = true) {
every { basePath } returns tempDir.toString()
every { name } returns "TestProject"
}
artifactManager = ArtifactManager(mockProject, manifestFetcher, artifactHelper, manifestVersionRanges)
artifactManager = ArtifactManager()
}

@Test
fun `fetch artifact fetcher throws exception if manifest is null`() {
every { manifestFetcher.fetch() }.returns(null)

assertThatThrownBy {
runBlocking { artifactManager.fetchArtifact() }
runBlocking { artifactManager.fetchArtifact(projectExtension.project) }
}
.isInstanceOf(LspException::class.java)
.hasFieldOrPropertyWithValue("errorCode", LspException.ErrorCode.MANIFEST_FETCH_FAILED)
Expand All @@ -64,14 +62,14 @@ class ArtifactManagerTest {
@Test
fun `fetch artifact does not have any valid lsp versions`() {
every { manifestFetcher.fetch() }.returns(ManifestManager.Manifest())
artifactManager = spyk(ArtifactManager(mockProject, manifestFetcher, artifactHelper, manifestVersionRanges))
artifactManager = spyk(ArtifactManager())

every { artifactManager.getLSPVersionsFromManifestWithSpecifiedRange(any()) }.returns(
ArtifactManager.LSPVersions(deListedVersions = emptyList(), inRangeVersions = emptyList())
)

assertThatThrownBy {
runBlocking { artifactManager.fetchArtifact() }
runBlocking { artifactManager.fetchArtifact(projectExtension.project) }
}
.isInstanceOf(LspException::class.java)
.hasFieldOrPropertyWithValue("errorCode", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION)
Expand All @@ -84,7 +82,7 @@ class ArtifactManagerTest {
every { manifestFetcher.fetch() }.returns(ManifestManager.Manifest())
every { artifactHelper.getAllLocalLspArtifactsWithinManifestRange(any()) }.returns(expectedResult)

runBlocking { artifactManager.fetchArtifact() }
runBlocking { artifactManager.fetchArtifact(projectExtension.project) }

verify(exactly = 1) { manifestFetcher.fetch() }
verify(exactly = 1) { artifactHelper.getAllLocalLspArtifactsWithinManifestRange(any()) }
Expand All @@ -95,7 +93,7 @@ class ArtifactManagerTest {
val target = ManifestManager.VersionTarget(platform = "temp", arch = "temp")
val versions = listOf(ManifestManager.Version("1.0.0", targets = listOf(target)))

artifactManager = spyk(ArtifactManager(mockProject, manifestFetcher, artifactHelper, manifestVersionRanges))
artifactManager = spyk(ArtifactManager())

every { artifactManager.getLSPVersionsFromManifestWithSpecifiedRange(any()) }.returns(
ArtifactManager.LSPVersions(deListedVersions = emptyList(), inRangeVersions = versions)
Expand All @@ -110,7 +108,7 @@ class ArtifactManagerTest {
coEvery { artifactHelper.tryDownloadLspArtifacts(any(), any(), any()) } returns tempDir
every { artifactHelper.deleteOlderLspArtifacts(any()) } just Runs

runBlocking { artifactManager.fetchArtifact() }
runBlocking { artifactManager.fetchArtifact(projectExtension.project) }

verify(exactly = 1) { runBlocking { artifactHelper.tryDownloadLspArtifacts(any(), any(), any()) } }
verify(exactly = 1) { artifactHelper.deleteOlderLspArtifacts(any()) }
Expand All @@ -122,7 +120,7 @@ class ArtifactManagerTest {
val versions = listOf(ManifestManager.Version("1.0.0", targets = listOf(target)))
val expectedResult = listOf(Pair(tempDir, SemVer("1.0.0", 1, 0, 0)))

artifactManager = spyk(ArtifactManager(mockProject, manifestFetcher, artifactHelper, manifestVersionRanges))
artifactManager = spyk(ArtifactManager())

every { artifactManager.getLSPVersionsFromManifestWithSpecifiedRange(any()) }.returns(
ArtifactManager.LSPVersions(deListedVersions = emptyList(), inRangeVersions = versions)
Expand All @@ -137,7 +135,7 @@ class ArtifactManagerTest {
every { artifactHelper.deleteOlderLspArtifacts(any()) } just Runs
every { artifactHelper.getAllLocalLspArtifactsWithinManifestRange(any()) }.returns(expectedResult)

runBlocking { artifactManager.fetchArtifact() }
runBlocking { artifactManager.fetchArtifact(projectExtension.project) }

verify(exactly = 0) { runBlocking { artifactHelper.tryDownloadLspArtifacts(any(), any(), any()) } }
verify(exactly = 1) { artifactHelper.deleteOlderLspArtifacts(any()) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ amazonqFeatureDev.placeholder.after_code_generation=Choose an option to proceed
amazonqFeatureDev.placeholder.after_monthly_limit=Chat input is disabled
amazonqFeatureDev.placeholder.closed_session=Open a new chat tab to continue
amazonqFeatureDev.placeholder.context_gathering_complete=Gathering context...
amazonqFeatureDev.placeholder.downloading_and_extracting_lsp_artifacts=Downloading and Extracting Lsp Artifacts...
amazonqFeatureDev.placeholder.downloading_and_extracting_lsp_artifacts=Downloading and Extracting LSP Artifacts...
amazonqFeatureDev.placeholder.generating_code=Generating code...
amazonqFeatureDev.placeholder.lsp=LSP
amazonqFeatureDev.placeholder.new_plan=Describe your task or issue in as much detail as possible
Expand Down
Loading