Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions packages/amazonq/src/lsp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: '*' }]

Expand Down
29 changes: 7 additions & 22 deletions packages/core/src/amazonq/lsp/lspClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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.
Expand Down Expand Up @@ -197,19 +182,19 @@ 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 = {
run: { module: serverModule, transport: TransportKind.ipc },
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: '*' }]

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
45 changes: 45 additions & 0 deletions packages/core/src/shared/lsp/utils/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
4 changes: 4 additions & 0 deletions packages/core/src/shared/utilities/processUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading