|
// 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.Collections.Immutable;
using System.CommandLine;
using System.Diagnostics;
using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Text.Json;
using Microsoft.CodeAnalysis.Contracts.Telemetry;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageServer;
using Microsoft.CodeAnalysis.LanguageServer.BrokeredServices;
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
using Microsoft.CodeAnalysis.LanguageServer.LanguageServer;
using Microsoft.CodeAnalysis.LanguageServer.Logging;
using Microsoft.CodeAnalysis.LanguageServer.Services;
using Microsoft.CodeAnalysis.LanguageServer.StarredSuggestions;
using Microsoft.CodeAnalysis.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using Roslyn.Utilities;
using RoslynLog = Microsoft.CodeAnalysis.Internal.Log;
// Setting the title can fail if the process is run without a window, such
// as when launched detached from nodejs
try
{
Console.Title = "Microsoft.CodeAnalysis.LanguageServer";
}
catch (IOException)
{
}
WindowsErrorReporting.SetErrorModeOnWindows();
var parser = CreateCommandLineParser();
return await parser.Parse(args).InvokeAsync(CancellationToken.None);
static async Task RunAsync(ServerConfiguration serverConfiguration, CancellationToken cancellationToken)
{
// Before we initialize the LSP server we can't send LSP log messages.
// Create a console logger as a fallback to use before the LSP server starts.
using var loggerFactory = LoggerFactory.Create(builder =>
{
// The actual logger is responsible for deciding whether to log based on the current log level.
// The factory should be configured to log everything.
builder.SetMinimumLevel(LogLevel.Trace);
builder.AddProvider(new LspLogMessageLoggerProvider(fallbackLoggerFactory:
// Add a console logger as a fallback for when the LSP server has not finished initializing.
LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(LogLevel.Trace);
builder.AddConsole();
// The console logger outputs control characters on unix for colors which don't render correctly in VSCode.
builder.AddSimpleConsole(formatterOptions => formatterOptions.ColorBehavior = LoggerColorBehavior.Disabled);
}), serverConfiguration
));
});
var logger = loggerFactory.CreateLogger<Program>();
logger.Log(serverConfiguration.LaunchDebugger ? LogLevel.Critical : LogLevel.Trace, "Server started with process ID {processId}", Environment.ProcessId);
if (serverConfiguration.LaunchDebugger)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Debugger.Launch() only works on Windows.
_ = Debugger.Launch();
}
else
{
var timeout = TimeSpan.FromMinutes(2);
logger.LogCritical($"Waiting {timeout:g} for a debugger to attach");
using var timeoutSource = new CancellationTokenSource(timeout);
while (!Debugger.IsAttached && !timeoutSource.Token.IsCancellationRequested)
{
await Task.Delay(100, CancellationToken.None);
}
}
}
logger.LogTrace($".NET Runtime Version: {RuntimeInformation.FrameworkDescription}");
var extensionManager = ExtensionAssemblyManager.Create(serverConfiguration, loggerFactory);
var assemblyLoader = new CustomExportAssemblyLoader(extensionManager, loggerFactory);
var typeRefResolver = new ExtensionTypeRefResolver(assemblyLoader, loggerFactory);
using var exportProvider = await ExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, serverConfiguration.DevKitDependencyPath, loggerFactory);
// LSP server doesn't have the pieces yet to support 'balanced' mode for source-generators. Hardcode us to
// 'automatic' for now.
var globalOptionService = exportProvider.GetExportedValue<IGlobalOptionService>();
globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, SourceGeneratorExecutionPreference.Automatic);
// The log file directory passed to us by VSCode might not exist yet, though its parent directory is guaranteed to exist.
Directory.CreateDirectory(serverConfiguration.ExtensionLogDirectory);
// Initialize the server configuration MEF exported value.
exportProvider.GetExportedValue<ServerConfigurationFactory>().InitializeConfiguration(serverConfiguration);
// Initialize the fault handler if it's available
var telemetryReporter = exportProvider.GetExports<ITelemetryReporter>().SingleOrDefault()?.Value;
RoslynLogger.Initialize(telemetryReporter, serverConfiguration.TelemetryLevel, serverConfiguration.SessionId);
// Create the workspace first, since right now the language server will assume there's at least one Workspace
var workspaceFactory = exportProvider.GetExportedValue<LanguageServerWorkspaceFactory>();
var analyzerPaths = new DirectoryInfo(AppContext.BaseDirectory).GetFiles("*.dll")
.Where(f => f.Name.StartsWith("Microsoft.CodeAnalysis.", StringComparison.Ordinal) && !f.Name.Contains("LanguageServer", StringComparison.Ordinal))
.Select(f => f.FullName)
.ToImmutableArray();
// Include analyzers from extension assemblies.
analyzerPaths = analyzerPaths.AddRange(extensionManager.ExtensionAssemblyPaths);
await workspaceFactory.InitializeSolutionLevelAnalyzersAsync(analyzerPaths);
var serviceBrokerFactory = exportProvider.GetExportedValue<ServiceBrokerFactory>();
StarredCompletionAssemblyHelper.InitializeInstance(serverConfiguration.StarredCompletionsPath, extensionManager, loggerFactory, serviceBrokerFactory);
// TODO: Remove, the path should match exactly. Workaround for https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1830914.
Microsoft.CodeAnalysis.EditAndContinue.EditAndContinueMethodDebugInfoReader.IgnoreCaseWhenComparingDocumentNames = Path.DirectorySeparatorChar == '\\';
var languageServerLogger = loggerFactory.CreateLogger(nameof(LanguageServerHost));
var (clientPipeName, serverPipeName) = CreateNewPipeNames();
var pipeServer = new NamedPipeServerStream(serverPipeName,
PipeDirection.InOut,
maxNumberOfServerInstances: 1,
PipeTransmissionMode.Byte,
PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous);
// Send the named pipe connection info to the client
Console.WriteLine(JsonSerializer.Serialize(new NamedPipeInformation(clientPipeName)));
// Wait for connection from client
await pipeServer.WaitForConnectionAsync(cancellationToken);
var server = new LanguageServerHost(pipeServer, pipeServer, exportProvider, languageServerLogger, typeRefResolver);
server.Start();
logger.LogInformation("Language server initialized");
RoslynLog.Logger.Log(RoslynLog.FunctionId.VSCode_LanguageServer_Started, logLevel: RoslynLog.LogLevel.Information);
try
{
await server.WaitForExitAsync();
}
finally
{
// After the LSP server shutdown, report session wide telemetry
RoslynLogger.ShutdownAndReportSessionTelemetry();
// Server has exited, cancel our service broker service
await serviceBrokerFactory.ShutdownAndWaitForCompletionAsync();
}
}
static CliRootCommand CreateCommandLineParser()
{
var debugOption = new CliOption<bool>("--debug")
{
Description = "Flag indicating if the debugger should be launched on startup.",
Required = false,
DefaultValueFactory = _ => false,
};
var brokeredServicePipeNameOption = new CliOption<string?>("--brokeredServicePipeName")
{
Description = "The name of the pipe used to connect to a remote process (if one exists).",
Required = false,
};
var logLevelOption = new CliOption<LogLevel>("--logLevel")
{
Description = "The minimum log verbosity.",
Required = true,
};
var starredCompletionsPathOption = new CliOption<string?>("--starredCompletionComponentPath")
{
Description = "The location of the starred completion component (if one exists).",
Required = false,
};
var telemetryLevelOption = new CliOption<string?>("--telemetryLevel")
{
Description = "Telemetry level, Defaults to 'off'. Example values: 'all', 'crash', 'error', or 'off'.",
Required = false,
};
var extensionLogDirectoryOption = new CliOption<string>("--extensionLogDirectory")
{
Description = "The directory where we should write log files to",
Required = true,
};
var sessionIdOption = new CliOption<string?>("--sessionId")
{
Description = "Session Id to use for telemetry",
Required = false
};
var extensionAssemblyPathsOption = new CliOption<string[]?>("--extension")
{
Description = "Full paths of extension assemblies to load (optional).",
Required = false
};
var devKitDependencyPathOption = new CliOption<string?>("--devKitDependencyPath")
{
Description = "Full path to the Roslyn dependency used with DevKit (optional).",
Required = false
};
var razorSourceGeneratorOption = new CliOption<string?>("--razorSourceGenerator")
{
Description = "Full path to the Razor source generator (optional).",
Required = false
};
var razorDesignTimePathOption = new CliOption<string?>("--razorDesignTimePath")
{
Description = "Full path to the Razor design time target path (optional).",
Required = false
};
var rootCommand = new CliRootCommand()
{
debugOption,
brokeredServicePipeNameOption,
logLevelOption,
starredCompletionsPathOption,
telemetryLevelOption,
sessionIdOption,
extensionAssemblyPathsOption,
devKitDependencyPathOption,
razorSourceGeneratorOption,
razorDesignTimePathOption,
extensionLogDirectoryOption
};
rootCommand.SetAction((parseResult, cancellationToken) =>
{
var launchDebugger = parseResult.GetValue(debugOption);
var logLevel = parseResult.GetValue(logLevelOption);
var starredCompletionsPath = parseResult.GetValue(starredCompletionsPathOption);
var telemetryLevel = parseResult.GetValue(telemetryLevelOption);
var sessionId = parseResult.GetValue(sessionIdOption);
var extensionAssemblyPaths = parseResult.GetValue(extensionAssemblyPathsOption) ?? [];
var devKitDependencyPath = parseResult.GetValue(devKitDependencyPathOption);
var razorSourceGenerator = parseResult.GetValue(razorSourceGeneratorOption);
var razorDesignTimePath = parseResult.GetValue(razorDesignTimePathOption);
var extensionLogDirectory = parseResult.GetValue(extensionLogDirectoryOption)!;
var serverConfiguration = new ServerConfiguration(
LaunchDebugger: launchDebugger,
LogConfiguration: new LogConfiguration(logLevel),
StarredCompletionsPath: starredCompletionsPath,
TelemetryLevel: telemetryLevel,
SessionId: sessionId,
ExtensionAssemblyPaths: extensionAssemblyPaths,
DevKitDependencyPath: devKitDependencyPath,
RazorSourceGenerator: razorSourceGenerator,
RazorDesignTimePath: razorDesignTimePath,
ExtensionLogDirectory: extensionLogDirectory);
return RunAsync(serverConfiguration, cancellationToken);
});
return rootCommand;
}
static (string clientPipe, string serverPipe) CreateNewPipeNames()
{
// On windows, .NET and Nodejs use different formats for the pipe name
const string WINDOWS_NODJS_PREFIX = @"\\.\pipe\";
const string WINDOWS_DOTNET_PREFIX = @"\\.\";
// The pipe name constructed by some systems is very long (due to temp path).
// Shorten the unique id for the pipe.
var newGuid = Guid.NewGuid().ToString();
var pipeName = newGuid.Split('-')[0];
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? (WINDOWS_NODJS_PREFIX + pipeName, WINDOWS_DOTNET_PREFIX + pipeName)
: (GetUnixTypePipeName(pipeName), GetUnixTypePipeName(pipeName));
}
static string GetUnixTypePipeName(string pipeName)
{
// Unix-type pipes are actually writing to a file
return Path.Combine(Path.GetTempPath(), pipeName + ".sock");
}
|