Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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.idea.AppMode
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 @@ -17,13 +18,14 @@ import com.intellij.ui.dsl.builder.AlignX
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import software.aws.toolkits.jetbrains.isDeveloperMode
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
import software.aws.toolkits.jetbrains.services.amazonq.apps.AppConnection
import software.aws.toolkits.jetbrains.services.amazonq.commands.MessageTypeRegistry
import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactHelper
import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactManager
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AsyncChatUiListener
import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessageConnector
Expand Down Expand Up @@ -105,7 +107,7 @@ class AmazonQPanel(val project: Project, private val scope: CoroutineScope) : Di
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,32 @@

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

import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
import com.intellij.serviceContainer.NonInjectable
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 @NonInjectable internal constructor(private val manifestFetcher: ManifestFetcher, private val artifactHelper: ArtifactHelper) {
constructor() : this(

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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused symbol

Constructor is never used

Check warning

Code scanning / QDJVMC

Unused symbol Warning

Constructor is never used
ManifestFetcher(),
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 +39,6 @@
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 +47,47 @@
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 +99,7 @@
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
Loading
Loading