From 526016ca889e147dc36228ecfba87022e523008e Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Tue, 27 Aug 2019 13:55:08 -0700 Subject: [PATCH] refactor server setup --- .../Hosting/EditorServicesHost.cs | 137 ++--------- .../Hosting/{LogLevel.cs => PsesLogLevel.cs} | 0 .../Interface/ILanguageServer.cs | 11 - .../Interface/ILanguageServerBuilder.cs | 17 -- .../LanguageServer/OmnisharpLanguageServer.cs | 229 ------------------ .../OmnisharpLanguageServerBuilder.cs | 40 --- .../Logging/LoggerExtensions.cs | 1 - .../Server/NamedPipePsesLanguageServer.cs | 151 ++++++++++++ .../Server/PsesLanguageServer.cs | 215 ++++++++++++++++ .../Server/StdioPsesLanguageServer.cs | 41 ++++ 10 files changed, 433 insertions(+), 409 deletions(-) rename src/PowerShellEditorServices.Engine/Hosting/{LogLevel.cs => PsesLogLevel.cs} (100%) delete mode 100644 src/PowerShellEditorServices.Engine/Interface/ILanguageServer.cs delete mode 100644 src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs delete mode 100644 src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs delete mode 100644 src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs create mode 100644 src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs create mode 100644 src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs create mode 100644 src/PowerShellEditorServices.Engine/Server/StdioPsesLanguageServer.cs diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 2cf1ab05f..161d298fc 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -6,20 +6,15 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Management.Automation; using System.Management.Automation.Host; -using System.Management.Automation.Runspaces; using System.Reflection; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Extensions; -using Microsoft.PowerShell.EditorServices.Host; -using Microsoft.PowerShell.EditorServices.Templates; +using Microsoft.PowerShell.EditorServices.Engine.Server; using Serilog; namespace Microsoft.PowerShell.EditorServices.Engine @@ -62,8 +57,6 @@ public class EditorServicesHost { #region Private Fields - private readonly IServiceCollection _serviceCollection; - private readonly HostDetails _hostDetails; private readonly PSHost _internalHost; @@ -74,7 +67,7 @@ public class EditorServicesHost private readonly string[] _additionalModules; - private ILanguageServer _languageServer; + private PsesLanguageServer _languageServer; private Microsoft.Extensions.Logging.ILogger _logger; @@ -139,16 +132,15 @@ public EditorServicesHost( Validate.IsNotNull(nameof(hostDetails), hostDetails); Validate.IsNotNull(nameof(internalHost), internalHost); - _serviceCollection = new ServiceCollection(); _hostDetails = hostDetails; //this._hostDetails = hostDetails; - this._enableConsoleRepl = enableConsoleRepl; + _enableConsoleRepl = enableConsoleRepl; //this.bundledModulesPath = bundledModulesPath; - this._additionalModules = additionalModules ?? Array.Empty(); - this._featureFlags = new HashSet(featureFlags ?? Array.Empty()); + _additionalModules = additionalModules ?? Array.Empty(); + _featureFlags = new HashSet(featureFlags ?? Array.Empty()); //this.serverCompletedTask = new TaskCompletionSource(); - this._internalHost = internalHost; + _internalHost = internalHost; #if DEBUG if (waitForDebugger) @@ -236,59 +228,33 @@ public void StartLanguageService( _logger.LogInformation($"LSP NamedPipe: {config.InOutPipeName}\nLSP OutPipe: {config.OutPipeName}"); - _serviceCollection - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton( - (provider) => - GetFullyInitializedPowerShellContext( - provider.GetService(), - profilePaths)) - .AddSingleton() - .AddSingleton() - .AddSingleton( - (provider) => - { - var extensionService = new ExtensionService( - provider.GetService(), - provider.GetService()); - extensionService.InitializeAsync( - serviceProvider: provider, - editorOperations: provider.GetService()) - .Wait(); - return extensionService; - }) - .AddSingleton( - (provider) => - { - return AnalysisService.Create( - provider.GetService(), - provider.GetService(), - _factory.CreateLogger()); - }); + switch (config.TransportType) { case EditorServiceTransportType.NamedPipe: - _languageServer = new OmnisharpLanguageServerBuilder(_serviceCollection) - { - NamedPipeName = config.InOutPipeName ?? config.InPipeName, - OutNamedPipeName = config.OutPipeName, - LoggerFactory = _factory, - MinimumLogLevel = LogLevel.Trace, - } - .BuildLanguageServer(); + _languageServer = new NamedPipePsesLanguageServer( + _factory, + LogLevel.Trace, + _enableConsoleRepl, + _featureFlags, + _hostDetails, + _additionalModules, + _internalHost, + profilePaths, + config.InOutPipeName ?? config.InPipeName, + config.OutPipeName); break; case EditorServiceTransportType.Stdio: - _languageServer = new OmnisharpLanguageServerBuilder(_serviceCollection) - { - Stdio = true, - LoggerFactory = _factory, - MinimumLogLevel = LogLevel.Trace, - } - .BuildLanguageServer(); + _languageServer = new StdioPsesLanguageServer( + _factory, + LogLevel.Trace, + _featureFlags, + _hostDetails, + _additionalModules, + _internalHost, + profilePaths); break; } @@ -302,57 +268,6 @@ public void StartLanguageService( config.TransportType, config.Endpoint)); } - private PowerShellContextService GetFullyInitializedPowerShellContext( - OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer languageServer, - ProfilePaths profilePaths) - { - var logger = _factory.CreateLogger(); - - // PSReadLine can only be used when -EnableConsoleRepl is specified otherwise - // issues arise when redirecting stdio. - var powerShellContext = new PowerShellContextService( - logger, - languageServer, - _featureFlags.Contains("PSReadLine") && _enableConsoleRepl); - - EditorServicesPSHostUserInterface hostUserInterface = - _enableConsoleRepl - ? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, logger, _internalHost) - : new ProtocolPSHostUserInterface(languageServer, powerShellContext, logger); - - EditorServicesPSHost psHost = - new EditorServicesPSHost( - powerShellContext, - _hostDetails, - hostUserInterface, - logger); - - Runspace initialRunspace = PowerShellContextService.CreateRunspace(psHost); - powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); - - powerShellContext.ImportCommandsModuleAsync( - Path.Combine( - Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), - @"..\Commands")); - - // TODO: This can be moved to the point after the $psEditor object - // gets initialized when that is done earlier than LanguageServer.Initialize - foreach (string module in this._additionalModules) - { - var command = - new PSCommand() - .AddCommand("Microsoft.PowerShell.Core\\Import-Module") - .AddParameter("Name", module); - - powerShellContext.ExecuteCommandAsync( - command, - sendOutputToHost: false, - sendErrorToHost: true); - } - - return powerShellContext; - } - /// /// Starts the debug service with the specified config. /// diff --git a/src/PowerShellEditorServices.Engine/Hosting/LogLevel.cs b/src/PowerShellEditorServices.Engine/Hosting/PsesLogLevel.cs similarity index 100% rename from src/PowerShellEditorServices.Engine/Hosting/LogLevel.cs rename to src/PowerShellEditorServices.Engine/Hosting/PsesLogLevel.cs diff --git a/src/PowerShellEditorServices.Engine/Interface/ILanguageServer.cs b/src/PowerShellEditorServices.Engine/Interface/ILanguageServer.cs deleted file mode 100644 index 07605468c..000000000 --- a/src/PowerShellEditorServices.Engine/Interface/ILanguageServer.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Engine -{ - public interface ILanguageServer - { - Task StartAsync(); - - Task WaitForShutdown(); - } -} diff --git a/src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs b/src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs deleted file mode 100644 index 0b5801b36..000000000 --- a/src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace Microsoft.PowerShell.EditorServices.Engine -{ - public interface ILanguageServerBuilder - { - string NamedPipeName { get; set; } - - string OutNamedPipeName { get; set; } - - ILoggerFactory LoggerFactory { get; set; } - - LogLevel MinimumLogLevel { get; set; } - - ILanguageServer BuildLanguageServer(); - } -} diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs deleted file mode 100644 index 4a08bec2d..000000000 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ /dev/null @@ -1,229 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System.IO; -using System.IO.Pipes; -using System.Reflection; -using System.Security.AccessControl; -using System.Security.Principal; -using System.Threading.Tasks; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.TextDocument; -using OmniSharp.Extensions.LanguageServer.Server; -using OS = OmniSharp.Extensions.LanguageServer.Server; -using PowerShellEditorServices.Engine.Services.Handlers; - -namespace Microsoft.PowerShell.EditorServices.Engine -{ - public class OmnisharpLanguageServer : ILanguageServer - { - public class Configuration - { - public bool Stdio { get; set; } - - public string NamedPipeName { get; set; } - - public string OutNamedPipeName { get; set; } - - public ILoggerFactory LoggerFactory { get; set; } - - public LogLevel MinimumLogLevel { get; set; } - - public IServiceCollection Services { get; set; } - } - - // This int will be casted to a PipeOptions enum that only exists in .NET Core 2.1 and up which is why it's not available to us in .NET Standard. - private const int CurrentUserOnly = 0x20000000; - - // In .NET Framework, NamedPipeServerStream has a constructor that takes in a PipeSecurity object. We will use reflection to call the constructor, - // since .NET Framework doesn't have the `CurrentUserOnly` PipeOption. - // doc: https://docs.microsoft.com/en-us/dotnet/api/system.io.pipes.namedpipeserverstream.-ctor?view=netframework-4.7.2#System_IO_Pipes_NamedPipeServerStream__ctor_System_String_System_IO_Pipes_PipeDirection_System_Int32_System_IO_Pipes_PipeTransmissionMode_System_IO_Pipes_PipeOptions_System_Int32_System_Int32_System_IO_Pipes_PipeSecurity_ - private static readonly ConstructorInfo s_netFrameworkPipeServerConstructor = - typeof(NamedPipeServerStream).GetConstructor(new [] { typeof(string), typeof(PipeDirection), typeof(int), typeof(PipeTransmissionMode), typeof(PipeOptions), typeof(int), typeof(int), typeof(PipeSecurity) }); - - private OS.ILanguageServer _languageServer; - - private TaskCompletionSource _serverStart; - - private readonly Configuration _configuration; - - public OmnisharpLanguageServer( - Configuration configuration) - { - _configuration = configuration; - _serverStart = new TaskCompletionSource(); - } - - public async Task StartAsync() - { - _languageServer = await OS.LanguageServer.From(options => { - - options.AddDefaultLoggingProvider(); - options.LoggerFactory = _configuration.LoggerFactory; - ILogger logger = options.LoggerFactory.CreateLogger("OptionsStartup"); - - if (_configuration.Stdio) - { - options.WithInput(System.Console.OpenStandardInput()); - options.WithOutput(System.Console.OpenStandardOutput()); - } - else - { - NamedPipeServerStream namedPipe = CreateNamedPipe( - _configuration.NamedPipeName, - _configuration.OutNamedPipeName, - out NamedPipeServerStream outNamedPipe); - - logger.LogInformation("Waiting for connection"); - namedPipe.WaitForConnection(); - if (outNamedPipe != null) - { - outNamedPipe.WaitForConnection(); - } - - logger.LogInformation("Connected"); - - options.Input = namedPipe; - options.Output = outNamedPipe ?? namedPipe; - } - - options.MinimumLogLevel = _configuration.MinimumLogLevel; - options.Services = _configuration.Services; - logger.LogInformation("Adding handlers"); - - options - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .OnInitialize( - async (languageServer, request) => - { - var serviceProvider = languageServer.Services; - var workspaceService = serviceProvider.GetService(); - - // Grab the workspace path from the parameters - workspaceService.WorkspacePath = request.RootPath; - - // Set the working directory of the PowerShell session to the workspace path - if (workspaceService.WorkspacePath != null - && Directory.Exists(workspaceService.WorkspacePath)) - { - await serviceProvider.GetService().SetWorkingDirectoryAsync( - workspaceService.WorkspacePath, - isPathAlreadyEscaped: false); - } - }); - - logger.LogInformation("Handlers added"); - }); - - _serverStart.SetResult(true); - } - - public async Task WaitForShutdown() - { - await _serverStart.Task; - await _languageServer.WaitForExit; - } - - private static NamedPipeServerStream CreateNamedPipe( - string inOutPipeName, - string outPipeName, - out NamedPipeServerStream outPipe) - { - // .NET Core implementation is simplest so try that first - if (VersionUtils.IsNetCore) - { - outPipe = outPipeName == null - ? null - : new NamedPipeServerStream( - pipeName: outPipeName, - direction: PipeDirection.Out, - maxNumberOfServerInstances: 1, - transmissionMode: PipeTransmissionMode.Byte, - options: (PipeOptions)CurrentUserOnly); - - return new NamedPipeServerStream( - pipeName: inOutPipeName, - direction: PipeDirection.InOut, - maxNumberOfServerInstances: 1, - transmissionMode: PipeTransmissionMode.Byte, - options: PipeOptions.Asynchronous | (PipeOptions)CurrentUserOnly); - } - - // Now deal with Windows PowerShell - // We need to use reflection to get a nice constructor - - var pipeSecurity = new PipeSecurity(); - - WindowsIdentity identity = WindowsIdentity.GetCurrent(); - WindowsPrincipal principal = new WindowsPrincipal(identity); - - if (principal.IsInRole(WindowsBuiltInRole.Administrator)) - { - // Allow the Administrators group full access to the pipe. - pipeSecurity.AddAccessRule(new PipeAccessRule( - new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).Translate(typeof(NTAccount)), - PipeAccessRights.FullControl, AccessControlType.Allow)); - } - else - { - // Allow the current user read/write access to the pipe. - pipeSecurity.AddAccessRule(new PipeAccessRule( - WindowsIdentity.GetCurrent().User, - PipeAccessRights.ReadWrite, AccessControlType.Allow)); - } - - outPipe = outPipeName == null - ? null - : (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( - new object[] { - outPipeName, - PipeDirection.InOut, - 1, // maxNumberOfServerInstances - PipeTransmissionMode.Byte, - PipeOptions.Asynchronous, - 1024, // inBufferSize - 1024, // outBufferSize - pipeSecurity - }); - - return (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( - new object[] { - inOutPipeName, - PipeDirection.InOut, - 1, // maxNumberOfServerInstances - PipeTransmissionMode.Byte, - PipeOptions.Asynchronous, - 1024, // inBufferSize - 1024, // outBufferSize - pipeSecurity - }); - } - } -} diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs deleted file mode 100644 index d283b99a8..000000000 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Microsoft.PowerShell.EditorServices.Engine -{ - public class OmnisharpLanguageServerBuilder : ILanguageServerBuilder - { - public OmnisharpLanguageServerBuilder(IServiceCollection serviceCollection) - { - Services = serviceCollection; - } - - public bool Stdio { get; set; } - - public string NamedPipeName { get; set; } - - public string OutNamedPipeName { get; set; } - - public ILoggerFactory LoggerFactory { get; set; } = new LoggerFactory(); - - public LogLevel MinimumLogLevel { get; set; } = LogLevel.Trace; - - public IServiceCollection Services { get; } - - public ILanguageServer BuildLanguageServer() - { - var config = new OmnisharpLanguageServer.Configuration() - { - LoggerFactory = LoggerFactory, - MinimumLogLevel = MinimumLogLevel, - NamedPipeName = NamedPipeName, - OutNamedPipeName = OutNamedPipeName, - Services = Services, - Stdio = Stdio - }; - - return new OmnisharpLanguageServer(config); - } - } -} diff --git a/src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs b/src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs index 4faf9629f..9ee76baa2 100644 --- a/src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs +++ b/src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs @@ -1,6 +1,5 @@ using System; using System.Runtime.CompilerServices; -using System.Text; using Microsoft.Extensions.Logging; namespace Microsoft.PowerShell.EditorServices diff --git a/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs b/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs new file mode 100644 index 000000000..c47cd1c8d --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs @@ -0,0 +1,151 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.IO; +using System.IO.Pipes; +using System.Management.Automation.Host; +using System.Reflection; +using System.Security.AccessControl; +using System.Security.Principal; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Engine.Server +{ + internal class NamedPipePsesLanguageServer : PsesLanguageServer + { + // This int will be casted to a PipeOptions enum that only exists in .NET Core 2.1 and up which is why it's not available to us in .NET Standard. + private const int CurrentUserOnly = 0x20000000; + + // In .NET Framework, NamedPipeServerStream has a constructor that takes in a PipeSecurity object. We will use reflection to call the constructor, + // since .NET Framework doesn't have the `CurrentUserOnly` PipeOption. + // doc: https://docs.microsoft.com/en-us/dotnet/api/system.io.pipes.namedpipeserverstream.-ctor?view=netframework-4.7.2#System_IO_Pipes_NamedPipeServerStream__ctor_System_String_System_IO_Pipes_PipeDirection_System_Int32_System_IO_Pipes_PipeTransmissionMode_System_IO_Pipes_PipeOptions_System_Int32_System_Int32_System_IO_Pipes_PipeSecurity_ + private static readonly ConstructorInfo s_netFrameworkPipeServerConstructor = + typeof(NamedPipeServerStream).GetConstructor(new[] { typeof(string), typeof(PipeDirection), typeof(int), typeof(PipeTransmissionMode), typeof(PipeOptions), typeof(int), typeof(int), typeof(PipeSecurity) }); + + private readonly string _namedPipeName; + private readonly string _outNamedPipeName; + + internal NamedPipePsesLanguageServer( + ILoggerFactory factory, + LogLevel minimumLogLevel, + bool enableConsoleRepl, + HashSet featureFlags, + HostDetails hostDetails, + string[] additionalModules, + PSHost internalHost, + ProfilePaths profilePaths, + string namedPipeName, + string outNamedPipeName) : base( + factory, + minimumLogLevel, + enableConsoleRepl, + featureFlags, + hostDetails, + additionalModules, + internalHost, + profilePaths) + { + _namedPipeName = namedPipeName; + _outNamedPipeName = outNamedPipeName; + } + + protected override (Stream input, Stream output) GetInputOutputStreams() + { + NamedPipeServerStream namedPipe = CreateNamedPipe( + _namedPipeName, + _outNamedPipeName, + out NamedPipeServerStream outNamedPipe); + + var logger = _loggerFactory.CreateLogger("NamedPipeConnection"); + + logger.LogInformation("Waiting for connection"); + namedPipe.WaitForConnection(); + if (outNamedPipe != null) + { + outNamedPipe.WaitForConnection(); + } + + logger.LogInformation("Connected"); + + return (namedPipe, outNamedPipe ?? namedPipe); + } + + private static NamedPipeServerStream CreateNamedPipe( + string inOutPipeName, + string outPipeName, + out NamedPipeServerStream outPipe) + { + // .NET Core implementation is simplest so try that first + if (VersionUtils.IsNetCore) + { + outPipe = outPipeName == null + ? null + : new NamedPipeServerStream( + pipeName: outPipeName, + direction: PipeDirection.Out, + maxNumberOfServerInstances: 1, + transmissionMode: PipeTransmissionMode.Byte, + options: (PipeOptions)CurrentUserOnly); + + return new NamedPipeServerStream( + pipeName: inOutPipeName, + direction: PipeDirection.InOut, + maxNumberOfServerInstances: 1, + transmissionMode: PipeTransmissionMode.Byte, + options: PipeOptions.Asynchronous | (PipeOptions)CurrentUserOnly); + } + + // Now deal with Windows PowerShell + // We need to use reflection to get a nice constructor + + var pipeSecurity = new PipeSecurity(); + + WindowsIdentity identity = WindowsIdentity.GetCurrent(); + WindowsPrincipal principal = new WindowsPrincipal(identity); + + if (principal.IsInRole(WindowsBuiltInRole.Administrator)) + { + // Allow the Administrators group full access to the pipe. + pipeSecurity.AddAccessRule(new PipeAccessRule( + new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).Translate(typeof(NTAccount)), + PipeAccessRights.FullControl, AccessControlType.Allow)); + } + else + { + // Allow the current user read/write access to the pipe. + pipeSecurity.AddAccessRule(new PipeAccessRule( + WindowsIdentity.GetCurrent().User, + PipeAccessRights.ReadWrite, AccessControlType.Allow)); + } + + outPipe = outPipeName == null + ? null + : (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( + new object[] { + outPipeName, + PipeDirection.InOut, + 1, // maxNumberOfServerInstances + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous, + 1024, // inBufferSize + 1024, // outBufferSize + pipeSecurity + }); + + return (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( + new object[] { + inOutPipeName, + PipeDirection.InOut, + 1, // maxNumberOfServerInstances + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous, + 1024, // inBufferSize + 1024, // outBufferSize + pipeSecurity + }); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs new file mode 100644 index 000000000..819ce9aa7 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs @@ -0,0 +1,215 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.IO; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Management.Automation.Runspaces; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Extensions; +using Microsoft.PowerShell.EditorServices.Host; +using Microsoft.PowerShell.EditorServices.Templates; +using Microsoft.PowerShell.EditorServices.TextDocument; +using OmniSharp.Extensions.LanguageServer.Server; +using PowerShellEditorServices.Engine.Services.Handlers; + +namespace Microsoft.PowerShell.EditorServices.Engine.Server +{ + internal abstract class PsesLanguageServer + { + protected readonly ILoggerFactory _loggerFactory; + private readonly LogLevel _minimumLogLevel; + private readonly bool _enableConsoleRepl; + private readonly HashSet _featureFlags; + private readonly HostDetails _hostDetails; + private readonly string[] _additionalModules; + private readonly PSHost _internalHost; + private readonly ProfilePaths _profilePaths; + private readonly TaskCompletionSource _serverStart; + + private ILanguageServer _languageServer; + + internal PsesLanguageServer( + ILoggerFactory factory, + LogLevel minimumLogLevel, + bool enableConsoleRepl, + HashSet featureFlags, + HostDetails hostDetails, + string[] additionalModules, + PSHost internalHost, + ProfilePaths profilePaths) + { + _loggerFactory = factory; + _minimumLogLevel = minimumLogLevel; + _enableConsoleRepl = enableConsoleRepl; + _featureFlags = featureFlags; + _hostDetails = hostDetails; + _additionalModules = additionalModules; + _internalHost = internalHost; + _profilePaths = profilePaths; + _serverStart = new TaskCompletionSource(); + } + + public async Task StartAsync() + { + _languageServer = await LanguageServer.From(options => + { + options.AddDefaultLoggingProvider(); + options.LoggerFactory = _loggerFactory; + ILogger logger = options.LoggerFactory.CreateLogger("OptionsStartup"); + options.Services = new ServiceCollection() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton( + (provider) => + GetFullyInitializedPowerShellContext( + provider.GetService(), + _profilePaths)) + .AddSingleton() + .AddSingleton() + .AddSingleton( + (provider) => + { + var extensionService = new ExtensionService( + provider.GetService(), + provider.GetService()); + extensionService.InitializeAsync( + serviceProvider: provider, + editorOperations: provider.GetService()) + .Wait(); + return extensionService; + }) + .AddSingleton( + (provider) => + { + return AnalysisService.Create( + provider.GetService(), + provider.GetService(), + options.LoggerFactory.CreateLogger()); + }); + + (Stream input, Stream output) = GetInputOutputStreams(); + + options + .WithInput(input) + .WithOutput(output); + + options.MinimumLogLevel = _minimumLogLevel; + + logger.LogInformation("Adding handlers"); + + options + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .OnInitialize( + async (languageServer, request) => + { + var serviceProvider = languageServer.Services; + var workspaceService = serviceProvider.GetService(); + + // Grab the workspace path from the parameters + workspaceService.WorkspacePath = request.RootPath; + + // Set the working directory of the PowerShell session to the workspace path + if (workspaceService.WorkspacePath != null + && Directory.Exists(workspaceService.WorkspacePath)) + { + await serviceProvider.GetService().SetWorkingDirectoryAsync( + workspaceService.WorkspacePath, + isPathAlreadyEscaped: false); + } + }); + + logger.LogInformation("Handlers added"); + }); + } + + public async Task WaitForShutdown() + { + await _serverStart.Task; + await _languageServer.WaitForExit; + } + + private PowerShellContextService GetFullyInitializedPowerShellContext( + OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer languageServer, + ProfilePaths profilePaths) + { + var logger = _loggerFactory.CreateLogger(); + + // PSReadLine can only be used when -EnableConsoleRepl is specified otherwise + // issues arise when redirecting stdio. + var powerShellContext = new PowerShellContextService( + logger, + languageServer, + _featureFlags.Contains("PSReadLine") && _enableConsoleRepl); + + EditorServicesPSHostUserInterface hostUserInterface = + _enableConsoleRepl + ? (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, _internalHost) + : new ProtocolPSHostUserInterface(languageServer, powerShellContext, logger); + + EditorServicesPSHost psHost = + new EditorServicesPSHost( + powerShellContext, + _hostDetails, + hostUserInterface, + logger); + + Runspace initialRunspace = PowerShellContextService.CreateRunspace(psHost); + powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); + + powerShellContext.ImportCommandsModuleAsync( + Path.Combine( + Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), + @"..\Commands")); + + // TODO: This can be moved to the point after the $psEditor object + // gets initialized when that is done earlier than LanguageServer.Initialize + foreach (string module in this._additionalModules) + { + var command = + new PSCommand() + .AddCommand("Microsoft.PowerShell.Core\\Import-Module") + .AddParameter("Name", module); + + powerShellContext.ExecuteCommandAsync( + command, + sendOutputToHost: false, + sendErrorToHost: true); + } + + return powerShellContext; + } + + protected abstract (Stream input, Stream output) GetInputOutputStreams(); + } +} diff --git a/src/PowerShellEditorServices.Engine/Server/StdioPsesLanguageServer.cs b/src/PowerShellEditorServices.Engine/Server/StdioPsesLanguageServer.cs new file mode 100644 index 000000000..462dcd30f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Server/StdioPsesLanguageServer.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.IO; +using System.Management.Automation.Host; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Engine.Server +{ + internal class StdioPsesLanguageServer : PsesLanguageServer + { + internal StdioPsesLanguageServer( + ILoggerFactory factory, + LogLevel minimumLogLevel, + HashSet featureFlags, + HostDetails hostDetails, + string[] additionalModules, + PSHost internalHost, + ProfilePaths profilePaths) : base( + factory, + minimumLogLevel, + // Stdio server can't support an integrated console so we pass in false. + false, + featureFlags, + hostDetails, + additionalModules, + internalHost, + profilePaths) + { + + } + + protected override (Stream input, Stream output) GetInputOutputStreams() + { + return (System.Console.OpenStandardInput(), System.Console.OpenStandardOutput()); + } + } +}