Skip to content

Commit 9abe243

Browse files
authored
fix(amazonq): only download single copy of LSP at once (#5659)
This addresses multiple problems with the Flare dependency resolution: * Multiple projects starting will attempt to download the same artifact concurrently to the same location * If the user has never downloaded the LSP, they always get an error because we incorrectly assume that a file exists * Multiple projects can resolve different versions of the server/client
1 parent 09be52d commit 9abe243

File tree

7 files changed

+106
-91
lines changed

7 files changed

+106
-91
lines changed

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQPanel.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.intellij.idea.AppMode
77
import com.intellij.openapi.Disposable
88
import com.intellij.openapi.application.ApplicationManager
99
import com.intellij.openapi.application.runInEdt
10+
import com.intellij.openapi.components.service
1011
import com.intellij.openapi.project.Project
1112
import com.intellij.openapi.util.Disposer
1213
import com.intellij.ui.components.JBLoadingPanel
@@ -19,11 +20,12 @@ import com.intellij.ui.dsl.builder.panel
1920
import com.intellij.ui.jcef.JBCefApp
2021
import kotlinx.coroutines.CoroutineScope
2122
import kotlinx.coroutines.launch
23+
import kotlinx.coroutines.runBlocking
2224
import software.aws.toolkits.jetbrains.isDeveloperMode
2325
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
2426
import software.aws.toolkits.jetbrains.services.amazonq.apps.AppConnection
2527
import software.aws.toolkits.jetbrains.services.amazonq.commands.MessageTypeRegistry
26-
import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactHelper
28+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactManager
2729
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AsyncChatUiListener
2830
import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
2931
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessageConnector
@@ -105,7 +107,7 @@ class AmazonQPanel(val project: Project, private val scope: CoroutineScope) : Di
105107
wrapper.setContent(loadingPanel)
106108

107109
ApplicationManager.getApplication().executeOnPooledThread {
108-
val webUri = ArtifactHelper().getLatestLocalLspArtifact().resolve("amazonq-ui.js").toUri()
110+
val webUri = runBlocking { service<ArtifactManager>().fetchArtifact(project).resolve("amazonq-ui.js").toUri() }
109111
loadingPanel.stopLoading()
110112
runInEdt {
111113
browser.complete(

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class Browser(parent: Disposable, private val webUri: URI, val project: Project)
116116
): String {
117117
val quickActionConfig = generateQuickActionConfig()
118118
val postMessageToJavaJsCode = receiveMessageQuery.inject("JSON.stringify(message)")
119+
// language=HTML
119120
val jsScripts = """
120121
<script type="text/javascript" src="$webUri" defer onload="init()"></script>
121122
<script type="text/javascript">

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs
266266

267267
init {
268268
// will cause slow service init, but maybe fine for now. will not block UI since fetch/extract will be under background progress
269-
val artifact = runBlocking { ArtifactManager(project, manifestRange = null).fetchArtifact() }.toAbsolutePath()
269+
val artifact = runBlocking { service<ArtifactManager>().fetchArtifact(project) }.toAbsolutePath()
270270
val node = if (SystemInfo.isWindows) "node.exe" else "node"
271271
val cmd = GeneralCommandLine(
272272
artifact.resolve(node).toString(),

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

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

6+
import com.intellij.openapi.progress.ProgressManager
67
import com.intellij.openapi.project.Project
78
import com.intellij.platform.ide.progress.withBackgroundProgress
89
import com.intellij.util.io.createDirectories
@@ -18,16 +19,14 @@ import software.aws.toolkits.core.utils.warn
1819
import software.aws.toolkits.jetbrains.core.saveFileFromUrl
1920
import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
2021
import software.aws.toolkits.resources.AwsCoreBundle
22+
import java.nio.file.Files
2123
import java.nio.file.Path
2224
import java.util.concurrent.atomic.AtomicInteger
2325

24-
class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, private val maxDownloadAttempts: Int = MAX_DOWNLOAD_ATTEMPTS) {
25-
26-
companion object {
27-
private val DEFAULT_ARTIFACT_PATH = getToolkitsCommonCacheRoot().resolve("aws").resolve("toolkits").resolve("language-servers")
28-
private val logger = getLogger<ArtifactHelper>()
29-
private const val MAX_DOWNLOAD_ATTEMPTS = 3
30-
}
26+
class ArtifactHelper internal constructor(
27+
private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
28+
private val maxDownloadAttempts: Int = MAX_DOWNLOAD_ATTEMPTS,
29+
) {
3130
private val currentAttempt = AtomicInteger(0)
3231

3332
fun removeDelistedVersions(delistedVersions: List<ManifestManager.Version>) {
@@ -79,16 +78,6 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
7978
.sortedByDescending { (_, semVer) -> semVer }
8079
}
8180

82-
fun getLatestLocalLspArtifact(): Path {
83-
val localFolders = getSubFolders(lspArtifactsPath)
84-
return localFolders.map { localFolder ->
85-
localFolder to SemVer.parseFromText(localFolder.fileName.toString())
86-
}
87-
.sortedByDescending { (_, semVer) -> semVer }
88-
.first()
89-
.first
90-
}
91-
9281
fun getExistingLspArtifacts(versions: List<ManifestManager.Version>, target: ManifestManager.VersionTarget?): Boolean {
9382
if (versions.isEmpty() || target?.contents == null) return false
9483

@@ -114,7 +103,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
114103
}
115104

116105
suspend fun tryDownloadLspArtifacts(project: Project, versions: List<ManifestManager.Version>, target: ManifestManager.VersionTarget?): Path? {
117-
val temporaryDownloadPath = lspArtifactsPath.resolve("temp")
106+
val temporaryDownloadPath = Files.createTempDirectory("lsp-dl")
118107
val downloadPath = lspArtifactsPath.resolve(versions.first().serverVersion.toString())
119108

120109
while (currentAttempt.get() < maxDownloadAttempts) {
@@ -188,7 +177,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
188177
try {
189178
if (!filePath.exists()) {
190179
logger.info { "Downloading file: ${filePath.fileName}" }
191-
saveFileFromUrl(url, filePath)
180+
saveFileFromUrl(url, filePath, ProgressManager.getInstance().progressIndicator)
192181
}
193182
if (!validateFileHash(filePath, expectedHash)) {
194183
logger.warn { "Hash mismatch for ${filePath.fileName}, re-downloading" }
@@ -222,4 +211,10 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
222211
throw LspException(errorMessage, LspException.ErrorCode.DOWNLOAD_FAILED)
223212
}
224213
}
214+
215+
companion object {
216+
private val DEFAULT_ARTIFACT_PATH = getToolkitsCommonCacheRoot().resolve("aws").resolve("toolkits").resolve("language-servers")
217+
private val logger = getLogger<ArtifactHelper>()
218+
private const val MAX_DOWNLOAD_ATTEMPTS = 3
219+
}
225220
}

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManager.kt

Lines changed: 56 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,32 @@
33

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

6+
import com.intellij.openapi.components.Service
67
import com.intellij.openapi.project.Project
8+
import com.intellij.serviceContainer.NonInjectable
79
import com.intellij.util.text.SemVer
10+
import kotlinx.coroutines.Deferred
11+
import kotlinx.coroutines.async
12+
import kotlinx.coroutines.coroutineScope
13+
import kotlinx.coroutines.sync.Mutex
14+
import kotlinx.coroutines.sync.withLock
815
import org.jetbrains.annotations.VisibleForTesting
916
import software.aws.toolkits.core.utils.error
1017
import software.aws.toolkits.core.utils.getLogger
1118
import software.aws.toolkits.core.utils.info
1219
import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
1320
import java.nio.file.Path
1421

15-
class ArtifactManager(
16-
private val project: Project,
17-
private val manifestFetcher: ManifestFetcher = ManifestFetcher(),
18-
private val artifactHelper: ArtifactHelper = ArtifactHelper(),
19-
manifestRange: SupportedManifestVersionRange?,
20-
) {
22+
@Service
23+
class ArtifactManager @NonInjectable internal constructor(private val manifestFetcher: ManifestFetcher, private val artifactHelper: ArtifactHelper) {
24+
constructor() : this(
25+
ManifestFetcher(),
26+
ArtifactHelper()
27+
)
28+
29+
// we currently cannot handle the versions swithing in the middle of a user's session
30+
private val mutex = Mutex()
31+
private var artifactDeferred: Deferred<Path>? = null
2132

2233
data class SupportedManifestVersionRange(
2334
val startVersion: SemVer,
@@ -28,8 +39,6 @@ class ArtifactManager(
2839
val inRangeVersions: List<ManifestManager.Version>,
2940
)
3041

31-
private val manifestVersionRanges: SupportedManifestVersionRange = manifestRange ?: DEFAULT_VERSION_RANGE
32-
3342
companion object {
3443
private val DEFAULT_VERSION_RANGE = SupportedManifestVersionRange(
3544
startVersion = SemVer("0.0.0", 0, 0, 0),
@@ -38,35 +47,47 @@ class ArtifactManager(
3847
private val logger = getLogger<ArtifactManager>()
3948
}
4049

41-
suspend fun fetchArtifact(): Path {
42-
val manifest = manifestFetcher.fetch() ?: throw LspException(
43-
"Language Support is not available, as manifest is missing.",
44-
LspException.ErrorCode.MANIFEST_FETCH_FAILED
45-
)
46-
val lspVersions = getLSPVersionsFromManifestWithSpecifiedRange(manifest)
50+
suspend fun fetchArtifact(project: Project): Path {
51+
mutex.withLock { artifactDeferred }?.let {
52+
return it.await()
53+
}
4754

48-
this.artifactHelper.removeDelistedVersions(lspVersions.deListedVersions)
55+
return mutex.withLock {
56+
coroutineScope {
57+
async {
58+
val manifest = manifestFetcher.fetch() ?: throw LspException(
59+
"Language Support is not available, as manifest is missing.",
60+
LspException.ErrorCode.MANIFEST_FETCH_FAILED
61+
)
62+
val lspVersions = getLSPVersionsFromManifestWithSpecifiedRange(manifest)
4963

50-
if (lspVersions.inRangeVersions.isEmpty()) {
51-
// No versions are found which are in the given range. Fallback to local lsp artifacts.
52-
val localLspArtifacts = this.artifactHelper.getAllLocalLspArtifactsWithinManifestRange(manifestVersionRanges)
53-
if (localLspArtifacts.isNotEmpty()) {
54-
return localLspArtifacts.first().first
55-
}
56-
throw LspException("Language server versions not found in manifest.", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION)
57-
}
64+
artifactHelper.removeDelistedVersions(lspVersions.deListedVersions)
5865

59-
// If there is an LSP Manifest with the same version
60-
val target = getTargetFromLspManifest(lspVersions.inRangeVersions)
61-
// Get Local LSP files and check if we can re-use existing LSP Artifacts
62-
val artifactPath: Path = if (this.artifactHelper.getExistingLspArtifacts(lspVersions.inRangeVersions, target)) {
63-
this.artifactHelper.getAllLocalLspArtifactsWithinManifestRange(manifestVersionRanges).first().first
64-
} else {
65-
this.artifactHelper.tryDownloadLspArtifacts(project, lspVersions.inRangeVersions, target)
66-
?: throw LspException("Failed to download LSP artifacts", LspException.ErrorCode.DOWNLOAD_FAILED)
67-
}
68-
this.artifactHelper.deleteOlderLspArtifacts(manifestVersionRanges)
69-
return artifactPath
66+
if (lspVersions.inRangeVersions.isEmpty()) {
67+
// No versions are found which are in the given range. Fallback to local lsp artifacts.
68+
val localLspArtifacts = artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE)
69+
if (localLspArtifacts.isNotEmpty()) {
70+
return@async localLspArtifacts.first().first
71+
}
72+
throw LspException("Language server versions not found in manifest.", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION)
73+
}
74+
75+
// If there is an LSP Manifest with the same version
76+
val target = getTargetFromLspManifest(lspVersions.inRangeVersions)
77+
// Get Local LSP files and check if we can re-use existing LSP Artifacts
78+
val artifactPath: Path = if (artifactHelper.getExistingLspArtifacts(lspVersions.inRangeVersions, target)) {
79+
artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE).first().first
80+
} else {
81+
artifactHelper.tryDownloadLspArtifacts(project, lspVersions.inRangeVersions, target)
82+
?: throw LspException("Failed to download LSP artifacts", LspException.ErrorCode.DOWNLOAD_FAILED)
83+
}
84+
artifactHelper.deleteOlderLspArtifacts(DEFAULT_VERSION_RANGE)
85+
return@async artifactPath
86+
}
87+
}.also {
88+
artifactDeferred = it
89+
}
90+
}.await()
7091
}
7192

7293
@VisibleForTesting
@@ -78,7 +99,7 @@ class ArtifactManager(
7899
SemVer.parseFromText(serverVersion)?.let { semVer ->
79100
when {
80101
version.isDelisted != false -> Pair(version, true) // Is deListed
81-
semVer in manifestVersionRanges.startVersion..manifestVersionRanges.endVersion -> Pair(version, false) // Is in range
102+
semVer in DEFAULT_VERSION_RANGE.let { it.startVersion..it.endVersion } -> Pair(version, false) // Is in range
82103
else -> null
83104
}
84105
}

0 commit comments

Comments
 (0)