diff --git a/packages/amazonq/src/lsp/activation.ts b/packages/amazonq/src/lsp/activation.ts index c4855c17970..84bae8a01a6 100644 --- a/packages/amazonq/src/lsp/activation.ts +++ b/packages/amazonq/src/lsp/activation.ts @@ -6,7 +6,7 @@ import vscode from 'vscode' import { startLanguageServer } from './client' import { AmazonQLspInstaller } from './lspInstaller' -import { lspSetupStage, ToolkitError } from 'aws-core-vscode/shared' +import { lspSetupStage, ToolkitError, messages } from 'aws-core-vscode/shared' export async function activate(ctx: vscode.ExtensionContext): Promise { try { @@ -16,6 +16,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { }) } catch (err) { const e = err as ToolkitError - void vscode.window.showInformationMessage(`Unable to launch amazonq language server: ${e.message}`) + void messages.showViewLogsMessage(`Failed to launch Amazon Q language server: ${e.message}`) } } diff --git a/packages/core/src/amazonq/lsp/lspClient.ts b/packages/core/src/amazonq/lsp/lspClient.ts index 61e19333f43..767af8dd5ee 100644 --- a/packages/core/src/amazonq/lsp/lspClient.ts +++ b/packages/core/src/amazonq/lsp/lspClient.ts @@ -15,7 +15,7 @@ import * as jose from 'jose' import { Disposable, ExtensionContext } from 'vscode' -import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient' +import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient' import { BuildIndexRequestPayload, BuildIndexRequestType, @@ -235,7 +235,7 @@ async function validateNodeExe(nodePath: string, lsp: string, args: string[]) { 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}` + const msg = `failed to run basic "node -e" test (exitcode=${r.exitCode}): ${proc.toString(false, true)}` logger.error(msg) throw new ToolkitError(`amazonqLsp: ${msg}`) } @@ -244,7 +244,7 @@ async function validateNodeExe(nodePath: string, lsp: string, args: string[]) { 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)) + lspProc.run().catch((e) => logger.error('failed to run: %s', lspProc.toString(false, true))) const ok2 = !lspProc.stopped && @@ -264,7 +264,9 @@ async function validateNodeExe(nodePath: string, lsp: string, args: string[]) { truthy: true, }) if (!ok2 || selfExit) { - throw new ToolkitError(`amazonqLsp: failed to run (exitcode=${lspProc.exitCode()}): ${lspProc}`) + throw new ToolkitError( + `amazonqLsp: failed to run (exitcode=${lspProc.exitCode()}): ${lspProc.toString(false, true)}` + ) } } finally { lspProc.stop(true) @@ -300,18 +302,11 @@ export async function activate(extensionContext: ExtensionContext, resourcePaths const serverModule = resourcePaths.lsp - // If the extension is launch in debug mode the debug server options are use - // Otherwise the run options are used - let serverOptions: ServerOptions = { - run: { module: serverModule, transport: TransportKind.ipc }, - debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }, - } - - // TODO(jmkeyes): this overwrites the above...? - serverOptions = createServerOptions({ + const serverOptions = createServerOptions({ encryptionKey: key, executable: resourcePaths.node, serverModule, + // TODO(jmkeyes): we always use the debug options...? execArgv: debugOptions.execArgv, }) diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts index f20a415b305..3b7bd98a61d 100644 --- a/packages/core/src/amazonq/lsp/lspController.ts +++ b/packages/core/src/amazonq/lsp/lspController.ts @@ -98,11 +98,11 @@ export class LspController { } async buildIndex(buildIndexConfig: BuildIndexConfig) { - this.logger.info(`LspController: Starting to build index of project`) + this.logger.info(`Starting to build index of project`) const start = performance.now() const projPaths = (vscode.workspace.workspaceFolders ?? []).map((folder) => folder.uri.fsPath) if (projPaths.length === 0) { - this.logger.info(`LspController: Skipping building index. No projects found in workspace`) + this.logger.info(`Skipping building index. No projects found in workspace`) return } projPaths.sort() @@ -119,12 +119,12 @@ export class LspController { (accumulator, currentFile) => accumulator + currentFile.fileSizeBytes, 0 ) - this.logger.info(`LspController: Found ${files.length} files in current project ${projPaths}`) + this.logger.info(`Found ${files.length} files in current project ${projPaths}`) const config = buildIndexConfig.isVectorIndexEnabled ? 'all' : 'default' const r = files.map((f) => f.fileUri.fsPath) const resp = await LspClient.instance.buildIndex(r, projRoot, config) if (resp) { - this.logger.debug(`LspController: Finish building index of project`) + this.logger.debug(`Finish building index of project`) const usage = await LspClient.instance.getLspServerUsage() telemetry.amazonq_indexWorkspace.emit({ duration: performance.now() - start, @@ -137,7 +137,7 @@ export class LspController { credentialStartUrl: buildIndexConfig.startUrl, }) } else { - this.logger.error(`LspController: Failed to build index of project`) + this.logger.error(`Failed to build index of project`) telemetry.amazonq_indexWorkspace.emit({ duration: performance.now() - start, result: 'Failed', @@ -149,7 +149,7 @@ export class LspController { } } catch (error) { // TODO: use telemetry.run() - this.logger.error(`LspController: Failed to build index of project`) + this.logger.error(`Failed to build index of project`) telemetry.amazonq_indexWorkspace.emit({ duration: performance.now() - start, result: 'Failed', @@ -166,7 +166,7 @@ export class LspController { async trySetupLsp(context: vscode.ExtensionContext, buildIndexConfig: BuildIndexConfig) { if (isCloud9() || isWeb() || isAmazonInternalOs()) { - this.logger.warn('LspController: Skipping LSP setup. LSP is not compatible with the current environment. ') + this.logger.warn('Skipping LSP setup. LSP is not compatible with the current environment. ') // do not do anything if in Cloud9 or Web mode or in AL2 (AL2 does not support node v18+) return } @@ -181,7 +181,7 @@ export class LspController { const usage = await LspClient.instance.getLspServerUsage() if (usage) { this.logger.info( - `LspController: LSP server CPU ${usage.cpuUsage}%, LSP server Memory ${ + `LSP server CPU ${usage.cpuUsage}%, LSP server Memory ${ usage.memoryUsage / (1024 * 1024) }MB ` ) @@ -190,7 +190,7 @@ export class LspController { 30 * 60 * 1000 ) } catch (e) { - this.logger.error(`LspController: LSP failed to activate ${e}`) + this.logger.error(`LSP failed to activate ${e}`) } }) } @@ -207,7 +207,7 @@ export class LspController { return } this._contextCommandSymbolsUpdated = true - getLogger().debug(`LspController: Start adding symbols to context picker menu`) + getLogger().debug(`Start adding symbols to context picker menu`) try { const indexSeqNum = await LspClient.instance.getIndexSequenceNumber() await LspClient.instance.updateIndex([], 'context_command_symbol_update') @@ -223,7 +223,7 @@ export class LspController { { interval: 1000, timeout: 60_000, truthy: true } ) } catch (err) { - getLogger().error(`LspController: Failed to find symbols`) + this.logger.error(`Failed to find symbols`) } } @@ -231,7 +231,7 @@ export class LspController { await lspSetupStage('all', async () => { const installResult = await new WorkspaceLspInstaller().resolve() await lspSetupStage('launch', async () => activateLsp(context, installResult.resourcePaths)) - this.logger.info('LspController: LSP activated') + this.logger.info('LSP activated') }) } } diff --git a/packages/core/src/shared/lsp/baseLspInstaller.ts b/packages/core/src/shared/lsp/baseLspInstaller.ts index 593b11a30c9..ee71b25a4b5 100644 --- a/packages/core/src/shared/lsp/baseLspInstaller.ts +++ b/packages/core/src/shared/lsp/baseLspInstaller.ts @@ -52,7 +52,9 @@ export abstract class BaseLspInstaller 0) { + this.logger.debug(`cleaning old LSP versions: deleted ${deletedVersions.length} versions`) + } const r = { ...installationResult, diff --git a/packages/core/src/shared/lsp/lspResolver.ts b/packages/core/src/shared/lsp/lspResolver.ts index 09c2970e702..90892148a9f 100644 --- a/packages/core/src/shared/lsp/lspResolver.ts +++ b/packages/core/src/shared/lsp/lspResolver.ts @@ -40,44 +40,41 @@ export class LanguageServerResolver { languageServerVersion: result.version, } } - try { - const latestVersion = this.latestCompatibleLspVersion() - const targetContents = this.getLSPTargetContents(latestVersion) - const cacheDirectory = this.getDownloadDirectory(latestVersion.serverVersion) - - const serverResolvers: StageResolver[] = [ - { - // 1: Use the current local ("cached") LSP server bundle, if any. - resolve: async () => await this.getLocalServer(cacheDirectory, latestVersion, targetContents), - telemetryMetadata: { id: this.lsName, languageServerLocation: 'cache' }, - }, - { - // 2: Download the latest LSP server bundle. - resolve: async () => await this.fetchRemoteServer(cacheDirectory, latestVersion, targetContents), - telemetryMetadata: { id: this.lsName, languageServerLocation: 'remote' }, - }, - { - // 3: If the download fails, try an older, cached version. - resolve: async () => await this.getFallbackServer(latestVersion), - telemetryMetadata: { id: this.lsName, languageServerLocation: 'fallback' }, - }, - ] - - /** - * Example: - * ``` - * LspResult { - * assetDirectory = "/aws/toolkits/language-servers/AmazonQ/3.3.0" - * location = 'cache' - * version = '3.3.0' - * } - * ``` - */ - const resolved = await tryStageResolvers('getServer', serverResolvers, getServerVersion) - return resolved - } finally { - logger.info(`Finished setting up LSP server`) - } + const latestVersion = this.latestCompatibleLspVersion() + const targetContents = this.getLSPTargetContents(latestVersion) + const cacheDirectory = this.getDownloadDirectory(latestVersion.serverVersion) + + const serverResolvers: StageResolver[] = [ + { + // 1: Use the current local ("cached") LSP server bundle, if any. + resolve: async () => await this.getLocalServer(cacheDirectory, latestVersion, targetContents), + telemetryMetadata: { id: this.lsName, languageServerLocation: 'cache' }, + }, + { + // 2: Download the latest LSP server bundle. + resolve: async () => await this.fetchRemoteServer(cacheDirectory, latestVersion, targetContents), + telemetryMetadata: { id: this.lsName, languageServerLocation: 'remote' }, + }, + { + // 3: If the download fails, try an older, cached version. + resolve: async () => await this.getFallbackServer(latestVersion), + telemetryMetadata: { id: this.lsName, languageServerLocation: 'fallback' }, + }, + ] + + /** + * Example: + * ``` + * LspResult { + * assetDirectory = "/aws/toolkits/language-servers/AmazonQ/3.3.0" + * location = 'cache' + * version = '3.3.0' + * } + * ``` + */ + const resolved = await tryStageResolvers('getServer', serverResolvers, getServerVersion) + logger.info('Finished preparing "%s" LSP server: %O', this.lsName, resolved.assetDirectory) + return resolved } /** Finds an older, cached version of the LSP server bundle. */ @@ -147,11 +144,7 @@ export class LanguageServerResolver { } } else { // Delete the cached directory since it's invalid - if (await fs.existsDir(cacheDirectory)) { - await fs.delete(cacheDirectory, { - recursive: true, - }) - } + await fs.delete(cacheDirectory, { force: true, recursive: true }) throw new ToolkitError('Failed to retrieve server from cache', { code: 'InvalidCache' }) } } diff --git a/packages/core/src/shared/lsp/manifestResolver.ts b/packages/core/src/shared/lsp/manifestResolver.ts index 289fc63268d..60cc466a835 100644 --- a/packages/core/src/shared/lsp/manifestResolver.ts +++ b/packages/core/src/shared/lsp/manifestResolver.ts @@ -64,11 +64,20 @@ export class ManifestResolver { }).getNewETagContent(this.getEtag()) if (!resp.content) { - throw new ToolkitError( - `New content was not downloaded; fallback to the locally stored "${this.lsName}" manifest` - ) + const msg = `"${this.lsName}" manifest unchanged, skipping download: ${this.manifestURL}` + logger.debug(msg) + + const localManifest = await this.getLocalManifest(true).catch(() => undefined) + if (localManifest) { + localManifest.location = 'remote' + return localManifest + } else { + // Will emit a `languageServer_setup` result=failed metric... + throw new ToolkitError(msg) + } } + logger.debug(`fetched "${this.lsName}" manifest: ${this.manifestURL}`) const manifest = this.parseManifest(resp.content) await this.saveManifest(resp.eTag, resp.content) await this.checkDeprecation(manifest) @@ -76,13 +85,17 @@ export class ManifestResolver { return manifest } - private async getLocalManifest(): Promise { - logger.info(`Failed to download latest "${this.lsName}" manifest. Falling back to local manifest.`) + private async getLocalManifest(silent: boolean = false): Promise { + if (!silent) { + logger.info(`trying local "${this.lsName}" manifest...`) + } const storage = this.getStorage() const manifestData = storage[this.lsName] if (!manifestData?.content) { - throw new ToolkitError(`Failed to download "${this.lsName}" manifest and no local manifest found.`) + const msg = `local "${this.lsName}" manifest not found` + logger.warn(msg) + throw new ToolkitError(msg) } const manifest = this.parseManifest(manifestData.content) @@ -145,6 +158,7 @@ export class ManifestResolver { ...storage, [this.lsName]: { etag, + // XXX: this stores the entire manifest. vscode warns about our globalStorage size... content, }, }) diff --git a/packages/core/src/shared/resourcefetcher/httpResourceFetcher.ts b/packages/core/src/shared/resourcefetcher/httpResourceFetcher.ts index 18cc696770f..b79b673c74d 100644 --- a/packages/core/src/shared/resourcefetcher/httpResourceFetcher.ts +++ b/packages/core/src/shared/resourcefetcher/httpResourceFetcher.ts @@ -68,9 +68,9 @@ export class HttpResourceFetcher implements ResourceFetcher { if (response.status === 304) { // Explanation: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match contents = undefined - this.logger.verbose(`E-Tag, ${eTagResponse}, matched. No content downloaded from: ${this.url}`) + this.logger.verbose(`E-Tag matched (${eTagResponse}). Download skipped: ${this.url}`) } else { - this.logger.verbose(`No E-Tag match. Downloaded content from: ${this.logText()}`) + this.logger.verbose(`E-Tag not matched. Downloaded: ${this.logText()}`) } return { content: contents, eTag: eTagResponse } diff --git a/packages/core/src/shared/utilities/processUtils.ts b/packages/core/src/shared/utilities/processUtils.ts index 31f2ec238f3..0204736f500 100644 --- a/packages/core/src/shared/utilities/processUtils.ts +++ b/packages/core/src/shared/utilities/processUtils.ts @@ -513,9 +513,10 @@ export class ChildProcess { * Gets a string representation of the process invocation. * * @param noparams Omit parameters in the result (to protect sensitive info). + * @param nostatus Omit "(not started)" note. */ - public toString(noparams = false): string { - const pid = this.pid() > 0 ? `PID ${this.pid()}:` : '(not started)' + public toString(noparams = false, nostatus = false): string { + const pid = this.pid() > 0 ? `PID ${this.pid()}:` : nostatus ? '' : '(not started)' return `${pid} [${this.#command} ${noparams ? '...' : this.#args.join(' ')}]` } }