From 781bec6899d06e57bfc9e52c5fce31b2093fe3b5 Mon Sep 17 00:00:00 2001 From: Jared Parsons Date: Fri, 23 Jun 2023 09:08:46 -0700 Subject: [PATCH] Allow rzls to launch using standard dotnet This PR has two purposes: 1. Carving a path out where by we can ship rzls in a non-self contained fashion. 2. Unify the `dotnet` acquisition and launch process between the razor and roslyn servers This is my first every TypeScript PR so please be gentle :smile: --- src/lsptoolshost/roslynLanguageServer.ts | 41 +++----------- src/razor/src/RazorLanguageServerClient.ts | 54 ++++++++++++------- .../src/RazorLanguageServerOptionsResolver.ts | 24 +++------ src/razor/src/extension.ts | 8 ++- src/{lsptoolshost => shared}/dotnetRuntime.ts | 34 ++++++++++++ 5 files changed, 89 insertions(+), 72 deletions(-) rename src/{lsptoolshost => shared}/dotnetRuntime.ts (59%) diff --git a/src/lsptoolshost/roslynLanguageServer.ts b/src/lsptoolshost/roslynLanguageServer.ts index edee850ef..ea19c115a 100644 --- a/src/lsptoolshost/roslynLanguageServer.ts +++ b/src/lsptoolshost/roslynLanguageServer.ts @@ -38,7 +38,7 @@ import { CompletionItem, } from 'vscode-languageclient/node'; import { PlatformInformation } from '../shared/platform'; -import { acquireDotNetProcessDependencies } from './dotnetRuntime'; +import { getDotNetProcessInfo } from '../shared/dotnetRuntime'; import { readConfigurations } from './configurationMiddleware'; import OptionProvider from '../shared/observers/OptionProvider'; import { DynamicFileInfoHandler } from '../razor/src/DynamicFile/DynamicFileInfoHandler'; @@ -209,7 +209,7 @@ export class RoslynLanguageServer { /** * Restarts the language server. This does not wait until the server has been restarted. * Note that since some options affect how the language server is initialized, we must - * re-create the LanguageClient instance instead of just stopping/starting it. + * re-create the LanguageClient instance instead of just stopping/starting it. */ public async restart(): Promise { await this.stop(); @@ -356,31 +356,9 @@ export class RoslynLanguageServer { let options = this.optionProvider.GetLatestOptions(); let serverPath = this.getServerPath(options); + let [appPath, env, args] = await getDotNetProcessInfo(serverPath, this.platformInfo, options); - let dotnetRuntimePath = options.commonOptions.dotnetPath; - if (!dotnetRuntimePath) - { - let dotnetPath = await acquireDotNetProcessDependencies(serverPath); - dotnetRuntimePath = path.dirname(dotnetPath); - } - - const dotnetExecutableName = this.platformInfo.isWindows() ? 'dotnet.exe' : 'dotnet'; - const dotnetExecutablePath = path.join(dotnetRuntimePath, dotnetExecutableName); - if (!fs.existsSync(dotnetExecutablePath)) { - throw new Error(`Cannot find dotnet path '${dotnetExecutablePath}'`); - } - - _channel.appendLine("Dotnet path: " + dotnetExecutablePath); - - // Take care to always run .NET processes on the runtime that we intend. - // The dotnet.exe we point to should not go looking for other runtimes. - const env: NodeJS.ProcessEnv = { ...process.env }; - env.DOTNET_ROOT = dotnetRuntimePath; - env.DOTNET_MULTILEVEL_LOOKUP = '0'; - // Save user's DOTNET_ROOT env-var value so server can recover the user setting when needed - env.DOTNET_ROOT_USER = process.env.DOTNET_ROOT ?? 'EMPTY'; - - let args: string[] = [ ]; + _channel.appendLine("Dotnet path: " + appPath); if (options.commonOptions.waitForDebugger) { args.push("--debug"); @@ -427,17 +405,10 @@ export class RoslynLanguageServer { let cpOptions: cp.SpawnOptionsWithoutStdio = { detached: true, windowsHide: true, - env: env + env: env, }; - if (serverPath.endsWith('.dll')) { - // If we were given a path to a dll, launch that via dotnet. - const argsWithPath = [ serverPath ].concat(args); - childProcess = cp.spawn(dotnetExecutablePath, argsWithPath, cpOptions); - } else { - // Otherwise assume we were given a path to an executable. - childProcess = cp.spawn(serverPath, args, cpOptions); - } + childProcess = cp.spawn(appPath, args, cpOptions); return childProcess; } diff --git a/src/razor/src/RazorLanguageServerClient.ts b/src/razor/src/RazorLanguageServerClient.ts index 1f2b627cd..5bd08baba 100644 --- a/src/razor/src/RazorLanguageServerClient.ts +++ b/src/razor/src/RazorLanguageServerClient.ts @@ -5,6 +5,7 @@ import { EventEmitter } from 'events'; import * as vscode from 'vscode'; +import * as cp from 'child_process'; import { RequestHandler, RequestType, @@ -25,6 +26,9 @@ import { resolveRazorLanguageServerOptions } from './RazorLanguageServerOptionsR import { resolveRazorLanguageServerTrace } from './RazorLanguageServerTraceResolver'; import { RazorLogger } from './RazorLogger'; import { TelemetryReporter } from './TelemetryReporter'; +import { getDotNetProcessInfo } from '../../shared/dotnetRuntime'; +import { PlatformInformation } from '../../shared/platform'; +import OptionProvider from '../../shared/observers/OptionProvider'; const events = { ServerStop: 'ServerStop', @@ -32,7 +36,6 @@ const events = { export class RazorLanguageServerClient implements vscode.Disposable { private clientOptions!: LanguageClientOptions; - private serverOptions!: ServerOptions; private client!: LanguageClient; private onStartListeners: Array<() => Promise> = []; private onStartedListeners: Array<() => Promise> = []; @@ -45,6 +48,8 @@ export class RazorLanguageServerClient implements vscode.Disposable { private readonly vscodeType: typeof vscode, private readonly languageServerDir: string, private readonly telemetryReporter: TelemetryReporter, + private readonly platformInfo: PlatformInformation, + private readonly optionProvider: OptionProvider, private readonly logger: RazorLogger) { this.isStarted = false; @@ -208,26 +213,12 @@ export class RazorLanguageServerClient implements vscode.Disposable { return this.stopHandle; } - private setupLanguageServer() { - const languageServerTrace = resolveRazorLanguageServerTrace(this.vscodeType); - const options: RazorLanguageServerOptions = resolveRazorLanguageServerOptions(this.vscodeType, this.languageServerDir, languageServerTrace, this.logger); + private async startServer(options: RazorLanguageServerOptions): Promise { - this.clientOptions = { - outputChannel: options.outputChannel, - documentSelector: [ { language: RazorLanguage.id, pattern: RazorLanguage.globbingPattern } ], - }; - - const args: string[] = []; - let command = options.serverPath; - if (options.serverPath.endsWith('.dll')) { - this.logger.logMessage('Razor Language Server path is an assembly. ' + - 'Using \'dotnet\' from the current path to start the server.'); - - command = 'dotnet'; - args.push(options.serverPath); - } + let [appPath, env, args] = await getDotNetProcessInfo(options.serverPath, this.platformInfo, this.optionProvider.GetLatestOptions()); this.logger.logMessage(`Razor language server path: ${options.serverPath}`); + this.logger.logMessage(`Razor language app path: ${appPath}`); args.push('--trace'); args.push(options.trace.toString()); @@ -252,7 +243,30 @@ export class RazorLanguageServerClient implements vscode.Disposable { args.push('true'); } - this.serverOptions = { command, args }; - this.client = new LanguageClient('razorLanguageServer', 'Razor Language Server', this.serverOptions, this.clientOptions); + let childProcess: cp.ChildProcessWithoutNullStreams; + let cpOptions: cp.SpawnOptionsWithoutStdio = { + detached: true, + windowsHide: true, + env: env, + }; + + childProcess = cp.spawn(appPath, args, cpOptions); + + return childProcess; + } + + private setupLanguageServer() { + const languageServerTrace = resolveRazorLanguageServerTrace(this.vscodeType); + const options: RazorLanguageServerOptions = resolveRazorLanguageServerOptions(this.vscodeType, this.languageServerDir, languageServerTrace, this.logger); + + this.clientOptions = { + outputChannel: options.outputChannel, + documentSelector: [ { language: RazorLanguage.id, pattern: RazorLanguage.globbingPattern } ], + }; + + let serverOptions: ServerOptions = async () => { + return await this.startServer(options); + }; + this.client = new LanguageClient('razorLanguageServer', 'Razor Language Server', serverOptions, this.clientOptions); } } diff --git a/src/razor/src/RazorLanguageServerOptionsResolver.ts b/src/razor/src/RazorLanguageServerOptionsResolver.ts index 11c889902..cbd7698c0 100644 --- a/src/razor/src/RazorLanguageServerOptionsResolver.ts +++ b/src/razor/src/RazorLanguageServerOptionsResolver.ts @@ -32,25 +32,17 @@ export function resolveRazorLanguageServerOptions( } function findLanguageServerExecutable(withinDir: string) { - const extension = isWindows() ? '.exe' : ''; - const executablePath = path.join( - withinDir, - `rzls${extension}`); + const isSelfContained = fs.existsSync(path.join(withinDir, 'coreclr.dll')); let fullPath = ''; - - if (fs.existsSync(executablePath)) { - fullPath = executablePath; + if (isSelfContained) { + const fileName = isWindows() ? 'rzls.exe' : 'rzls'; + fullPath = path.join(withinDir, fileName); } else { - // Exe doesn't exist. - const dllPath = path.join( - withinDir, - 'rzls.dll'); - - if (!fs.existsSync(dllPath)) { - throw new Error(`Could not find Razor Language Server executable within directory '${withinDir}'`); - } + fullPath = path.join(withinDir, 'rzls.dll'); + } - fullPath = dllPath; + if (!fs.existsSync(fullPath)) { + throw new Error(`Could not find Razor Language Server executable within directory '${withinDir}'`); } return fullPath; diff --git a/src/razor/src/extension.ts b/src/razor/src/extension.ts index af7f7ce35..c4672ce6f 100644 --- a/src/razor/src/extension.ts +++ b/src/razor/src/extension.ts @@ -43,6 +43,9 @@ import { SemanticTokensRangeHandler } from './Semantic/SemanticTokensRangeHandle import { RazorSignatureHelpProvider } from './SignatureHelp/RazorSignatureHelpProvider'; import { TelemetryReporter } from './TelemetryReporter'; import { RazorDiagnosticHandler } from './Diagnostics/RazorDiagnosticHandler'; +import { PlatformInformation } from '../../shared/platform'; +import createOptionStream from '../../shared/observables/CreateOptionStream'; +import OptionProvider from '../../shared/observers/OptionProvider'; // We specifically need to take a reference to a particular instance of the vscode namespace, // otherwise providers attempt to operate on the null extension. @@ -52,11 +55,14 @@ export async function activate(vscodeType: typeof vscodeapi, context: ExtensionC create: () => new vscode.EventEmitter(), }; + const optionStream = createOptionStream(vscode); + let optionProvider = new OptionProvider(optionStream); const languageServerTrace = resolveRazorLanguageServerTrace(vscodeType); const logger = new RazorLogger(vscodeType, eventEmitterFactory, languageServerTrace); try { - const languageServerClient = new RazorLanguageServerClient(vscodeType, languageServerDir, telemetryReporter, logger); + const platformInfo = await PlatformInformation.GetCurrent(); + const languageServerClient = new RazorLanguageServerClient(vscodeType, languageServerDir, telemetryReporter, platformInfo, optionProvider, logger); const languageServiceClient = new RazorLanguageServiceClient(languageServerClient); const documentManager = new RazorDocumentManager(languageServerClient, logger); diff --git a/src/lsptoolshost/dotnetRuntime.ts b/src/shared/dotnetRuntime.ts similarity index 59% rename from src/lsptoolshost/dotnetRuntime.ts rename to src/shared/dotnetRuntime.ts index dbd069195..8ca7a4d23 100644 --- a/src/lsptoolshost/dotnetRuntime.ts +++ b/src/shared/dotnetRuntime.ts @@ -4,7 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; import { CSharpExtensionId } from '../constants/CSharpExtensionId'; +import { PlatformInformation } from './platform'; +import { Options } from './options'; export const DotNetRuntimeVersion = '7.0'; @@ -56,3 +60,33 @@ export async function acquireDotNetProcessDependencies(path: string) { return dotnetPath; } + +export async function getDotNetProcessInfo(appPath: string, platformInfo: PlatformInformation, options: Options): Promise<[appPath: string, env: NodeJS.ProcessEnv, args: string[]]> { + let dotnetRuntimePath = options.commonOptions.dotnetPath; + if (!dotnetRuntimePath) { + let dotnetPath = await acquireDotNetProcessDependencies(appPath); + dotnetRuntimePath = path.dirname(dotnetPath); + } + + // Take care to always run .NET processes on the runtime that we intend. + // The dotnet.exe we point to should not go looking for other runtimes. + const env: NodeJS.ProcessEnv = { ...process.env }; + env.DOTNET_ROOT = dotnetRuntimePath; + env.DOTNET_MULTILEVEL_LOOKUP = '0'; + // Save user's DOTNET_ROOT env-var value so server can recover the user setting when needed + env.DOTNET_ROOT_USER = process.env.DOTNET_ROOT ?? 'EMPTY'; + + let args: string[] = [ ]; + if (!appPath.endsWith('.dll')) { + return [appPath, env, args]; + } + + const dotnetFileName = platformInfo.isWindows() ? 'dotnet.exe' : 'dotnet'; + const dotnetPath = path.join(dotnetRuntimePath, dotnetFileName); + if (!fs.existsSync(dotnetPath)) { + throw new Error(`Cannot find dotnet path '${dotnetPath}'`); + } + + args.push(appPath); + return [dotnetPath, env, args]; +}