Skip to content

Commit fc03752

Browse files
Merge master into feature/LSP-alpha
2 parents eb99105 + 62d50d9 commit fc03752

File tree

6 files changed

+223
-7
lines changed

6 files changed

+223
-7
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Launch LSP with bundled artifacts as fallback"
4+
}

packages/amazonq/src/lsp/activation.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
import vscode from 'vscode'
77
import { startLanguageServer } from './client'
8-
import { AmazonQLspInstaller } from './lspInstaller'
9-
import { lspSetupStage, ToolkitError, messages } from 'aws-core-vscode/shared'
8+
import { AmazonQLspInstaller, getBundledResourcePaths } from './lspInstaller'
9+
import { lspSetupStage, ToolkitError, messages, getLogger } from 'aws-core-vscode/shared'
1010

1111
export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
1212
try {
@@ -16,6 +16,15 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
1616
})
1717
} catch (err) {
1818
const e = err as ToolkitError
19-
void messages.showViewLogsMessage(`Failed to launch Amazon Q language server: ${e.message}`)
19+
getLogger('amazonqLsp').warn(`Failed to start downloaded LSP, falling back to bundled LSP: ${e.message}`)
20+
try {
21+
await lspSetupStage('all', async () => {
22+
await lspSetupStage('launch', async () => await startLanguageServer(ctx, getBundledResourcePaths(ctx)))
23+
})
24+
} catch (error) {
25+
void messages.showViewLogsMessage(
26+
`Failed to launch Amazon Q language server: ${(error as ToolkitError).message}`
27+
)
28+
}
2029
}
2130
}

packages/amazonq/src/lsp/chat/webviewProvider.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
AmazonQPromptSettings,
2020
LanguageServerResolver,
2121
amazonqMark,
22+
getLogger,
2223
} from 'aws-core-vscode/shared'
2324
import { AuthUtil, RegionProfile } from 'aws-core-vscode/codewhisperer'
2425
import { featureConfig } from 'aws-core-vscode/amazonq'
@@ -44,9 +45,12 @@ export class AmazonQChatViewProvider implements WebviewViewProvider {
4445
) {
4546
const lspDir = Uri.file(LanguageServerResolver.defaultDir())
4647
const dist = Uri.joinPath(globals.context.extensionUri, 'dist')
47-
48-
const resourcesRoots = [lspDir, dist]
49-
48+
const bundledResources = Uri.joinPath(globals.context.extensionUri, 'resources/language-server')
49+
let resourcesRoots = [lspDir, dist]
50+
if (this.mynahUIPath?.startsWith(globals.context.extensionUri.fsPath)) {
51+
getLogger('amazonqLsp').info(`Using bundled webview resources ${bundledResources.fsPath}`)
52+
resourcesRoots = [bundledResources, dist]
53+
}
5054
/**
5155
* if the mynah chat client is defined, then make sure to add it to the resource roots, otherwise
5256
* it will 401 when trying to load

packages/amazonq/src/lsp/lspInstaller.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6+
import vscode from 'vscode'
67
import { fs, getNodeExecutableName, getRgExecutableName, BaseLspInstaller, ResourcePaths } from 'aws-core-vscode/shared'
78
import path from 'path'
89
import { ExtendedAmazonQLSPConfig, getAmazonQLspConfig } from './config'
@@ -54,3 +55,13 @@ export class AmazonQLspInstaller extends BaseLspInstaller.BaseLspInstaller<
5455

5556
protected override downloadMessageOverride: string | undefined = 'Updating Amazon Q plugin'
5657
}
58+
59+
export function getBundledResourcePaths(ctx: vscode.ExtensionContext): AmazonQResourcePaths {
60+
const assetDirectory = vscode.Uri.joinPath(ctx.extensionUri, 'resources', 'language-server').fsPath
61+
return {
62+
lsp: path.join(assetDirectory, 'servers', 'aws-lsp-codewhisperer.js'),
63+
node: process.execPath,
64+
ripGrep: '',
65+
ui: path.join(assetDirectory, 'clients', 'amazonq-ui.js'),
66+
}
67+
}

scripts/lspArtifact.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import * as https from 'https'
2+
import * as fs from 'fs'
3+
import * as crypto from 'crypto'
4+
import * as path from 'path'
5+
import * as os from 'os'
6+
import AdmZip from 'adm-zip'
7+
8+
interface ManifestContent {
9+
filename: string
10+
url: string
11+
hashes: string[]
12+
bytes: number
13+
}
14+
15+
interface ManifestTarget {
16+
platform: string
17+
arch: string
18+
contents: ManifestContent[]
19+
}
20+
21+
interface ManifestVersion {
22+
serverVersion: string
23+
isDelisted: boolean
24+
targets: ManifestTarget[]
25+
}
26+
27+
interface Manifest {
28+
versions: ManifestVersion[]
29+
}
30+
async function verifyFileHash(filePath: string, expectedHash: string): Promise<boolean> {
31+
return new Promise((resolve, reject) => {
32+
const hash = crypto.createHash('sha384')
33+
const stream = fs.createReadStream(filePath)
34+
35+
stream.on('data', (data) => {
36+
hash.update(data)
37+
})
38+
39+
stream.on('end', () => {
40+
const fileHash = hash.digest('hex')
41+
// Remove 'sha384:' prefix from expected hash if present
42+
const expectedHashValue = expectedHash.replace('sha384:', '')
43+
resolve(fileHash === expectedHashValue)
44+
})
45+
46+
stream.on('error', reject)
47+
})
48+
}
49+
50+
async function ensureDirectoryExists(dirPath: string): Promise<void> {
51+
if (!fs.existsSync(dirPath)) {
52+
await fs.promises.mkdir(dirPath, { recursive: true })
53+
}
54+
}
55+
56+
export async function downloadLanguageServer(): Promise<void> {
57+
const tempDir = path.join(os.tmpdir(), 'amazonq-download-temp')
58+
const resourcesDir = path.join(__dirname, '../packages/amazonq/resources/language-server')
59+
60+
// clear previous cached language server
61+
try {
62+
if (fs.existsSync(resourcesDir)) {
63+
fs.rmdirSync(resourcesDir, { recursive: true })
64+
}
65+
} catch (e) {
66+
throw Error(`Failed to clean up language server ${resourcesDir}`)
67+
}
68+
69+
await ensureDirectoryExists(tempDir)
70+
await ensureDirectoryExists(resourcesDir)
71+
72+
return new Promise((resolve, reject) => {
73+
const manifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/qAgenticChatServer/0/manifest.json'
74+
75+
https
76+
.get(manifestUrl, (res) => {
77+
let data = ''
78+
79+
res.on('data', (chunk) => {
80+
data += chunk
81+
})
82+
83+
res.on('end', async () => {
84+
try {
85+
const manifest: Manifest = JSON.parse(data)
86+
87+
const latestVersion = manifest.versions
88+
.filter((v) => !v.isDelisted)
89+
.sort((a, b) => b.serverVersion.localeCompare(a.serverVersion))[0]
90+
91+
if (!latestVersion) {
92+
throw new Error('No valid version found in manifest')
93+
}
94+
95+
const darwinArm64Target = latestVersion.targets.find(
96+
(t) => t.platform === 'darwin' && t.arch === 'arm64'
97+
)
98+
99+
if (!darwinArm64Target) {
100+
throw new Error('No darwin arm64 target found')
101+
}
102+
103+
for (const content of darwinArm64Target.contents) {
104+
const fileName = content.filename
105+
const fileUrl = content.url
106+
const expectedHash = content.hashes[0]
107+
const tempFilePath = path.join(tempDir, fileName)
108+
const fileFolderName = content.filename.replace('.zip', '')
109+
110+
console.log(`Downloading ${fileName} from ${fileUrl} ...`)
111+
112+
await new Promise((downloadResolve, downloadReject) => {
113+
https
114+
.get(fileUrl, (fileRes) => {
115+
const fileStream = fs.createWriteStream(tempFilePath)
116+
fileRes.pipe(fileStream)
117+
118+
fileStream.on('finish', () => {
119+
fileStream.close()
120+
downloadResolve(void 0)
121+
})
122+
123+
fileStream.on('error', (err) => {
124+
fs.unlink(tempFilePath, () => {})
125+
downloadReject(err)
126+
})
127+
})
128+
.on('error', (err) => {
129+
fs.unlink(tempFilePath, () => {})
130+
downloadReject(err)
131+
})
132+
})
133+
134+
console.log(`Verifying hash for ${fileName}...`)
135+
const isHashValid = await verifyFileHash(tempFilePath, expectedHash)
136+
137+
if (!isHashValid) {
138+
fs.unlinkSync(tempFilePath)
139+
throw new Error(`Hash verification failed for ${fileName}`)
140+
}
141+
142+
console.log(`Extracting ${fileName}...`)
143+
const zip = new AdmZip(tempFilePath)
144+
zip.extractAllTo(path.join(resourcesDir, fileFolderName), true) // true for overwrite
145+
146+
// Clean up temp file
147+
fs.unlinkSync(tempFilePath)
148+
console.log(`Successfully processed ${fileName}`)
149+
}
150+
151+
// Clean up temp directory
152+
fs.rmdirSync(tempDir)
153+
fs.rmdirSync(path.join(resourcesDir, 'servers', 'indexing'), { recursive: true })
154+
fs.rmdirSync(path.join(resourcesDir, 'servers', 'ripgrep'), { recursive: true })
155+
fs.rmSync(path.join(resourcesDir, 'servers', 'node'))
156+
if (!fs.existsSync(path.join(resourcesDir, 'servers', 'aws-lsp-codewhisperer.js'))) {
157+
throw new Error(`Extracting aws-lsp-codewhisperer.js failure`)
158+
}
159+
if (!fs.existsSync(path.join(resourcesDir, 'clients', 'amazonq-ui.js'))) {
160+
throw new Error(`Extracting amazonq-ui.js failure`)
161+
}
162+
console.log('Download and extraction completed successfully')
163+
resolve()
164+
} catch (err) {
165+
// Clean up temp directory on error
166+
if (fs.existsSync(tempDir)) {
167+
fs.rmdirSync(tempDir, { recursive: true })
168+
}
169+
reject(err)
170+
}
171+
})
172+
})
173+
.on('error', (err) => {
174+
// Clean up temp directory on error
175+
if (fs.existsSync(tempDir)) {
176+
fs.rmdirSync(tempDir, { recursive: true })
177+
}
178+
reject(err)
179+
})
180+
})
181+
}

scripts/package.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import * as child_process from 'child_process' // eslint-disable-line no-restricted-imports
2121
import * as nodefs from 'fs' // eslint-disable-line no-restricted-imports
2222
import * as path from 'path'
23+
import { downloadLanguageServer } from './lspArtifact'
2324

2425
function parseArgs() {
2526
// Invoking this script with argument "foo":
@@ -105,7 +106,7 @@ function getVersionSuffix(feature: string, debug: boolean): string {
105106
return `${debugSuffix}${featureSuffix}${commitSuffix}`
106107
}
107108

108-
function main() {
109+
async function main() {
109110
const args = parseArgs()
110111
// It is expected that this will package from a packages/{subproject} folder.
111112
// There is a base config in packages/
@@ -155,6 +156,12 @@ function main() {
155156
}
156157

157158
nodefs.writeFileSync(packageJsonFile, JSON.stringify(packageJson, undefined, ' '))
159+
160+
// add language server bundle
161+
if (packageJson.name === 'amazon-q-vscode') {
162+
await downloadLanguageServer()
163+
}
164+
158165
child_process.execFileSync(
159166
'vsce',
160167
[

0 commit comments

Comments
 (0)