Skip to content

Commit

Permalink
refactor(lsp): Use internal ChildProcess module for launching languag…
Browse files Browse the repository at this point in the history
…e servers (#6374)

## Problem
- we want to use internal ChildProcess module over node's child process
module
- some code for spawning the language server is duplicated between the
workspace server and the amazon q server

## Solution
- use internal ChildProcess module and de-dup the common language server
spawning code

---

- Treat all work as PUBLIC. Private `feature/x` branches will not be
squash-merged at release time.
- Your code changes must meet the guidelines in
[CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines).
- License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
  • Loading branch information
jpinkney-aws authored Jan 17, 2025
1 parent ff53d2c commit 45a1ab9
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 33 deletions.
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 @@ -389,6 +389,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

0 comments on commit 45a1ab9

Please sign in to comment.