-
Notifications
You must be signed in to change notification settings - Fork 751
feat(lsp): older and delisted versions of lsp are automatically removed #6409
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
9cd4544
e7bf0af
90d23b1
2c19cf4
1c366ef
b00ec0d
4198611
32d2ba3
a7a89c1
ee7343b
604d2e1
78aae8f
3b9878f
3b693c6
d5b972e
bf36a48
88db689
bace7ca
5e47cb5
da20782
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,33 +4,48 @@ | |
| */ | ||
|
|
||
| import path from 'path' | ||
| import { LspResolution, LspResolver } from '../../shared/lsp/types' | ||
| import { LspResolution, LspResolver, LspVersion } from '../../shared/lsp/types' | ||
| import { ManifestResolver } from '../../shared/lsp/manifestResolver' | ||
| import { LanguageServerResolver } from '../../shared/lsp/lspResolver' | ||
| import { Range } from 'semver' | ||
| import { Range, sort } from 'semver' | ||
| import { getNodeExecutableName } from '../../shared/lsp/utils/platform' | ||
| import { fs } from '../../shared/fs/fs' | ||
| import { partition } from '../../shared/utilities/tsUtils' | ||
|
|
||
| const manifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' | ||
| export const lspManifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' | ||
| // this LSP client in Q extension is only going to work with these LSP server versions | ||
| const supportedLspServerVersions = '0.1.32' | ||
| export const supportedLspServerVersions = '0.1.32' | ||
| export const lspWorkspaceName = 'AmazonQ-Workspace' | ||
|
|
||
| export class WorkspaceLSPResolver implements LspResolver { | ||
| private readonly versionRange: Range | ||
| private readonly shouldCleanUp: boolean | ||
| public constructor( | ||
| options?: Partial<{ | ||
| versionRange: Range | ||
| cleanUp: boolean | ||
| }> | ||
| ) { | ||
| this.versionRange = options?.versionRange ?? new Range(supportedLspServerVersions) | ||
| this.shouldCleanUp = options?.cleanUp ?? true | ||
| } | ||
|
|
||
| async resolve(): Promise<LspResolution> { | ||
| const name = 'AmazonQ-Workspace' | ||
| const manifest = await new ManifestResolver(manifestUrl, name).resolve() | ||
| const manifest = await new ManifestResolver(lspManifestUrl, lspWorkspaceName).resolve() | ||
| const installationResult = await new LanguageServerResolver( | ||
| manifest, | ||
| name, | ||
| new Range(supportedLspServerVersions) | ||
| lspWorkspaceName, | ||
| this.versionRange | ||
| ).resolve() | ||
|
|
||
| const nodeName = | ||
| process.platform === 'win32' ? getNodeExecutableName() : `node-${process.platform}-${process.arch}` | ||
| const nodePath = path.join(installationResult.assetDirectory, nodeName) | ||
| await fs.chmod(nodePath, 0o755) | ||
|
|
||
| // TODO Cleanup old versions of language servers | ||
| if (this.shouldCleanUp) { | ||
| await this.cleanUp(manifest.versions, path.dirname(installationResult.assetDirectory)) | ||
| } | ||
| return { | ||
| ...installationResult, | ||
| resourcePaths: { | ||
|
|
@@ -42,4 +57,31 @@ export class WorkspaceLSPResolver implements LspResolver { | |
| }, | ||
| } | ||
| } | ||
|
|
||
| private async getDownloadedVersions(downloadDirectory: string): Promise<string[]> { | ||
|
||
| return (await fs.readdir(downloadDirectory)).map(([f, _], __) => f) | ||
| } | ||
|
|
||
| private isDelisted(manifestVersions: LspVersion[], targetVersion: string): boolean { | ||
|
||
| return manifestVersions.find((v) => v.serverVersion === targetVersion)?.isDelisted ?? false | ||
| } | ||
|
|
||
| /** | ||
| * Delete all delisted versions and keep the two newest versions that remain | ||
| * @param manifest | ||
| * @param downloadDirectory | ||
| */ | ||
| async cleanUp(manifestVersions: LspVersion[], downloadDirectory: string): Promise<void> { | ||
| const downloadedVersions = await this.getDownloadedVersions(downloadDirectory) | ||
| const [delistedVersions, remainingVersions] = partition(downloadedVersions, (v: string) => | ||
| this.isDelisted(manifestVersions, v) | ||
| ) | ||
| for (const v of delistedVersions) { | ||
| await fs.delete(path.join(downloadDirectory, v), { force: true, recursive: true }) | ||
| } | ||
|
|
||
| for (const v of sort(remainingVersions).slice(0, -2)) { | ||
Hweinstock marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| await fs.delete(path.join(downloadDirectory, v), { force: true, recursive: true }) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| /*! | ||
Hweinstock marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import { Uri } from 'vscode' | ||
| import { fs } from '../../../shared' | ||
| import { createTestWorkspaceFolder } from '../../testUtil' | ||
| import path from 'path' | ||
| import { WorkspaceLSPResolver } from '../../../amazonq/lsp/workspaceInstaller' | ||
| import assert from 'assert' | ||
|
|
||
| async function fakeInstallVersion(version: string, installationDir: string): Promise<void> { | ||
| const versionDir = path.join(installationDir, version) | ||
| await fs.mkdir(versionDir) | ||
| await fs.writeFile(path.join(versionDir, 'file.txt'), 'content') | ||
| } | ||
|
|
||
| describe('workspaceInstaller', function () { | ||
| describe('cleanUp', function () { | ||
| let installationDir: Uri | ||
| let versions: string[] | ||
|
|
||
| before(async function () { | ||
| installationDir = (await createTestWorkspaceFolder()).uri | ||
| versions = ['1.0.0', '1.0.1', '1.1.1', '2.1.1'] | ||
Hweinstock marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }) | ||
|
|
||
| beforeEach(async function () { | ||
| for (const v of versions) { | ||
| await fakeInstallVersion(v, installationDir.fsPath) | ||
| } | ||
| }) | ||
|
|
||
| after(async function () { | ||
| await fs.delete(installationDir, { force: true, recursive: true }) | ||
| }) | ||
| it('keeps two newest versions', async function () { | ||
| const wsr = new WorkspaceLSPResolver() | ||
| await wsr.cleanUp([], installationDir.fsPath) | ||
|
|
||
| const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) | ||
| assert.strictEqual(result.length, 2) | ||
| assert.ok(result.includes('2.1.1')) | ||
| assert.ok(result.includes('1.1.1')) | ||
| }) | ||
|
|
||
| it('deletes delisted versions', async function () { | ||
|
||
| const wsr = new WorkspaceLSPResolver() | ||
| await wsr.cleanUp([{ serverVersion: '1.1.1', isDelisted: true, targets: [] }], installationDir.fsPath) | ||
|
|
||
| const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) | ||
| assert.strictEqual(result.length, 2) | ||
| assert.ok(result.includes('2.1.1')) | ||
| assert.ok(result.includes('1.0.1')) | ||
| }) | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| /*! | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import { partition } from '../../../shared/utilities/tsUtils' | ||
| import assert from 'assert' | ||
|
|
||
| describe('partition', function () { | ||
| it('should split the list according to predicate', function () { | ||
| const items = [1, 2, 3, 4, 5, 6, 7, 8] | ||
| const [even, odd] = partition(items, (i) => i % 2 === 0) | ||
| assert.deepStrictEqual(even, [2, 4, 6, 8]) | ||
| assert.deepStrictEqual(odd, [1, 3, 5, 7]) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| /*! | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import { Range, sort } from 'semver' | ||
| import assert from 'assert' | ||
| import { lspWorkspaceName, lspManifestUrl, WorkspaceLSPResolver } from '../../../amazonq/lsp/workspaceInstaller' | ||
| import { fs } from '../../../shared/fs/fs' | ||
| import path from 'path' | ||
| import * as sinon from 'sinon' | ||
| import { LanguageServerResolver, ManifestResolver } from '../../../shared' | ||
|
|
||
| async function installVersion(version: string, cleanUp: boolean = false) { | ||
| const resolver = new WorkspaceLSPResolver({ versionRange: new Range(version), cleanUp: cleanUp }) | ||
| return await resolver.resolve() | ||
| } | ||
|
|
||
| /** | ||
| * Installs all versions, only running 'cleanUp' on the last install. | ||
| * @param versions | ||
| * @returns | ||
| */ | ||
| async function testInstallVersions(versions: string[]) { | ||
| await Promise.all(versions.slice(0, -1).map(async (version) => await installVersion(version))) | ||
| const finalVersionResult = await installVersion(versions[versions.length - 1], true) | ||
| const allVersions = path.dirname(finalVersionResult.assetDirectory) | ||
| const versionsDownloaded = (await fs.readdir(allVersions)).map(([f, _], __) => f) | ||
| return versionsDownloaded | ||
| } | ||
|
|
||
| describe('workspaceInstaller', function () { | ||
| let testVersions: string[] | ||
| let onMac: boolean | ||
| before(async function () { | ||
| // TODO: remove this when non-mac support is added. | ||
| onMac = process.platform === 'darwin' | ||
| if (!onMac) { | ||
| this.skip() | ||
| } | ||
| await fs.delete(LanguageServerResolver.defaultDir, { force: true, recursive: true }) | ||
|
||
| const manifest = await new ManifestResolver(lspManifestUrl, lspWorkspaceName).resolve() | ||
| testVersions = sort( | ||
| manifest.versions | ||
| .filter((v) => !v.isDelisted) | ||
| .slice(0, 4) | ||
| .map((v) => v.serverVersion) | ||
| ) | ||
| }) | ||
|
|
||
| it('removes all but the latest two versions', async function () { | ||
| if (!onMac) { | ||
| this.skip() | ||
| } | ||
| const versionsDownloaded = await testInstallVersions(testVersions) | ||
|
|
||
| assert.strictEqual(versionsDownloaded.length, 2) | ||
| assert.ok(versionsDownloaded.includes(testVersions[testVersions.length - 1])) | ||
| assert.ok(versionsDownloaded.includes(testVersions[testVersions.length - 2])) | ||
| }) | ||
|
|
||
| it('removes delisted versions then keeps 2 remaining most recent', async function () { | ||
| if (!onMac) { | ||
| this.skip() | ||
| } | ||
| const isDelisted = sinon.stub(WorkspaceLSPResolver.prototype, 'isDelisted' as any) | ||
| isDelisted.callsFake((_manifestVersions, version) => { | ||
| return version === testVersions[testVersions.length - 2] | ||
| }) | ||
|
|
||
| const versionsDownloaded = await testInstallVersions(testVersions) | ||
|
|
||
| assert.strictEqual(versionsDownloaded.length, 2) | ||
| assert.ok(versionsDownloaded.includes(testVersions[testVersions.length - 1])) | ||
| assert.ok(versionsDownloaded.includes(testVersions[testVersions.length - 3])) | ||
| isDelisted.restore() | ||
| }) | ||
| }) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there any case where we would make this false?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was using this to get greater control in the integ tests, but considering those tests aren't even running, it likely makes sense to remove this bloat for now.