Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
28 changes: 14 additions & 14 deletions packages/core/src/amazonq/lsp/lspController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

import * as vscode from 'vscode'
import * as path from 'path'
import * as fs from 'fs-extra'
import * as crypto from 'crypto'
import { createWriteStream } from 'fs'
import { getLogger } from '../../shared/logger/logger'
import { CurrentWsFolders, collectFilesForIndex } from '../../shared/utilities/workspaceUtils'
import fetch from 'node-fetch'
Expand All @@ -24,7 +24,7 @@ import { AuthUtil } from '../../codewhisperer'
import { isWeb } from '../../shared/extensionGlobals'
import { getUserAgent } from '../../shared/telemetry/util'
import { isAmazonInternalOs } from '../../shared/vscode/env'
import { fs as fs2 } from '../../shared/fs/fs'
import { fs } from '../../shared/fs/fs'

function getProjectPaths() {
const workspaceFolders = vscode.workspace.workspaceFolders
Expand Down Expand Up @@ -106,7 +106,7 @@ export class LspController {
throw new ToolkitError(`Failed to download. Error: ${JSON.stringify(res)}`)
}
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(localFile)
const file = createWriteStream(localFile)
res.body.pipe(file)
res.body.on('error', (err) => {
reject(err)
Expand Down Expand Up @@ -134,16 +134,16 @@ export class LspController {
}

async getFileSha384(filePath: string): Promise<string> {
const fileBuffer = await fs.promises.readFile(filePath)
const fileBuffer = await fs.readFile(filePath)
const hash = crypto.createHash('sha384')
hash.update(fileBuffer)
return hash.digest('hex')
}

isLspInstalled(context: vscode.ExtensionContext) {
async isLspInstalled(context: vscode.ExtensionContext) {
const localQServer = context.asAbsolutePath(path.join('resources', 'qserver'))
const localNodeRuntime = context.asAbsolutePath(path.join('resources', nodeBinName))
return fs.existsSync(localQServer) && fs.existsSync(localNodeRuntime)
return (await fs.exists(localQServer)) && (await fs.exists(localNodeRuntime))
}

getQserverFromManifest(manifest: Manifest): Content | undefined {
Expand Down Expand Up @@ -208,7 +208,7 @@ export class LspController {
getLogger().error(
`LspController: Downloaded file sha ${sha384} does not match manifest ${content.hashes[0]}.`
)
fs.removeSync(filePath)
await fs.delete(filePath)
return false
}
return true
Expand All @@ -226,19 +226,19 @@ export class LspController {
async tryInstallLsp(context: vscode.ExtensionContext): Promise<boolean> {
let tempFolder = undefined
try {
if (this.isLspInstalled(context)) {
if (await this.isLspInstalled(context)) {
getLogger().info(`LspController: LSP already installed`)
return true
}
// clean up previous downloaded LSP
const qserverPath = context.asAbsolutePath(path.join('resources', 'qserver'))
if (fs.existsSync(qserverPath)) {
if (await fs.exists(qserverPath)) {
await tryRemoveFolder(qserverPath)
}
// clean up previous downloaded node runtime
const nodeRuntimePath = context.asAbsolutePath(path.join('resources', nodeBinName))
if (fs.existsSync(nodeRuntimePath)) {
fs.rmSync(nodeRuntimePath)
if (await fs.exists(nodeRuntimePath)) {
await fs.delete(nodeRuntimePath)
}
// fetch download url for qserver and node runtime
const manifest: Manifest = (await this.fetchManifest()) as Manifest
Expand All @@ -259,16 +259,16 @@ export class LspController {
}
const zip = new AdmZip(qserverZipTempPath)
zip.extractAllTo(tempFolder)
fs.moveSync(path.join(tempFolder, 'qserver'), qserverPath)
await fs.rename(path.join(tempFolder, 'qserver'), qserverPath)

// download node runtime to temp folder
const nodeRuntimeTempPath = path.join(tempFolder, nodeBinName)
const downloadNodeOk = await this.downloadAndCheckHash(nodeRuntimeTempPath, nodeRuntimeContent)
if (!downloadNodeOk) {
return false
}
await fs2.chmod(nodeRuntimeTempPath, 0o755)
fs.moveSync(nodeRuntimeTempPath, nodeRuntimePath)
await fs.chmod(nodeRuntimeTempPath, 0o755)
await fs.rename(nodeRuntimeTempPath, nodeRuntimePath)
return true
} catch (e) {
getLogger().error(`LspController: Failed to setup LSP server ${e}`)
Expand Down
150 changes: 150 additions & 0 deletions packages/core/src/test/amazonq/common/tryInstallLsp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'assert'
import { performanceTest } from '../../../shared/performance/performance'
import { LspController } from '../../../amazonq'
import { fs, getRandomString, globals } from '../../../shared'
import sinon from 'sinon'
import { Content } from 'aws-sdk/clients/codecommit'
import { createTestWorkspace } from '../../testUtil'
import AdmZip from 'adm-zip'
import path from 'path'

// fakeFileContent is matched to fakeQServerContent based on hash.
const fakeHash = '4eb2865c8f40a322aa04e17d8d83bdaa605d6f1cb363af615240a5442a010e0aef66e21bcf4c88f20fabff06efe8a214'

const fakeQServerContent = {
filename: 'qserver-fake.zip',
url: 'https://aws-language-servers/fake.zip',
hashes: [`sha384:${fakeHash}`],
bytes: 93610849,
serverVersion: '1.1.1',
}

const fakeNodeContent = {
filename: 'fake-file',
url: 'https://aws-language-servers.fake-file',
hashes: [`sha384:${fakeHash}`],
bytes: 94144448,
serverVersion: '1.1.1',
}

function createStubs(numberOfFiles: number, fileSize: number) {
// Avoid making HTTP request or mocking giant manifest, stub what we need directly from request.
sinon.stub(LspController.prototype, 'fetchManifest')
// Directly feed the runtime specifications.
sinon.stub(LspController.prototype, 'getQserverFromManifest').returns(fakeQServerContent)
sinon.stub(LspController.prototype, 'getNodeRuntimeFromManifest').returns(fakeNodeContent)
// avoid fetch call.
sinon.stub(LspController.prototype, '_download').callsFake(getFakeDownload(numberOfFiles, fileSize))
// Hard code the hash since we are creating files on the spot, whose hashes can't be predicted.
sinon.stub(LspController.prototype, 'getFileSha384').resolves(fakeHash)
// Don't allow tryInstallLsp to move runtimes out of temporary folder.
sinon.stub(fs, 'rename')
}

/**
* Creates a fake zip with some files in it.
* @param filepath where to write the zip to.
* @param _content unused parameter, for compatability with real function.
*/
const getFakeDownload = function (numberOfFiles: number, fileSize: number) {
return async function (filepath: string, _content: Content) {
const dummyFilesPath = (
await createTestWorkspace(numberOfFiles, {
fileNamePrefix: 'fakeFile',
fileContent: getRandomString(fileSize),
workspaceName: 'workspace',
})
).uri.fsPath
await fs.writeFile(path.join(dummyFilesPath, 'qserver'), 'this value shouldnt matter')
const zip = new AdmZip()
zip.addLocalFolder(dummyFilesPath)
zip.writeZip(filepath)
}
}

describe('tryInstallLsp performance test', function () {
afterEach(function () {
sinon.restore()
})
/**
* Test setting up LSP when fetched zip is many (250) small (10B) files.
*/
performanceTest(
{
testRuns: 10,
linux: {
userCpuUsage: 100,
systemCpuUsage: 35,
heapTotal: 6,
duration: 15,
},
darwin: {
userCpuUsage: 100,
systemCpuUsage: 35,
heapTotal: 6,
duration: 15,
},
win32: {
userCpuUsage: 100,
systemCpuUsage: 35,
heapTotal: 6,
duration: 15,
},
},
'many small files in zip',
function () {
return {
setup: async () => {
createStubs(250, 10)
},
execute: async () => {
return await LspController.instance.tryInstallLsp(globals.context)
},
verify: async (_setup: any, result: boolean) => {
assert.ok(result)
},
}
}
)
/**
* Test setting up LSP when fetched zip is few (10) large (1000B) files.
*/
performanceTest(
{
testRuns: 10,
linux: {
userCpuUsage: 100,
systemCpuUsage: 50,
heapTotal: 6,
},
darwin: {
userCpuUsage: 100,
systemCpuUsage: 50,
heapTotal: 6,
},
win32: {
userCpuUsage: 100,
systemCpuUsage: 50,
heapTotal: 6,
},
},
'few large files in zip',
function () {
return {
setup: async () => {
createStubs(10, 1000)
},
execute: async () => {
return await LspController.instance.tryInstallLsp(globals.context)
},
verify: async (_setup: any, result: boolean) => {
assert.ok(result)
},
}
}
)
})
Loading