Skip to content

Commit a264377

Browse files
authored
fix(lsp): verify that nodejs runs before starting LSP client #7043
## Problem When lsp fetch/install/start fails it does not mention the download path, which could help with troubleshooting. #6972 [info] using amazonqWorkspaceLsp service configuration: default [info] lsp: Failed to download latest "AmazonQ-Workspace" manifest. Falling back to local manifest. [info] lsp: Finished setting up LSP server [info] [Error] Starting client failed [info] Error: write EPIPE ## Solution - Validate that `node` can actually run, before passing it to `LspClient`. - Add more logging. Also captured by telemetry: ``` 2025-04-16 08:24:51.738 [debug] telemetry: languageServer_setup { Metadata: { missingFields: 'id', metricId: '8da91a4b-ee00-4115-9b9e-796b5357402c', traceId: '8569c16e-d319-486e-a6f3-d4ee91698468', languageServerSetupStage: 'all', duration: '1417', result: 'Failed', reason: 'Error', reasonDesc: 'amazonqLsp: failed to run basic "node -e" test (exitcode=-2): [/Users/x/x/x/aws/x/x/x/x/x -e console.log("ok " + process.version)]', awsAccount: 'not-set', awsRegion: 'us-east-1' }, Value: 1, Unit: 'Milliseconds', Passive: true } ```
1 parent 1c91037 commit a264377

File tree

9 files changed

+166
-32
lines changed

9 files changed

+166
-32
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class AmazonQChatViewProvider implements WebviewViewProvider {
5050
) {
5151
this.webview = webviewView.webview
5252

53-
const lspDir = Uri.parse(LanguageServerResolver.defaultDir)
53+
const lspDir = Uri.parse(LanguageServerResolver.defaultDir())
5454
webviewView.webview.options = {
5555
enableScripts: true,
5656
enableCommandUris: true,

packages/amazonq/test/unit/amazonq/lsp/lspClient.test.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
*/
55
import * as sinon from 'sinon'
66
import assert from 'assert'
7-
import { globals } from 'aws-core-vscode/shared'
8-
import { LspClient } from 'aws-core-vscode/amazonq'
7+
import { globals, getNodeExecutableName } from 'aws-core-vscode/shared'
8+
import { LspClient, lspClient as lspClientModule } from 'aws-core-vscode/amazonq'
99

1010
describe('Amazon Q LSP client', function () {
1111
let lspClient: LspClient
@@ -50,6 +50,22 @@ describe('Amazon Q LSP client', function () {
5050
assert.ok(!encryptedSample.includes('hello'))
5151
})
5252

53+
it('validates node executable + lsp bundle', async () => {
54+
await assert.rejects(async () => {
55+
await lspClientModule.activate(globals.context, {
56+
// Mimic the `LspResolution<ResourcePaths>` type.
57+
node: 'node.bogus.exe',
58+
lsp: 'fake/lsp.js',
59+
})
60+
}, /.*failed to run basic .*node.*exitcode.*node\.bogus\.exe.*/)
61+
await assert.rejects(async () => {
62+
await lspClientModule.activate(globals.context, {
63+
node: getNodeExecutableName(),
64+
lsp: 'fake/lsp.js',
65+
})
66+
}, /.*failed to run .*exitcode.*node.*lsp\.js/)
67+
})
68+
5369
afterEach(() => {
5470
sinon.restore()
5571
})

packages/core/src/amazonq/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export {
1717
} from './onboardingPage/walkthrough'
1818
export { LspController } from './lsp/lspController'
1919
export { LspClient } from './lsp/lspClient'
20+
export * as lspClient from './lsp/lspClient'
2021
export { api } from './extApi'
2122
export { AmazonQChatViewProvider } from './webview/webView'
2223
export { init as cwChatAppInit } from '../codewhispererChat/app'

packages/core/src/amazonq/lsp/lspClient.ts

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,13 @@ import globals from '../../shared/extensionGlobals'
4141
import { ResourcePaths } from '../../shared/lsp/types'
4242
import { createServerOptions } from '../../shared/lsp/utils/platform'
4343
import { waitUntil } from '../../shared/utilities/timeoutUtils'
44+
import { ToolkitError } from '../../shared/errors'
45+
import { ChildProcess } from '../../shared/utilities/processUtils'
4446

4547
const localize = nls.loadMessageBundle()
4648

4749
const key = crypto.randomBytes(32)
50+
const logger = getLogger('amazonqLsp.lspClient')
4851

4952
/**
5053
* LspClient manages the API call between VS Code extension and LSP server
@@ -80,7 +83,7 @@ export class LspClient {
8083
const resp = await this.client?.sendRequest(BuildIndexRequestType, encryptedRequest)
8184
return resp
8285
} catch (e) {
83-
getLogger().error(`LspClient: buildIndex error: ${e}`)
86+
logger.error(`buildIndex error: ${e}`)
8487
return undefined
8588
}
8689
}
@@ -95,7 +98,7 @@ export class LspClient {
9598
const resp = await this.client?.sendRequest(QueryVectorIndexRequestType, encryptedRequest)
9699
return resp
97100
} catch (e) {
98-
getLogger().error(`LspClient: queryVectorIndex error: ${e}`)
101+
logger.error(`queryVectorIndex error: ${e}`)
99102
return []
100103
}
101104
}
@@ -111,7 +114,7 @@ export class LspClient {
111114
const resp: any = await this.client?.sendRequest(QueryInlineProjectContextRequestType, encrypted)
112115
return resp
113116
} catch (e) {
114-
getLogger().error(`LspClient: queryInlineProjectContext error: ${e}`)
117+
logger.error(`queryInlineProjectContext error: ${e}`)
115118
throw e
116119
}
117120
}
@@ -132,7 +135,7 @@ export class LspClient {
132135
const resp = await this.client?.sendRequest(UpdateIndexV2RequestType, encryptedRequest)
133136
return resp
134137
} catch (e) {
135-
getLogger().error(`LspClient: updateIndex error: ${e}`)
138+
logger.error(`updateIndex error: ${e}`)
136139
return undefined
137140
}
138141
}
@@ -144,7 +147,7 @@ export class LspClient {
144147
const resp: any = await this.client?.sendRequest(QueryRepomapIndexRequestType, await this.encrypt(request))
145148
return resp
146149
} catch (e) {
147-
getLogger().error(`LspClient: QueryRepomapIndex error: ${e}`)
150+
logger.error(`QueryRepomapIndex error: ${e}`)
148151
throw e
149152
}
150153
}
@@ -157,7 +160,7 @@ export class LspClient {
157160
)
158161
return resp
159162
} catch (e) {
160-
getLogger().error(`LspClient: queryInlineProjectContext error: ${e}`)
163+
logger.error(`queryInlineProjectContext error: ${e}`)
161164
throw e
162165
}
163166
}
@@ -174,7 +177,7 @@ export class LspClient {
174177
)
175178
return resp
176179
} catch (e) {
177-
getLogger().error(`LspClient: getContextCommandItems error: ${e}`)
180+
logger.error(`getContextCommandItems error: ${e}`)
178181
throw e
179182
}
180183
}
@@ -190,7 +193,7 @@ export class LspClient {
190193
)
191194
return resp || []
192195
} catch (e) {
193-
getLogger().error(`LspClient: getContextCommandPrompt error: ${e}`)
196+
logger.error(`getContextCommandPrompt error: ${e}`)
194197
throw e
195198
}
196199
}
@@ -204,7 +207,7 @@ export class LspClient {
204207
)
205208
return resp
206209
} catch (e) {
207-
getLogger().error(`LspClient: getIndexSequenceNumber error: ${e}`)
210+
logger.error(`getIndexSequenceNumber error: ${e}`)
208211
throw e
209212
}
210213
}
@@ -222,20 +225,65 @@ export class LspClient {
222225
)
223226
}
224227
}
228+
225229
/**
226-
* Activates the language server, this will start LSP server running over IPC protocol.
227-
* It will create a output channel named Amazon Q Language Server.
228-
* This function assumes the LSP server has already been downloaded.
230+
* Checks that we can actually run the `node` executable and execute code with it.
231+
*/
232+
async function validateNodeExe(nodePath: string, lsp: string, args: string[]) {
233+
// Check that we can start `node` by itself.
234+
const proc = new ChildProcess(nodePath, ['-e', 'console.log("ok " + process.version)'], { logging: 'no' })
235+
const r = await proc.run()
236+
const ok = r.exitCode === 0 && r.stdout.includes('ok')
237+
if (!ok) {
238+
const msg = `failed to run basic "node -e" test (exitcode=${r.exitCode}): ${proc}`
239+
logger.error(msg)
240+
throw new ToolkitError(`amazonqLsp: ${msg}`)
241+
}
242+
243+
// Check that we can start `node …/lsp.js --stdio …`.
244+
const lspProc = new ChildProcess(nodePath, [lsp, ...args], { logging: 'no' })
245+
try {
246+
// Start asynchronously (it never stops; we need to stop it below).
247+
lspProc.run().catch((e) => logger.error('failed to run: %s', lspProc))
248+
249+
const ok2 =
250+
!lspProc.stopped &&
251+
(await waitUntil(
252+
async () => {
253+
return lspProc.pid() !== undefined
254+
},
255+
{
256+
timeout: 5000,
257+
interval: 100,
258+
truthy: true,
259+
}
260+
))
261+
const selfExit = await waitUntil(async () => lspProc.stopped, {
262+
timeout: 500,
263+
interval: 100,
264+
truthy: true,
265+
})
266+
if (!ok2 || selfExit) {
267+
throw new ToolkitError(`amazonqLsp: failed to run (exitcode=${lspProc.exitCode()}): ${lspProc}`)
268+
}
269+
} finally {
270+
lspProc.stop(true)
271+
}
272+
}
273+
274+
/**
275+
* Activates the language server (assumes the LSP server has already been downloaded):
276+
* 1. start LSP server running over IPC protocol.
277+
* 2. create a output channel named Amazon Q Language Server.
229278
*/
230279
export async function activate(extensionContext: ExtensionContext, resourcePaths: ResourcePaths) {
231-
LspClient.instance
280+
LspClient.instance // Tickle the singleton... :/
232281
const toDispose = extensionContext.subscriptions
233282

234283
let rangeFormatting: Disposable | undefined
235284
// The debug options for the server
236285
// --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
237286
const debugOptions = { execArgv: ['--nolazy', '--preserve-symlinks', '--stdio'] }
238-
239287
const workerThreads = CodeWhispererSettings.instance.getIndexWorkerThreads()
240288
const gpu = CodeWhispererSettings.instance.isLocalIndexGPUEnabled()
241289

@@ -259,6 +307,7 @@ export async function activate(extensionContext: ExtensionContext, resourcePaths
259307
debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions },
260308
}
261309

310+
// TODO(jmkeyes): this overwrites the above...?
262311
serverOptions = createServerOptions({
263312
encryptionKey: key,
264313
executable: resourcePaths.node,
@@ -268,6 +317,8 @@ export async function activate(extensionContext: ExtensionContext, resourcePaths
268317

269318
const documentSelector = [{ scheme: 'file', language: '*' }]
270319

320+
await validateNodeExe(resourcePaths.node, resourcePaths.lsp, debugOptions.execArgv)
321+
271322
// Options to control the language client
272323
const clientOptions: LanguageClientOptions = {
273324
// Register the server for json documents
@@ -359,10 +410,15 @@ export async function activate(extensionContext: ExtensionContext, resourcePaths
359410
})
360411
)
361412

362-
return LspClient.instance.client.onReady().then(() => {
363-
const disposableFunc = { dispose: () => rangeFormatting?.dispose() as void }
364-
toDispose.push(disposableFunc)
365-
})
413+
return LspClient.instance.client.onReady().then(
414+
() => {
415+
const disposableFunc = { dispose: () => rangeFormatting?.dispose() as void }
416+
toDispose.push(disposableFunc)
417+
},
418+
(reason) => {
419+
logger.error('client.onReady() failed: %O', reason)
420+
}
421+
)
366422
}
367423

368424
export async function deactivate(): Promise<any> {

packages/core/src/amazonq/webview/generators/webViewContent.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { AuthUtil } from '../../../codewhisperer/util/authUtil'
99
import { FeatureConfigProvider, FeatureContext } from '../../../shared/featureConfig'
1010
import globals from '../../../shared/extensionGlobals'
1111
import { isSageMaker } from '../../../shared/extensionUtilities'
12-
import { RegionProfile } from '../../../codewhisperer/models/model'
1312
import { AmazonQPromptSettings } from '../../../shared/settings'
1413

1514
export class WebViewContentGenerator {
@@ -90,10 +89,10 @@ export class WebViewContentGenerator {
9089
// only show profile card when the two conditions
9190
// 1. profile count >= 2
9291
// 2. not default (fallback) which has empty arn
93-
let regionProfile: RegionProfile | undefined = AuthUtil.instance.regionProfileManager.activeRegionProfile
94-
if (AuthUtil.instance.regionProfileManager.profiles.length === 1) {
95-
regionProfile = undefined
96-
}
92+
const regionProfile =
93+
AuthUtil.instance.regionProfileManager.profiles.length === 1
94+
? undefined
95+
: AuthUtil.instance.regionProfileManager.activeRegionProfile
9796

9897
const regionProfileString: string = JSON.stringify(regionProfile)
9998
const authState = (await AuthUtil.instance.getChatAuthState()).amazonQ
@@ -104,7 +103,7 @@ export class WebViewContentGenerator {
104103
<script type="text/javascript">
105104
const init = () => {
106105
createMynahUI(
107-
acquireVsCodeApi(),
106+
acquireVsCodeApi(),
108107
${authState === 'connected'},
109108
${featureConfigsString},
110109
${welcomeLoadCount},

packages/core/src/shared/logger/logger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type LogTopic =
1414
| 'lsp'
1515
| 'amazonqWorkspaceLsp'
1616
| 'amazonqLsp'
17+
| 'amazonqLsp.lspClient'
1718
| 'chat'
1819
| 'stepfunctions'
1920
| 'unknown'

packages/core/src/shared/lsp/baseLspInstaller.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,19 @@ export abstract class BaseLspInstaller<T extends ResourcePaths = ResourcePaths,
5454
const deletedVersions = await cleanLspDownloads(manifest.versions, nodePath.dirname(assetDirectory))
5555
this.logger.debug(`cleaning old LSP versions deleted ${deletedVersions.length} versions`)
5656

57-
return {
57+
const r = {
5858
...installationResult,
59+
// Example:
60+
// ```
61+
// resourcePaths = {
62+
// lsp = '<cachedir>/aws/toolkits/language-servers/AmazonQ/3.3.0/servers/aws-lsp-codewhisperer.js'
63+
// node = '<cachedir>/aws/toolkits/language-servers/AmazonQ/3.3.0/servers/node'
64+
// ui = '<cachedir>/aws/toolkits/language-servers/AmazonQ/3.3.0/clients/amazonq-ui.js'
65+
// }
66+
// ```
5967
resourcePaths: this.resourcePaths(assetDirectory),
6068
}
69+
return r
6170
}
6271

6372
protected abstract postInstall(assetDirectory: string): Promise<void>

packages/core/src/shared/lsp/lspResolver.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,25 +47,40 @@ export class LanguageServerResolver {
4747

4848
const serverResolvers: StageResolver<LspResult>[] = [
4949
{
50+
// 1: Use the current local ("cached") LSP server bundle, if any.
5051
resolve: async () => await this.getLocalServer(cacheDirectory, latestVersion, targetContents),
5152
telemetryMetadata: { id: this.lsName, languageServerLocation: 'cache' },
5253
},
5354
{
55+
// 2: Download the latest LSP server bundle.
5456
resolve: async () => await this.fetchRemoteServer(cacheDirectory, latestVersion, targetContents),
5557
telemetryMetadata: { id: this.lsName, languageServerLocation: 'remote' },
5658
},
5759
{
60+
// 3: If the download fails, try an older, cached version.
5861
resolve: async () => await this.getFallbackServer(latestVersion),
5962
telemetryMetadata: { id: this.lsName, languageServerLocation: 'fallback' },
6063
},
6164
]
6265

63-
return await tryStageResolvers('getServer', serverResolvers, getServerVersion)
66+
/**
67+
* Example:
68+
* ```
69+
* LspResult {
70+
* assetDirectory = "<cachedir>/aws/toolkits/language-servers/AmazonQ/3.3.0"
71+
* location = 'cache'
72+
* version = '3.3.0'
73+
* }
74+
* ```
75+
*/
76+
const resolved = await tryStageResolvers('getServer', serverResolvers, getServerVersion)
77+
return resolved
6478
} finally {
6579
logger.info(`Finished setting up LSP server`)
6680
}
6781
}
6882

83+
/** Finds an older, cached version of the LSP server bundle. */
6984
private async getFallbackServer(latestVersion: LspVersion): Promise<LspResult> {
7085
const fallbackDirectory = await this.getFallbackDir(latestVersion.serverVersion)
7186
if (!fallbackDirectory) {
@@ -96,6 +111,7 @@ export class LanguageServerResolver {
96111
return timeout
97112
}
98113

114+
/** Downloads the latest LSP server bundle. */
99115
private async fetchRemoteServer(
100116
cacheDirectory: string,
101117
latestVersion: LspVersion,
@@ -117,6 +133,7 @@ export class LanguageServerResolver {
117133
}
118134
}
119135

136+
/** Gets the current local ("cached") LSP server bundle. */
120137
private async getLocalServer(
121138
cacheDirectory: string,
122139
latestVersion: LspVersion,
@@ -417,13 +434,17 @@ export class LanguageServerResolver {
417434
return version.targets.find((x) => x.arch === arch && x.platform === platform)
418435
}
419436

420-
// lazy calls to `getApplicationSupportFolder()` to avoid failure on windows.
421-
public static get defaultDir() {
437+
/**
438+
* Gets platform-specific "cache" dir ("$LOCALAPPDATA/aws/…" or "~/.cache/aws/…").
439+
*
440+
* Lazy-calls `getCacheDir()` to avoid failure on Windows.
441+
*/
442+
public static defaultDir() {
422443
return path.join(fs.getCacheDir(), `aws/toolkits/language-servers`)
423444
}
424445

425446
defaultDownloadFolder() {
426-
return path.join(LanguageServerResolver.defaultDir, `${this.lsName}`)
447+
return path.join(LanguageServerResolver.defaultDir(), `${this.lsName}`)
427448
}
428449

429450
private getDownloadDirectory(version: string) {

0 commit comments

Comments
 (0)