diff --git a/package.json b/package.json index 1d3a1c0ea..16ce598a1 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ } }, "defaults": { - "roslyn": "4.8.0-3.23458.4", + "roslyn": "4.8.0-3.23463.5", "omniSharp": "1.39.7", "razor": "7.0.0-preview.23456.2", "razorOmnisharp": "7.0.0-preview.23363.1" @@ -4992,4 +4992,4 @@ } ] } -} \ No newline at end of file +} diff --git a/src/lsptoolshost/roslynLanguageServer.ts b/src/lsptoolshost/roslynLanguageServer.ts index d7faf81d1..962c674b2 100644 --- a/src/lsptoolshost/roslynLanguageServer.ts +++ b/src/lsptoolshost/roslynLanguageServer.ts @@ -8,6 +8,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as cp from 'child_process'; import * as uuid from 'uuid'; +import * as net from 'net'; import { registerCommands } from './commands'; import { registerDebugger } from './debugger'; import { UriConverter } from './uriConverter'; @@ -21,6 +22,10 @@ import { RequestType0, PartialResultParams, ProtocolRequestType, + SocketMessageWriter, + SocketMessageReader, + MessageTransports, + RAL, } from 'vscode-languageclient/node'; import { PlatformInformation } from '../shared/platform'; import { readConfigurations } from './configurationMiddleware'; @@ -49,6 +54,7 @@ import { RoslynLanguageServerEvents } from './languageServerEvents'; import { registerShowToastNotification } from './showToastNotification'; import { registerRazorCommands } from './razorCommands'; import { registerOnAutoInsert } from './onAutoInsert'; +import { NamedPipeInformation } from './roslynProtocol'; let _channel: vscode.OutputChannel; let _traceChannel: vscode.OutputChannel; @@ -62,6 +68,16 @@ export class RoslynLanguageServer { private static readonly provideRazorDynamicFileInfoMethodName: string = 'razor/provideDynamicFileInfo'; private static readonly removeRazorDynamicFileInfoMethodName: string = 'razor/removeDynamicFileInfo'; + /** + * The encoding to use when writing to and from the stream. + */ + private static readonly encoding: RAL.MessageBufferEncoding = 'utf-8'; + + /** + * The regular expression used to find the named pipe key in the LSP server's stdout stream. + */ + private static readonly namedPipeKeyRegex = /{"pipeName":"[^"]+"}/; + /** * The timeout for stopping the language server (in ms). */ @@ -439,7 +455,7 @@ export class RoslynLanguageServer { context: vscode.ExtensionContext, telemetryReporter: TelemetryReporter, additionalExtensionPaths: string[] - ): Promise { + ): Promise { const options = optionProvider.GetLatestOptions(); const serverPath = getServerPath(options, platformInfo); @@ -538,7 +554,57 @@ export class RoslynLanguageServer { childProcess = cp.spawn(serverPath, args, cpOptions); } - return childProcess; + // Timeout promise used to time out the connection process if it takes too long. + const timeout = new Promise((resolve) => { + RAL().timer.setTimeout(resolve, 30000); + }); + + // The server process will create the named pipe used for communcation. Wait for it to be created, + // and listen for the server to pass back the connection information via stdout. + const namedPipeConnectionPromise = new Promise((resolve) => { + _channel.appendLine('waiting for named pipe information from server...'); + childProcess.stdout.on('data', (data: { toString: (arg0: any) => any }) => { + const result: string = isString(data) ? data : data.toString(RoslynLanguageServer.encoding); + _channel.append('[stdout] ' + result); + + // Use the regular expression to find all JSON lines + const jsonLines = result.match(RoslynLanguageServer.namedPipeKeyRegex); + if (jsonLines) { + const transmittedPipeNameInfo: NamedPipeInformation = JSON.parse(jsonLines[0]); + _channel.appendLine('received named pipe information from server'); + resolve(transmittedPipeNameInfo); + } + }); + }); + + // Wait for the server to send back the name of the pipe to connect to. + // If it takes too long it will timeout and throw an error. + const pipeConnectionInfo = await Promise.race([namedPipeConnectionPromise, timeout]); + if (pipeConnectionInfo === undefined) { + throw new Error('Timeout. Named pipe information not received from server.'); + } + + const socketPromise = new Promise((resolve) => { + _channel.appendLine('attempting to connect client to server...'); + const socket = net.createConnection(pipeConnectionInfo.pipeName, () => { + _channel.appendLine('client has connected to server'); + resolve(socket); + }); + }); + + // Wait for the client to connect to the named pipe. + // If it takes too long it will timeout and throw an error. + const socket = await Promise.race([socketPromise, timeout]); + if (socket === undefined) { + throw new Error( + 'Timeout. Client cound not connect to server via named pipe: ' + pipeConnectionInfo.pipeName + ); + } + + return { + reader: new SocketMessageReader(socket, RoslynLanguageServer.encoding), + writer: new SocketMessageWriter(socket, RoslynLanguageServer.encoding), + }; } private registerDynamicFileInfo() { @@ -836,3 +902,7 @@ function getSessionId(): string { return sessionId; } + +export function isString(value: any): value is string { + return typeof value === 'string' || value instanceof String; +} diff --git a/src/lsptoolshost/roslynProtocol.ts b/src/lsptoolshost/roslynProtocol.ts index 122fee686..b5883db50 100644 --- a/src/lsptoolshost/roslynProtocol.ts +++ b/src/lsptoolshost/roslynProtocol.ts @@ -141,6 +141,10 @@ export interface BuildOnlyDiagnosticIdsResult { ids: string[]; } +export interface NamedPipeInformation { + pipeName: string; +} + export namespace WorkspaceDebugConfigurationRequest { export const method = 'workspace/debugConfiguration'; export const messageDirection: lsp.MessageDirection = lsp.MessageDirection.clientToServer;