From a26437735b08d60fa21e7db9269ad6f2c5a3c1bb Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Wed, 16 Apr 2025 08:38:17 -0700 Subject: [PATCH 01/13] 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 } ``` --- .../amazonq/src/lsp/chat/webviewProvider.ts | 2 +- .../test/unit/amazonq/lsp/lspClient.test.ts | 20 +++- packages/core/src/amazonq/index.ts | 1 + packages/core/src/amazonq/lsp/lspClient.ts | 92 +++++++++++++++---- .../webview/generators/webViewContent.ts | 11 +-- packages/core/src/shared/logger/logger.ts | 1 + .../core/src/shared/lsp/baseLspInstaller.ts | 11 ++- packages/core/src/shared/lsp/lspResolver.ts | 29 +++++- packages/core/src/shared/lsp/types.ts | 31 +++++++ 9 files changed, 166 insertions(+), 32 deletions(-) diff --git a/packages/amazonq/src/lsp/chat/webviewProvider.ts b/packages/amazonq/src/lsp/chat/webviewProvider.ts index 426af63739d..e1eb3215395 100644 --- a/packages/amazonq/src/lsp/chat/webviewProvider.ts +++ b/packages/amazonq/src/lsp/chat/webviewProvider.ts @@ -50,7 +50,7 @@ export class AmazonQChatViewProvider implements WebviewViewProvider { ) { this.webview = webviewView.webview - const lspDir = Uri.parse(LanguageServerResolver.defaultDir) + const lspDir = Uri.parse(LanguageServerResolver.defaultDir()) webviewView.webview.options = { enableScripts: true, enableCommandUris: true, diff --git a/packages/amazonq/test/unit/amazonq/lsp/lspClient.test.ts b/packages/amazonq/test/unit/amazonq/lsp/lspClient.test.ts index 0ffb6adc907..369cda5402d 100644 --- a/packages/amazonq/test/unit/amazonq/lsp/lspClient.test.ts +++ b/packages/amazonq/test/unit/amazonq/lsp/lspClient.test.ts @@ -4,8 +4,8 @@ */ import * as sinon from 'sinon' import assert from 'assert' -import { globals } from 'aws-core-vscode/shared' -import { LspClient } from 'aws-core-vscode/amazonq' +import { globals, getNodeExecutableName } from 'aws-core-vscode/shared' +import { LspClient, lspClient as lspClientModule } from 'aws-core-vscode/amazonq' describe('Amazon Q LSP client', function () { let lspClient: LspClient @@ -50,6 +50,22 @@ describe('Amazon Q LSP client', function () { assert.ok(!encryptedSample.includes('hello')) }) + it('validates node executable + lsp bundle', async () => { + await assert.rejects(async () => { + await lspClientModule.activate(globals.context, { + // Mimic the `LspResolution` type. + node: 'node.bogus.exe', + lsp: 'fake/lsp.js', + }) + }, /.*failed to run basic .*node.*exitcode.*node\.bogus\.exe.*/) + await assert.rejects(async () => { + await lspClientModule.activate(globals.context, { + node: getNodeExecutableName(), + lsp: 'fake/lsp.js', + }) + }, /.*failed to run .*exitcode.*node.*lsp\.js/) + }) + afterEach(() => { sinon.restore() }) diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts index 70223bf72d7..06af40f2c29 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -17,6 +17,7 @@ export { } from './onboardingPage/walkthrough' export { LspController } from './lsp/lspController' export { LspClient } from './lsp/lspClient' +export * as lspClient from './lsp/lspClient' export { api } from './extApi' export { AmazonQChatViewProvider } from './webview/webView' export { init as cwChatAppInit } from '../codewhispererChat/app' diff --git a/packages/core/src/amazonq/lsp/lspClient.ts b/packages/core/src/amazonq/lsp/lspClient.ts index f2bfaca8d8a..61e19333f43 100644 --- a/packages/core/src/amazonq/lsp/lspClient.ts +++ b/packages/core/src/amazonq/lsp/lspClient.ts @@ -41,10 +41,13 @@ import globals from '../../shared/extensionGlobals' import { ResourcePaths } from '../../shared/lsp/types' import { createServerOptions } from '../../shared/lsp/utils/platform' import { waitUntil } from '../../shared/utilities/timeoutUtils' +import { ToolkitError } from '../../shared/errors' +import { ChildProcess } from '../../shared/utilities/processUtils' const localize = nls.loadMessageBundle() const key = crypto.randomBytes(32) +const logger = getLogger('amazonqLsp.lspClient') /** * LspClient manages the API call between VS Code extension and LSP server @@ -80,7 +83,7 @@ export class LspClient { const resp = await this.client?.sendRequest(BuildIndexRequestType, encryptedRequest) return resp } catch (e) { - getLogger().error(`LspClient: buildIndex error: ${e}`) + logger.error(`buildIndex error: ${e}`) return undefined } } @@ -95,7 +98,7 @@ export class LspClient { const resp = await this.client?.sendRequest(QueryVectorIndexRequestType, encryptedRequest) return resp } catch (e) { - getLogger().error(`LspClient: queryVectorIndex error: ${e}`) + logger.error(`queryVectorIndex error: ${e}`) return [] } } @@ -111,7 +114,7 @@ export class LspClient { const resp: any = await this.client?.sendRequest(QueryInlineProjectContextRequestType, encrypted) return resp } catch (e) { - getLogger().error(`LspClient: queryInlineProjectContext error: ${e}`) + logger.error(`queryInlineProjectContext error: ${e}`) throw e } } @@ -132,7 +135,7 @@ export class LspClient { const resp = await this.client?.sendRequest(UpdateIndexV2RequestType, encryptedRequest) return resp } catch (e) { - getLogger().error(`LspClient: updateIndex error: ${e}`) + logger.error(`updateIndex error: ${e}`) return undefined } } @@ -144,7 +147,7 @@ export class LspClient { const resp: any = await this.client?.sendRequest(QueryRepomapIndexRequestType, await this.encrypt(request)) return resp } catch (e) { - getLogger().error(`LspClient: QueryRepomapIndex error: ${e}`) + logger.error(`QueryRepomapIndex error: ${e}`) throw e } } @@ -157,7 +160,7 @@ export class LspClient { ) return resp } catch (e) { - getLogger().error(`LspClient: queryInlineProjectContext error: ${e}`) + logger.error(`queryInlineProjectContext error: ${e}`) throw e } } @@ -174,7 +177,7 @@ export class LspClient { ) return resp } catch (e) { - getLogger().error(`LspClient: getContextCommandItems error: ${e}`) + logger.error(`getContextCommandItems error: ${e}`) throw e } } @@ -190,7 +193,7 @@ export class LspClient { ) return resp || [] } catch (e) { - getLogger().error(`LspClient: getContextCommandPrompt error: ${e}`) + logger.error(`getContextCommandPrompt error: ${e}`) throw e } } @@ -204,7 +207,7 @@ export class LspClient { ) return resp } catch (e) { - getLogger().error(`LspClient: getIndexSequenceNumber error: ${e}`) + logger.error(`getIndexSequenceNumber error: ${e}`) throw e } } @@ -222,20 +225,65 @@ export class LspClient { ) } } + /** - * Activates the language server, this will start LSP server running over IPC protocol. - * It will create a output channel named Amazon Q Language Server. - * This function assumes the LSP server has already been downloaded. + * Checks that we can actually run the `node` executable and execute code with it. + */ +async function validateNodeExe(nodePath: string, lsp: string, args: string[]) { + // Check that we can start `node` by itself. + const proc = new ChildProcess(nodePath, ['-e', 'console.log("ok " + process.version)'], { logging: 'no' }) + const r = await proc.run() + const ok = r.exitCode === 0 && r.stdout.includes('ok') + if (!ok) { + const msg = `failed to run basic "node -e" test (exitcode=${r.exitCode}): ${proc}` + logger.error(msg) + throw new ToolkitError(`amazonqLsp: ${msg}`) + } + + // Check that we can start `node …/lsp.js --stdio …`. + const lspProc = new ChildProcess(nodePath, [lsp, ...args], { logging: 'no' }) + try { + // Start asynchronously (it never stops; we need to stop it below). + lspProc.run().catch((e) => logger.error('failed to run: %s', lspProc)) + + const ok2 = + !lspProc.stopped && + (await waitUntil( + async () => { + return lspProc.pid() !== undefined + }, + { + timeout: 5000, + interval: 100, + truthy: true, + } + )) + const selfExit = await waitUntil(async () => lspProc.stopped, { + timeout: 500, + interval: 100, + truthy: true, + }) + if (!ok2 || selfExit) { + throw new ToolkitError(`amazonqLsp: failed to run (exitcode=${lspProc.exitCode()}): ${lspProc}`) + } + } finally { + lspProc.stop(true) + } +} + +/** + * Activates the language server (assumes the LSP server has already been downloaded): + * 1. start LSP server running over IPC protocol. + * 2. create a output channel named Amazon Q Language Server. */ export async function activate(extensionContext: ExtensionContext, resourcePaths: ResourcePaths) { - LspClient.instance + LspClient.instance // Tickle the singleton... :/ const toDispose = extensionContext.subscriptions let rangeFormatting: Disposable | undefined // The debug options for the server // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging const debugOptions = { execArgv: ['--nolazy', '--preserve-symlinks', '--stdio'] } - const workerThreads = CodeWhispererSettings.instance.getIndexWorkerThreads() const gpu = CodeWhispererSettings.instance.isLocalIndexGPUEnabled() @@ -259,6 +307,7 @@ export async function activate(extensionContext: ExtensionContext, resourcePaths debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }, } + // TODO(jmkeyes): this overwrites the above...? serverOptions = createServerOptions({ encryptionKey: key, executable: resourcePaths.node, @@ -268,6 +317,8 @@ export async function activate(extensionContext: ExtensionContext, resourcePaths const documentSelector = [{ scheme: 'file', language: '*' }] + await validateNodeExe(resourcePaths.node, resourcePaths.lsp, debugOptions.execArgv) + // Options to control the language client const clientOptions: LanguageClientOptions = { // Register the server for json documents @@ -359,10 +410,15 @@ export async function activate(extensionContext: ExtensionContext, resourcePaths }) ) - return LspClient.instance.client.onReady().then(() => { - const disposableFunc = { dispose: () => rangeFormatting?.dispose() as void } - toDispose.push(disposableFunc) - }) + return LspClient.instance.client.onReady().then( + () => { + const disposableFunc = { dispose: () => rangeFormatting?.dispose() as void } + toDispose.push(disposableFunc) + }, + (reason) => { + logger.error('client.onReady() failed: %O', reason) + } + ) } export async function deactivate(): Promise { diff --git a/packages/core/src/amazonq/webview/generators/webViewContent.ts b/packages/core/src/amazonq/webview/generators/webViewContent.ts index ef1d37a705e..ae6c30f4f6a 100644 --- a/packages/core/src/amazonq/webview/generators/webViewContent.ts +++ b/packages/core/src/amazonq/webview/generators/webViewContent.ts @@ -9,7 +9,6 @@ import { AuthUtil } from '../../../codewhisperer/util/authUtil' import { FeatureConfigProvider, FeatureContext } from '../../../shared/featureConfig' import globals from '../../../shared/extensionGlobals' import { isSageMaker } from '../../../shared/extensionUtilities' -import { RegionProfile } from '../../../codewhisperer/models/model' import { AmazonQPromptSettings } from '../../../shared/settings' export class WebViewContentGenerator { @@ -90,10 +89,10 @@ export class WebViewContentGenerator { // only show profile card when the two conditions // 1. profile count >= 2 // 2. not default (fallback) which has empty arn - let regionProfile: RegionProfile | undefined = AuthUtil.instance.regionProfileManager.activeRegionProfile - if (AuthUtil.instance.regionProfileManager.profiles.length === 1) { - regionProfile = undefined - } + const regionProfile = + AuthUtil.instance.regionProfileManager.profiles.length === 1 + ? undefined + : AuthUtil.instance.regionProfileManager.activeRegionProfile const regionProfileString: string = JSON.stringify(regionProfile) const authState = (await AuthUtil.instance.getChatAuthState()).amazonQ @@ -104,7 +103,7 @@ export class WebViewContentGenerator { diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index 12341ff17ab..51929e97fda 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -61,6 +61,7 @@ export async function startLanguageServer( awsClientCapabilities: { window: { notifications: true, + showSaveFileDialog: true, }, }, }, From e0dbbacdd5ad12fce187dec1dfacd717cba11d1f Mon Sep 17 00:00:00 2001 From: Josh Pinkney Date: Thu, 17 Apr 2025 08:57:26 -0400 Subject: [PATCH 12/13] fix(amazonq): acknowledgement is not adding to suppressPrompts when pressed --- packages/amazonq/src/lsp/chat/messages.ts | 2 +- packages/core/src/amazonq/webview/messages/messageDispatcher.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index 1a9d1078533..8e4fd2af30f 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -138,7 +138,7 @@ export function registerMessageListeners( break } case DISCLAIMER_ACKNOWLEDGED: { - void AmazonQPromptSettings.instance.disablePrompt('amazonQChatDisclaimer') + void AmazonQPromptSettings.instance.update('amazonQChatDisclaimer', true) break } case chatRequestType.method: { diff --git a/packages/core/src/amazonq/webview/messages/messageDispatcher.ts b/packages/core/src/amazonq/webview/messages/messageDispatcher.ts index 10a59c355e1..56d4c8503b9 100644 --- a/packages/core/src/amazonq/webview/messages/messageDispatcher.ts +++ b/packages/core/src/amazonq/webview/messages/messageDispatcher.ts @@ -96,7 +96,7 @@ export function handleWebviewEvent(msg: any, webViewToAppsMessagePublishers: Map return } case 'disclaimer-acknowledged': { - void AmazonQPromptSettings.instance.disablePrompt('amazonQChatDisclaimer') + void AmazonQPromptSettings.instance.update('amazonQChatDisclaimer', true) return } case 'update-welcome-count': { From d457022377ad804f8a9c99d3f4964123c31deead Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Thu, 17 Apr 2025 08:52:08 -0700 Subject: [PATCH 13/13] fix(amazonq): chat disclaimer acknowledgement not recognized #7079 --- packages/amazonq/src/lsp/chat/webviewProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/amazonq/src/lsp/chat/webviewProvider.ts b/packages/amazonq/src/lsp/chat/webviewProvider.ts index e1eb3215395..ee73a599867 100644 --- a/packages/amazonq/src/lsp/chat/webviewProvider.ts +++ b/packages/amazonq/src/lsp/chat/webviewProvider.ts @@ -64,7 +64,7 @@ export class AmazonQChatViewProvider implements WebviewViewProvider { } private async getWebviewContent(mynahUIPath: string) { - const disclaimerAcknowledged = AmazonQPromptSettings.instance.isPromptEnabled('amazonQChatDisclaimer') + const disclaimerAcknowledged = !AmazonQPromptSettings.instance.isPromptEnabled('amazonQChatDisclaimer') return `