diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index da09f54bb06..4595b137636 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -5,18 +5,17 @@ import vscode, { env, version } from 'vscode' import * as nls from 'vscode-nls' -import * as cp from 'child_process' // eslint-disable-line no-restricted-imports -- language server options expect actual child process import * as crypto from 'crypto' import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient' import { registerInlineCompletion } from '../inline/completion' -import { AmazonQLspAuth, notificationTypes, writeEncryptionInit } from './auth' +import { AmazonQLspAuth, encryptionKey, notificationTypes } from './auth' import { AuthUtil } from 'aws-core-vscode/codewhisperer' import { ConnectionMetadata } from '@aws/language-server-runtimes/protocol' -import { ResourcePaths } from 'aws-core-vscode/shared' +import { ResourcePaths, createServerOptions } from 'aws-core-vscode/shared' const localize = nls.loadMessageBundle() -export function startLanguageServer(extensionContext: vscode.ExtensionContext, resourcePaths: ResourcePaths) { +export async function startLanguageServer(extensionContext: vscode.ExtensionContext, resourcePaths: ResourcePaths) { const toDispose = extensionContext.subscriptions // The debug options for the server @@ -31,19 +30,21 @@ export function startLanguageServer(extensionContext: vscode.ExtensionContext, r ], } - const serverPath = resourcePaths.lsp + 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: serverPath, transport: TransportKind.ipc }, - debug: { module: serverPath, transport: TransportKind.ipc, options: debugOptions }, + run: { module: serverModule, transport: TransportKind.ipc }, + debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }, } - const child = cp.spawn(resourcePaths.node, [serverPath, ...debugOptions.execArgv]) - writeEncryptionInit(child.stdin) - - serverOptions = () => Promise.resolve(child) + serverOptions = createServerOptions({ + encryptionKey, + executable: resourcePaths.node, + serverModule, + execArgv: debugOptions.execArgv, + }) const documentSelector = [{ scheme: 'file', language: '*' }] diff --git a/packages/core/src/amazonq/lsp/lspClient.ts b/packages/core/src/amazonq/lsp/lspClient.ts index f30aa5b681d..659df3ae078 100644 --- a/packages/core/src/amazonq/lsp/lspClient.ts +++ b/packages/core/src/amazonq/lsp/lspClient.ts @@ -10,7 +10,6 @@ import * as vscode from 'vscode' import * as path from 'path' import * as nls from 'vscode-nls' -import { spawn } from 'child_process' // eslint-disable-line no-restricted-imports import * as crypto from 'crypto' import * as jose from 'jose' @@ -30,27 +29,13 @@ import { GetRepomapIndexJSONRequestType, Usage, } from './types' -import { Writable } from 'stream' import { CodeWhispererSettings } from '../../codewhisperer/util/codewhispererSettings' -import { ResourcePaths, fs, getLogger, globals } from '../../shared' +import { ResourcePaths, createServerOptions, fs, getLogger, globals } from '../../shared' const localize = nls.loadMessageBundle() const key = crypto.randomBytes(32) -/** - * Sends a json payload to the language server, who is waiting to know what the encryption key is. - * Code reference: https://github.com/aws/language-servers/blob/7da212185a5da75a72ce49a1a7982983f438651a/client/vscode/src/credentialsActivation.ts#L77 - */ -export function writeEncryptionInit(stream: Writable): void { - const request = { - version: '1.0', - mode: 'JWT', - key: key.toString('base64'), - } - stream.write(JSON.stringify(request)) - stream.write('\n') -} /** * LspClient manages the API call between VS Code extension and LSP server * It encryptes the payload of API call. @@ -197,11 +182,6 @@ export async function activate(extensionContext: ExtensionContext, resourcePaths const serverModule = resourcePaths.lsp - const child = spawn(resourcePaths.node, [serverModule, ...debugOptions.execArgv]) - // share an encryption key using stdin - // follow same practice of DEXP LSP server - writeEncryptionInit(child.stdin) - // If the extension is launch in debug mode the debug server options are use // Otherwise the run options are used let serverOptions: ServerOptions = { @@ -209,7 +189,12 @@ export async function activate(extensionContext: ExtensionContext, resourcePaths debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }, } - serverOptions = () => Promise.resolve(child!) + serverOptions = createServerOptions({ + encryptionKey: key, + executable: resourcePaths.node, + serverModule, + execArgv: debugOptions.execArgv, + }) const documentSelector = [{ scheme: 'file', language: '*' }] diff --git a/packages/core/src/shared/index.ts b/packages/core/src/shared/index.ts index 206836ed45f..ed938d20b17 100644 --- a/packages/core/src/shared/index.ts +++ b/packages/core/src/shared/index.ts @@ -64,3 +64,4 @@ export * from './lsp/lspResolver' export * from './lsp/types' export { default as request } from './request' export * from './lsp/utils/platform' +export * as processUtils from './utilities/processUtils' diff --git a/packages/core/src/shared/lsp/utils/platform.ts b/packages/core/src/shared/lsp/utils/platform.ts index f313b66186f..5c2d7a59123 100644 --- a/packages/core/src/shared/lsp/utils/platform.ts +++ b/packages/core/src/shared/lsp/utils/platform.ts @@ -3,6 +3,51 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { ToolkitError } from '../../errors' +import { ChildProcess } from '../../utilities/processUtils' + export function getNodeExecutableName(): string { return process.platform === 'win32' ? 'node.exe' : 'node' } + +/** + * Get a json payload that will be sent to the language server, who is waiting to know what the encryption key is. + * Code reference: https://github.com/aws/language-servers/blob/7da212185a5da75a72ce49a1a7982983f438651a/client/vscode/src/credentialsActivation.ts#L77 + */ +function getEncryptionInit(key: Buffer): string { + const request = { + version: '1.0', + mode: 'JWT', + key: key.toString('base64'), + } + return JSON.stringify(request) + '\n' +} + +export function createServerOptions({ + encryptionKey, + executable, + serverModule, + execArgv, +}: { + encryptionKey: Buffer + executable: string + serverModule: string + execArgv: string[] +}) { + return async () => { + const lspProcess = new ChildProcess(executable, [serverModule, ...execArgv]) + + // this is a long running process, awaiting it will never resolve + void lspProcess.run() + + // share an encryption key using stdin + // follow same practice of DEXP LSP server + await lspProcess.send(getEncryptionInit(encryptionKey)) + + const proc = lspProcess.proc() + if (!proc) { + throw new ToolkitError('Language Server process was not started') + } + return proc + } +} diff --git a/packages/core/src/shared/utilities/processUtils.ts b/packages/core/src/shared/utilities/processUtils.ts index 2e179da98b8..357ac76ea18 100644 --- a/packages/core/src/shared/utilities/processUtils.ts +++ b/packages/core/src/shared/utilities/processUtils.ts @@ -257,6 +257,10 @@ export class ChildProcess { return this.#processResult } + public proc(): proc.ChildProcess | undefined { + return this.#childProcess + } + public pid(): number { return this.#childProcess?.pid ?? -1 }