diff --git a/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts b/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts index 4c88cba491b..c7ca7a4ff9b 100644 --- a/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts +++ b/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts @@ -14,6 +14,7 @@ import { ManifestResolver, request, TargetContent, + ToolkitError, } from 'aws-core-vscode/shared' import * as semver from 'semver' import { assertTelemetry } from 'aws-core-vscode/test' @@ -201,12 +202,6 @@ export function createLspInstallerTests({ id: lspConfig.id, manifestLocation: 'remote', languageServerSetupStage: 'getManifest', - result: 'Failed', - }, - { - id: lspConfig.id, - manifestLocation: 'cache', - languageServerSetupStage: 'getManifest', result: 'Succeeded', }, { @@ -282,6 +277,34 @@ export function createLspInstallerTests({ const download = await createInstaller(lspConfig).resolve() assert.ok(download.assetDirectory.endsWith('-rc.0')) }) + + it('throws on firewall error', async () => { + // Stub the manifest resolver to return a valid manifest + sandbox.stub(ManifestResolver.prototype, 'resolve').resolves({ + manifestSchemaVersion: '0.0.0', + artifactId: 'foo', + artifactDescription: 'foo', + isManifestDeprecated: false, + versions: [createVersion('1.0.0', targetContents)], + }) + + // Fail all HTTP requests for the language server + sandbox.stub(request, 'fetch').returns({ + response: Promise.resolve({ + ok: false, + }), + } as any) + + // This should now throw a NetworkConnectivityError + await assert.rejects( + async () => await installer.resolve(), + (err: ToolkitError) => { + assert.strictEqual(err.code, 'NetworkConnectivityError') + assert.ok(err.message.includes('Unable to download dependencies')) + return true + } + ) + }) }) }) } diff --git a/packages/core/src/shared/lsp/baseLspInstaller.ts b/packages/core/src/shared/lsp/baseLspInstaller.ts index 27e19df6b3a..0aeca1dfda4 100644 --- a/packages/core/src/shared/lsp/baseLspInstaller.ts +++ b/packages/core/src/shared/lsp/baseLspInstaller.ts @@ -45,6 +45,7 @@ export abstract class BaseLspInstaller { - const fallbackDirectory = await this.getFallbackDir(latestVersion.serverVersion) + const cachedVersions = await this.getCachedVersions() + if (cachedVersions.length === 0) { + /** + * at this point the latest version doesn't exist locally, lsp download (with retries) failed, and there are no cached fallback versions. + * This _probably_ only happens when the user hit a firewall/proxy issue, since otherwise they would probably have at least + * one other language server locally + */ + throw new ToolkitError( + `Unable to download dependencies from ${this.manifestUrl}. Check your network connectivity or firewall configuration and then try again.`, + { + code: 'NetworkConnectivityError', + } + ) + } + + const fallbackDirectory = await this.getFallbackDir(latestVersion.serverVersion, cachedVersions) if (!fallbackDirectory) { throw new ToolkitError('Unable to find a compatible version of the Language Server', { code: 'IncompatibleVersion', @@ -142,6 +158,7 @@ export class LanguageServerResolver { assetDirectory: cacheDirectory, } } else { + await this.cleanupVersion(latestVersion.serverVersion) throw new ToolkitError('Failed to download server from remote', { code: 'RemoteDownloadFailed' }) } } finally { @@ -149,6 +166,14 @@ export class LanguageServerResolver { } } + private async cleanupVersion(version: string) { + // clean up the X.X.X download directory since the download failed + const downloadDirectory = this.getDownloadDirectory(version) + if (await fs.existsDir(downloadDirectory)) { + await fs.delete(downloadDirectory) + } + } + /** Gets the current local ("cached") LSP server bundle. */ private async getLocalServer( cacheDirectory: string, @@ -178,16 +203,9 @@ export class LanguageServerResolver { /** * Returns the path to the most compatible cached LSP version that can serve as a fallback **/ - private async getFallbackDir(version: string) { + private async getFallbackDir(version: string, cachedVersions: string[]) { const compatibleLspVersions = this.compatibleManifestLspVersion() - // determine all folders containing lsp versions in the fallback parent folder - const cachedVersions = (await fs.readdir(this.defaultDownloadFolder())) - .filter(([_, filetype]) => filetype === FileType.Directory) - .map(([pathName, _]) => semver.parse(pathName)) - .filter((ver): ver is semver.SemVer => ver !== null) - .map((x) => x.version) - const expectedVersion = semver.parse(version) if (!expectedVersion) { return undefined @@ -203,6 +221,15 @@ export class LanguageServerResolver { return fallbackDir.length > 0 ? fallbackDir[0] : undefined } + private async getCachedVersions() { + // determine all folders containing lsp versions in the parent folder + return (await fs.readdir(this.defaultDownloadFolder())) + .filter(([_, filetype]) => filetype === FileType.Directory) + .map(([pathName, _]) => semver.parse(pathName)) + .filter((ver): ver is semver.SemVer => ver !== null) + .map((x) => x.version) + } + /** * Validate the local cache directory of the given lsp version (matches expected hash) * If valid return cache directory, else return undefined diff --git a/packages/core/src/shared/lsp/manifestResolver.ts b/packages/core/src/shared/lsp/manifestResolver.ts index 60cc466a835..6dd0a793178 100644 --- a/packages/core/src/shared/lsp/manifestResolver.ts +++ b/packages/core/src/shared/lsp/manifestResolver.ts @@ -69,7 +69,6 @@ export class ManifestResolver { const localManifest = await this.getLocalManifest(true).catch(() => undefined) if (localManifest) { - localManifest.location = 'remote' return localManifest } else { // Will emit a `languageServer_setup` result=failed metric...