/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/LanguageServer/Protocol/RoslynLanguageServer.cs
354 строки
14 KB
David Barbet
Implement LSP daemon mode (#84199)
25 июл 2026, 00:26
Не верифицирован
25 июл 2026, 00:26
490f516
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CommonLanguageServerProtocol.Framework; using Roslyn.LanguageServer.Protocol; using StreamJsonRpc; namespace Microsoft.CodeAnalysis.LanguageServer; internal sealed class RoslynLanguageServer : SystemTextJsonLanguageServer<RequestContext>, IOnInitialized { private static int s_clientProcessId = -1; private static readonly Lazy<int> s_currentProcessId = new(static () => { using var process = Process.GetCurrentProcess(); return process.Id; }); public static int ServerProcessId => s_currentProcessId.Value; private readonly AbstractLspServiceProvider _lspServiceProvider; private readonly FrozenDictionary<string, ImmutableArray<BaseService>> _baseServices; private readonly WellKnownLspServerKinds _serverKind; public RoslynLanguageServer( AbstractLspServiceProvider lspServiceProvider, JsonRpc jsonRpc, JsonSerializerOptions serializerOptions, HostServices hostServices, ImmutableArray<string> supportedLanguages, WellKnownLspServerKinds serverKind, AbstractTypeRefResolver? typeRefResolver = null, ILspLogger? logger = null) : base(jsonRpc, serializerOptions, typeRefResolver) { _lspServiceProvider = lspServiceProvider; _serverKind = serverKind; // Create services that require base dependencies (jsonrpc) or are more complex to create to the set manually. _baseServices = GetBaseServices(jsonRpc, hostServices, serverKind, supportedLanguages, logger); // This spins up the queue and ensure the LSP is ready to start receiving requests Initialize(); } public static bool TryRegisterClientProcessId(int clientProcessId) { if (s_clientProcessId != -1) return false; if (clientProcessId == ServerProcessId) return false; if (Interlocked.CompareExchange(ref s_clientProcessId, clientProcessId, -1) != -1) return false; _ = WaitForClientProcessExitAsync(s_clientProcessId); return true; static async Task WaitForClientProcessExitAsync(int clientProcessId) { try { var clientProcessExitTask = new TaskCompletionSource<bool>(); using var clientProcess = Process.GetProcessById(clientProcessId); clientProcess.EnableRaisingEvents = true; clientProcess.Exited += (sender, args) => clientProcessExitTask.SetResult(true); if (!clientProcess.HasExited) { // Wait for the client process to exit. await clientProcessExitTask.Task.ConfigureAwait(false); } } finally { // The process didn't exist, exited, or we ran into // issues checking whether the process had exited. Environment.Exit(ServerExitCodes.ClientProcessExited); } } } public static SystemTextJsonFormatter CreateJsonMessageFormatter() { var messageFormatter = new SystemTextJsonFormatter(); messageFormatter.JsonSerializerOptions.AddLspSerializerOptions(); return messageFormatter; } protected override ILspServices ConstructLspServices() { return _lspServiceProvider.CreateServices(_serverKind, _baseServices); } protected override IRequestExecutionQueue<RequestContext> ConstructRequestExecutionQueue() { var queue = new RoslynRequestExecutionQueue(this, HandlerProvider); queue.Start(); return queue; } private FrozenDictionary<string, ImmutableArray<BaseService>> GetBaseServices( JsonRpc jsonRpc, HostServices hostServices, WellKnownLspServerKinds serverKind, ImmutableArray<string> supportedLanguages, ILspLogger? logger) { // This map will hold either a single BaseService instance, or an ImmutableArray<BaseService>.Builder. var baseServiceMap = new Dictionary<string, object>(); var clientLanguageServerManager = new ClientLanguageServerManager(jsonRpc); // The server is allowed to pass in a specific logger to use - if none is passed in // it is required that there is an ILspService that exports the ILspLogger. if (logger != null) { AddService(logger); } AddService<IClientLanguageServerManager>(clientLanguageServerManager); AddService(new ServerInfoProvider(serverKind, supportedLanguages)); AddLazyService<AbstractRequestContextFactory<RequestContext>>(lspServices => new RequestContextFactory(lspServices)); AddLazyService<AbstractTelemetryService>(lspServices => new TelemetryService(lspServices)); AddLazyService<AbstractHandlerProvider>(_ => HandlerProvider); AddService<IInitializeManager>(new InitializeManager()); AddService<IMethodHandler>(new InitializeHandler()); AddService<IMethodHandler>(new InitializedHandler()); AddService<IOnInitialized>(this); AddService<ILanguageInfoProvider>(new LanguageInfoProvider()); AddService<HostServices>(hostServices); return baseServiceMap.ToFrozenDictionary( keySelector: kvp => kvp.Key, elementSelector: kvp => kvp.Value switch { BaseService service => [service], ImmutableArray<BaseService>.Builder builder => builder.ToImmutable(), _ => throw ExceptionUtilities.Unreachable() }); void AddService<T>(T instance) where T : class { AddBaseService(BaseService.Create(instance)); } void AddLazyService<T>(Func<ILspServices, T> creator) where T : class { AddBaseService(BaseService.CreateLazily(creator)); } void AddBaseService(BaseService baseService) { var typeName = baseService.Type.FullName; Contract.ThrowIfNull(typeName); // If the service doesn't exist in the map yet, just add it. if (!baseServiceMap.TryGetValue(typeName, out var value)) { baseServiceMap.Add(typeName, baseService); return; } // If the service exists in the map, check to see if it's a... switch (value) { // ... BaseService. In this case, update the map with an ImmutableArray<BaseService>.Builder // and add both the existing and new services to it. case BaseService existingService: var builder = ImmutableArray.CreateBuilder<BaseService>(); builder.Add(existingService); builder.Add(baseService); baseServiceMap[typeName] = builder; break; // ... ImmutableArray<BaseService>.Builder. In this case, just add the new service to the builder. case ImmutableArray<BaseService>.Builder existingBuilder: existingBuilder.Add(baseService); break; default: throw ExceptionUtilities.Unreachable(); } } } public async Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) { OnInitialized(); // Monitor the client process and shut down the server if the client process exits. var clientProcessMonitor = context.GetService<IClientProcessMonitor>(); if (clientProcessMonitor != null && clientProcessMonitor.GetClientProcessId() is { } processId) { _ = MonitorClientProcessAsync(processId, clientProcessMonitor.Strategy); } } private async Task MonitorClientProcessAsync(int processId, IClientProcessMonitor.ShutdownStrategy strategy) { var clientProcessExitTask = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); try { using var clientProcess = Process.GetProcessById(processId); clientProcess.EnableRaisingEvents = true; clientProcess.Exited += OnClientProcessExited; try { if (!clientProcess.HasExited) { // Stop monitoring when this logical server exits. In daemon mode the client process may // remain alive after disconnecting, so retaining the process event would leak this server. if (await Task.WhenAny(clientProcessExitTask.Task, WaitForExitAsync()).ConfigureAwait(false) != clientProcessExitTask.Task) return; } } finally { clientProcess.Exited -= OnClientProcessExited; } } finally { // The process didn't exist, exited, or we ran into issues checking whether it had exited. If the // logical server has not already exited, apply the configured process-exit shutdown behavior. if (!WaitForExitAsync().IsCompleted) { if (strategy == IClientProcessMonitor.ShutdownStrategy.ProcessExit) { Environment.Exit(ServerExitCodes.ClientProcessExited); } else { await ShutdownAsync().ConfigureAwait(false); await ExitAsync().ConfigureAwait(false); } } } void OnClientProcessExited(object? sender, EventArgs args) => clientProcessExitTask.TrySetResult(true); } public override bool TryGetLanguageForRequest(string methodName, object? serializedParameters, [NotNullWhen(true)] out string? language) { if (serializedParameters == null) { Logger.Value.LogDebug("No request parameters given, using default language handler"); language = LanguageServerConstants.DefaultLanguageName; return true; } // We implement the STJ language server so this must be a JsonElement. var parameters = (JsonElement)serializedParameters; // For certain requests like text syncing we'll always use the default language handler // as we do not want languages to be able to override them. if (ShouldUseDefaultLanguage(methodName)) { language = LanguageServerConstants.DefaultLanguageName; return true; } var lspWorkspaceManager = GetLspServices().GetRequiredService<LspWorkspaceManager>(); // All general LSP spec document params have the following json structure // { "textDocument": { "uri": "<uri>" ... } ... } // // We can easily identify the URI for the request by looking for this structure DocumentUri? uri = null; if (parameters.TryGetProperty("textDocument", out var textDocumentToken) || parameters.TryGetProperty("_vs_textDocument", out textDocumentToken)) { var textDocumentIdentifier = JsonSerializer.Deserialize<TextDocumentIdentifier>(textDocumentToken, ProtocolConversions.LspJsonSerializerOptions); Contract.ThrowIfNull(textDocumentIdentifier, "Failed to deserialize text document identifier property"); uri = textDocumentIdentifier.DocumentUri; } else if (TryGetRequestDataToken(parameters, out var dataToken)) { // All the LSP resolve params have the following known json structure // { "data": { "TextDocument": { "uri": "<uri>" ... } ... } ... } // // We can deserialize the data object using our unified DocumentResolveData. var data = JsonSerializer.Deserialize<DocumentResolveData>(dataToken, ProtocolConversions.LspJsonSerializerOptions); Contract.ThrowIfNull(data, "Failed to document resolve data object"); uri = data.TextDocument.DocumentUri; } if (uri == null) { // This request is not for a textDocument and is not a resolve request. Logger.Value.LogDebug("Request did not contain a textDocument, using default language handler"); language = LanguageServerConstants.DefaultLanguageName; return true; } if (!lspWorkspaceManager.TryGetLanguageForUri(uri, out language)) { Logger.Value.LogDebug($"Failed to get language for {uri} with language {language}"); return false; } return true; static bool TryGetRequestDataToken(JsonElement parameters, out JsonElement dataToken) { if (parameters.TryGetProperty("data", out dataToken)) { return true; } // Some LSP requests like call hierarchy incoming/outgoing calls and type hierarchy // subtype/supertype requests nest the same payload under item.data. if (parameters.TryGetProperty("item", out var itemToken) && itemToken.ValueKind == JsonValueKind.Object && itemToken.TryGetProperty("data", out dataToken)) { return true; } dataToken = default; return false; } static bool ShouldUseDefaultLanguage(string methodName) { return methodName switch { Methods.InitializeName => true, Methods.InitializedName => true, Methods.TextDocumentDidOpenName => true, Methods.TextDocumentDidChangeName => true, Methods.TextDocumentDidCloseName => true, Methods.TextDocumentDidSaveName => true, Methods.ShutdownName => true, Methods.ExitName => true, _ => false, }; } } }